From 16910e2cf353d371d39552bc8e40e573ee4863bf Mon Sep 17 00:00:00 2001 From: Barak Igal Date: Sun, 7 May 2023 15:04:54 +0300 Subject: [PATCH] feat: vite plugin (bring code to stylable) --- package-lock.json | 167 ++++++++--- packages/vite/package.json | 45 +++ packages/vite/src/index.ts | 276 ++++++++++++++++++ packages/vite/src/plugin-utils.ts | 121 ++++++++ packages/vite/src/tsconfig.json | 14 + .../vite/test/fixtures/vite-app/index.html | 12 + .../vite/test/fixtures/vite-app/src/main.js | 6 + .../test/fixtures/vite-app/src/stuff.st.css | 3 + packages/vite/test/tsconfig.json | 15 + packages/vite/test/vite-build.spec.ts | 81 +++++ packages/vite/test/vite-dev.spec.ts | 77 +++++ tsconfig.json | 4 +- 12 files changed, 784 insertions(+), 37 deletions(-) create mode 100644 packages/vite/package.json create mode 100644 packages/vite/src/index.ts create mode 100644 packages/vite/src/plugin-utils.ts create mode 100644 packages/vite/src/tsconfig.json create mode 100644 packages/vite/test/fixtures/vite-app/index.html create mode 100644 packages/vite/test/fixtures/vite-app/src/main.js create mode 100644 packages/vite/test/fixtures/vite-app/src/stuff.st.css create mode 100644 packages/vite/test/tsconfig.json create mode 100644 packages/vite/test/vite-build.spec.ts create mode 100644 packages/vite/test/vite-dev.spec.ts diff --git a/package-lock.json b/package-lock.json index 307f4ed5f..7b16e3072 100644 --- a/package-lock.json +++ b/package-lock.json @@ -814,6 +814,10 @@ "resolved": "packages/uni-driver", "link": true }, + "node_modules/@stylable/vite": { + "resolved": "packages/vite", + "link": true + }, "node_modules/@stylable/webpack-extensions": { "resolved": "packages/webpack-extensions", "link": true @@ -4294,6 +4298,17 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -6456,6 +6471,54 @@ "resolved": "https://registry.npmjs.org/vendor-prefixes/-/vendor-prefixes-1.0.0.tgz", "integrity": "sha512-oWOptgqBs948A3V9TmAUcVFvb0dJgmeHrcIcWq4rqtmCfaRs93t0+DfJu90V5n3drN0CKBYm4BTi9yvWyKXA+g==" }, + "node_modules/vite": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", + "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", + "peer": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "node_modules/vlq": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vlq/-/vlq-2.0.4.tgz", @@ -6772,17 +6835,6 @@ "node": ">=14.14.0" } }, - "packages/cli/node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, "packages/code-formatter": { "name": "@stylable/code-formatter", "version": "5.11.0", @@ -7048,17 +7100,6 @@ "rollup": "^2.70.0 || ^3.0.0" } }, - "packages/rollup-plugin/node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, "packages/runtime": { "name": "@stylable/runtime", "version": "5.11.0", @@ -7087,6 +7128,44 @@ "node": ">=14.14.0" } }, + "packages/vite": { + "name": "@stylable/vite", + "version": "5.11.0", + "license": "MIT", + "dependencies": { + "@stylable/build-tools": "^5.11.0", + "@stylable/cli": "^5.11.0", + "@stylable/core": "^5.11.0", + "@stylable/node": "^5.11.0", + "@stylable/optimizer": "^5.11.0", + "@stylable/runtime": "^5.11.0", + "decache": "^4.6.1", + "mime": "^3.0.0" + }, + "engines": { + "node": ">=14.14.0" + }, + "peerDependencies": { + "vite": "^4.3.5" + } + }, + "packages/vite-plugin-stylable": { + "name": "@stylable/vite", + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@stylable/build-tools": "^5.11.0", + "@stylable/cli": "^5.11.0", + "@stylable/core": "^5.11.0", + "@stylable/node": "^5.11.0", + "@stylable/optimizer": "^5.11.0", + "@stylable/runtime": "^5.11.0", + "decache": "^4.6.1" + }, + "peerDependencies": { + "vite": "^4.0.0" + } + }, "packages/webpack-extensions": { "name": "@stylable/webpack-extensions", "version": "5.11.0", @@ -7527,13 +7606,6 @@ "lodash.upperfirst": "^4.3.1", "mime": "^3.0.0", "yargs": "^17.7.2" - }, - "dependencies": { - "mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" - } } }, "@stylable/code-formatter": { @@ -7699,13 +7771,6 @@ "@stylable/runtime": "^5.11.0", "decache": "^4.6.1", "mime": "^3.0.0" - }, - "dependencies": { - "mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" - } } }, "@stylable/runtime": { @@ -7721,6 +7786,19 @@ "@stylable/uni-driver": { "version": "file:packages/uni-driver" }, + "@stylable/vite": { + "version": "file:packages/vite", + "requires": { + "@stylable/build-tools": "^5.11.0", + "@stylable/cli": "^5.11.0", + "@stylable/core": "^5.11.0", + "@stylable/node": "^5.11.0", + "@stylable/optimizer": "^5.11.0", + "@stylable/runtime": "^5.11.0", + "decache": "^4.6.1", + "mime": "^3.0.0" + } + }, "@stylable/webpack-extensions": { "version": "file:packages/webpack-extensions", "requires": { @@ -10352,6 +10430,11 @@ "picomatch": "^2.3.1" } }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" + }, "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -11857,6 +11940,18 @@ "resolved": "https://registry.npmjs.org/vendor-prefixes/-/vendor-prefixes-1.0.0.tgz", "integrity": "sha512-oWOptgqBs948A3V9TmAUcVFvb0dJgmeHrcIcWq4rqtmCfaRs93t0+DfJu90V5n3drN0CKBYm4BTi9yvWyKXA+g==" }, + "vite": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", + "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", + "peer": true, + "requires": { + "esbuild": "^0.17.5", + "fsevents": "~2.3.2", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + } + }, "vlq": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vlq/-/vlq-2.0.4.tgz", diff --git a/packages/vite/package.json b/packages/vite/package.json new file mode 100644 index 000000000..546ea9e6e --- /dev/null +++ b/packages/vite/package.json @@ -0,0 +1,45 @@ +{ + "name": "@stylable/vite", + "version": "5.11.0", + "description": "Stylable plugin for Vite", + "main": "dist/index.js", + "scripts": { + "test": "mocha \"dist/test/**/*.spec.js\"" + }, + "peerDependencies": { + "vite": "^4.3.5" + }, + "dependencies": { + "@stylable/build-tools": "^5.11.0", + "@stylable/cli": "^5.11.0", + "@stylable/core": "^5.11.0", + "@stylable/node": "^5.11.0", + "@stylable/optimizer": "^5.11.0", + "@stylable/runtime": "^5.11.0", + "decache": "^4.6.1", + "mime": "^3.0.0" + }, + "files": [ + "dist", + "!dist/test", + "src", + "runtime.js", + "!*/tsconfig.{json,tsbuildinfo}" + ], + "engines": { + "node": ">=14.14.0" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "vite", + "css", + "css modules", + "vite-plugin", + "Stylable" + ], + "repository": "https://github.com/wix/stylable/tree/master/packages/vite", + "author": "Wix.com", + "license": "MIT" +} diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts new file mode 100644 index 000000000..d639ec992 --- /dev/null +++ b/packages/vite/src/index.ts @@ -0,0 +1,276 @@ +import type { PluginOption } from 'vite'; +import { hasImportedSideEffects } from '@stylable/build-tools'; +import { resolveConfig as resolveStcConfig, STCBuilder } from '@stylable/cli'; +import type { DiagnosticsMode } from '@stylable/core/dist/index-internal'; +import { emitDiagnostics, tryCollectImportsDeep } from '@stylable/core/dist/index-internal'; +import { resolveNamespace as resolveNamespaceNode } from '@stylable/node'; +import { StylableOptimizer } from '@stylable/optimizer'; +import decache from 'decache'; +import fs from 'fs'; +import { Stylable } from '@stylable/core'; +import { + emitAssets, + generateCssString, + generateStylableModuleCode, + getDefaultMode, +} from './plugin-utils'; + +export interface StylableVitePluginOptions { + optimization?: { + minify?: boolean; + }; + inlineAssets?: boolean | ((filepath: string, buffer: Buffer) => boolean); + fileName?: string; + mode?: 'development' | 'production'; + diagnosticsMode?: DiagnosticsMode; + resolveNamespace?: typeof resolveNamespaceNode; + /** + * Runs "stc" programmatically with the webpack compilation. + * true - it will automatically detect the closest "stylable.config.js" file and use it. + * string - it will use the provided string as the "stcConfig" file path. + */ + stcConfig?: boolean | string; + projectRoot?: string; +} + +const requireModuleCache = new Set(); +const requireModule = (id: string) => { + requireModuleCache.add(id); + return require(id); +}; +const clearRequireCache = () => { + for (const id of requireModuleCache) { + decache(id); + } + requireModuleCache.clear(); +}; + +const ST_CSS = '.st.css'; + +export function viteStylable({ + optimization: { minify = false } = {}, + inlineAssets = true, + // Change when WSR works without it? + diagnosticsMode = 'loose', + mode = getDefaultMode(), + resolveNamespace = resolveNamespaceNode, + stcConfig, + projectRoot = process.cwd(), +}: StylableVitePluginOptions = {}): PluginOption { + let stylable!: Stylable; + let extracted!: Map; + let emittedAssets!: Map; + let stcBuilder: STCBuilder | undefined; + + return { + enforce: 'pre', + name: 'stylable', + async buildStart() { + extracted = extracted || new Map(); + emittedAssets = emittedAssets || new Map(); + if (stylable) { + clearRequireCache(); + stylable.initCache(); + } else { + stylable = new Stylable({ + fileSystem: fs, + projectRoot, + mode, + resolveNamespace, + optimizer: new StylableOptimizer(), + resolverCache: new Map(), + requireModule, + }); + } + + if (stcConfig) { + if (stcBuilder) { + for (const sourceDirectory of stcBuilder.getProjectsSources()) { + this.addWatchFile(sourceDirectory); + } + } else { + const configuration = resolveStcConfig( + projectRoot, + typeof stcConfig === 'string' ? stcConfig : undefined + ); + + if (!configuration) { + throw new Error( + `Could not find "stcConfig"${ + typeof stcConfig === 'string' ? ` at "${stcConfig}"` : '' + }` + ); + } + + stcBuilder = STCBuilder.create({ + rootDir: projectRoot, + configFilePath: configuration.path, + watchMode: this.meta.watchMode, + }); + + await stcBuilder.build(); + + for (const sourceDirectory of stcBuilder.getProjectsSources()) { + this.addWatchFile(sourceDirectory); + } + + stcBuilder.reportDiagnostics( + { + emitWarning: (e) => this.warn(e), + emitError: (e) => this.error(e), + }, + diagnosticsMode + ); + } + } + }, + async watchChange(id) { + if (stcBuilder) { + await stcBuilder.rebuild([id]); + + stcBuilder.reportDiagnostics( + { + emitWarning: (e) => this.warn(e), + emitError: (e) => this.error(e), + }, + diagnosticsMode + ); + } + }, + load(id) { + // Strip any resource queries + const idWithoutQuery = id.split('?')[0]; + + // When loading `*.st.css.js.css` modules - + // we read the virtual css chunk we generated when transforming + if (idWithoutQuery.endsWith(`${ST_CSS}.js.css`)) { + const code = extracted.get( + // We strip away the `.css` extension and the `\0` prefix + idWithoutQuery.slice(1, -1 * '.css'.length) + ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return { code: code!.css }; + } + + // So when loading `*.st.css.js` modules - + // we read the actual `*.st.css` file so our transform alone can process it + if (idWithoutQuery.endsWith(`${ST_CSS}.js`)) { + const code = fs.readFileSync( + // We strip away the `.js` extension and the `\0` prefix + idWithoutQuery.slice(1, -1 * '.js'.length), + 'utf8' + ); + return { code, moduleSideEffects: false }; + } + return null; + }, + async resolveId(id, importer, options) { + const [idWithoutQuery, query] = id.split('?'); + + // Here we fake-resolve our virtual `*.st.css.js.css` CSS modules + // otherwise our generated `*.st.css.js.css` import fails resolution + if (idWithoutQuery.endsWith(`${ST_CSS}.js.css`)) { + const resolution = await this.resolve( + `${idWithoutQuery.slice(1, -1 * '.js.css'.length)}${query ? `?${query}` : ''}`, + importer, + { + skipSelf: true, + ...options, + } + ); + if (!resolution) { + return resolution; + } + + const [resolvedWithoutQuery, resolvedQuery] = resolution.id.split('?'); + return { + ...resolution, + id: `\0${resolvedWithoutQuery}.js.css${ + resolvedQuery ? `?${resolvedQuery}` : '' + }`, + }; + } + + // Here we reroute `*.st.css` imports to `*.st.css.js` imports. + // We do this to avoid Vite's built-in CSS plugin + // from parsing our generated ES module as CSS. + if (idWithoutQuery.endsWith(ST_CSS)) { + const resolution = await this.resolve(id, importer, { + skipSelf: true, + ...options, + }); + + if (!resolution) { + return resolution; + } + + const [resolvedWithoutQuery, query] = resolution.id.split('?'); + return { + ...resolution, + id: `\0${resolvedWithoutQuery}.js${query ? `?${query}` : ''}`, + }; + } + return null; + }, + transform(source, id) { + const [idWithoutQuery] = id.split('?'); + + // We only transform the rerouted `*.st.css.js` imports + if (!idWithoutQuery.endsWith(`${ST_CSS}.js`)) { + return null; + } + const { meta, exports } = stylable.transform( + stylable.analyze( + idWithoutQuery.slice( + // Remove our conventional `\0` prefix + 1, + // We strip away the fake `.js` extension as far as Stylable is concerned + -3 + ), + source + ) + ); + const assetsIds = emitAssets(this, stylable, meta, emittedAssets, inlineAssets); + const css = generateCssString(meta, minify, stylable, assetsIds); + const moduleImports = []; + for (const imported of meta.getImportStatements()) { + if (hasImportedSideEffects(stylable, meta, imported)) { + moduleImports.push(`import ${JSON.stringify(imported.request)};`); + } + } + extracted.set(idWithoutQuery.slice(1), { css }); + + for (const filePath of tryCollectImportsDeep(stylable.resolver, meta)) { + this.addWatchFile(filePath); + } + + /** + * In case this Stylable module has sources the diagnostics will be emitted in `watchChange` hook. + */ + if ( + !stcBuilder?.getSourcesFiles( + idWithoutQuery.slice( + // Remove our conventional `\0` prefix + 1, + // We strip away the fake `.js` extension as far as Stylable is concerned + -3 + ) + ) + ) { + emitDiagnostics( + { + emitWarning: (e: Error) => this.warn(e), + emitError: (e: Error) => this.error(e), + }, + meta, + diagnosticsMode + ); + } + + return { + code: generateStylableModuleCode(id, meta, exports, moduleImports), + map: { mappings: '' }, + }; + }, + }; +} diff --git a/packages/vite/src/plugin-utils.ts b/packages/vite/src/plugin-utils.ts new file mode 100644 index 000000000..c62841a2b --- /dev/null +++ b/packages/vite/src/plugin-utils.ts @@ -0,0 +1,121 @@ +import type { Stylable, StylableMeta } from '@stylable/core'; +import type { StylableExports } from '@stylable/core/dist/index-internal'; +import { processUrlDependencies } from '@stylable/build-tools'; +import fs from 'fs'; +import { basename, extname, isAbsolute, join } from 'path'; +import { createHash } from 'crypto'; +import mime from 'mime'; +import type { PluginContext } from 'rollup'; +import type { StylableVitePluginOptions } from './index'; + + + +export function generateStylableModuleCode( + id: string, + meta: StylableMeta, + exports: StylableExports, + moduleImports: string[] +) { + const [idWithoutQuery, query] = id.split('?'); + return ` + import { classesRuntime, statesRuntime } from '@stylable/runtime/esm/pure'; + ${moduleImports.join('\n')} + + import '${idWithoutQuery}.css${query ? `?${query}` : ''}'; + + export var namespace = ${JSON.stringify(meta.namespace)}; + export var st = classesRuntime.bind(null, namespace); + export var style = st; + export var cssStates = statesRuntime.bind(null, namespace); + export var classes = ${JSON.stringify(exports.classes)}; + export var keyframes = ${JSON.stringify(exports.keyframes)}; + export var layers = ${JSON.stringify(exports.layers)}; + export var stVars = ${JSON.stringify(exports.stVars)}; + export var vars = ${JSON.stringify(exports.vars)}; + `; +} + +export function generateCssString( + meta: StylableMeta, + minify: boolean, + stylable: Stylable, + assetsIds: string[] +) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const css = meta + .targetAst!.toString() + .replace( + /__stylable_url_asset_(.*?)__/g, + (_$0, $1) => assetsIds[Number($1)] + ); + + if (minify && stylable.optimizer) { + return stylable.optimizer.minifyCSS(css); + } + return css; +} + +export function emitAssets( + ctx: PluginContext, + stylable: Stylable, + meta: StylableMeta, + emittedAssets: Map, + inlineAssets: StylableVitePluginOptions['inlineAssets'] +): string[] { + const assets = processUrlDependencies({ + meta, + rootContext: stylable.projectRoot, + host: { + isAbsolute, + join, + }, + }); + const assetsIds: string[] = []; + for (const asset of assets) { + const fileBuffer = fs.readFileSync(asset); + const shouldInline = + typeof inlineAssets === 'function' + ? inlineAssets(asset, fileBuffer) + : inlineAssets; + + if (shouldInline) { + const mimeType = mime.getType(extname(asset)); + assetsIds.push( + `data:${mimeType};base64,${fileBuffer.toString('base64')}` + ); + } else { + const name = basename(asset); + let hash = emittedAssets.get(asset); + if (hash) { + assetsIds.push(`${hash}_${name}`); + } else { + const fileBuffer = fs.readFileSync(asset); + hash = createHash('sha1').update(fileBuffer).digest('hex'); + const fileName = `${hash}_${name}`; + if (emittedAssets.has(fileName)) { + assetsIds.push(fileName); + } else { + emittedAssets.set(fileName, hash); + emittedAssets.set(asset, hash); + assetsIds.push(fileName); + ctx.emitFile({ + type: 'asset', + fileName, + source: fileBuffer, + }); + } + } + } + } + return assetsIds; +} + +export function getDefaultMode(): 'development' | 'production' { + if (process.env.NODE_ENV === 'production') { + return 'production'; + } + if (process.env.ROLLUP_WATCH) { + return 'development'; + } + return 'production'; +} diff --git a/packages/vite/src/tsconfig.json b/packages/vite/src/tsconfig.json new file mode 100644 index 000000000..e0342d219 --- /dev/null +++ b/packages/vite/src/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../dist" + }, + "references": [ + { "path": "../../build-tools/src" }, + { "path": "../../cli/src" }, + { "path": "../../core/src" }, + { "path": "../../node/src" }, + { "path": "../../runtime/src" }, + { "path": "../../optimizer/src" } + ] +} diff --git a/packages/vite/test/fixtures/vite-app/index.html b/packages/vite/test/fixtures/vite-app/index.html new file mode 100644 index 000000000..05a4fd406 --- /dev/null +++ b/packages/vite/test/fixtures/vite-app/index.html @@ -0,0 +1,12 @@ + + + + + + Vite + React + TS + Stylable + + +
+ + + diff --git a/packages/vite/test/fixtures/vite-app/src/main.js b/packages/vite/test/fixtures/vite-app/src/main.js new file mode 100644 index 000000000..9f9b4364f --- /dev/null +++ b/packages/vite/test/fixtures/vite-app/src/main.js @@ -0,0 +1,6 @@ +import { style, classes } from './stuff.st.css'; + +const root = document.getElementById('root'); +root.className = classes.root; +root.textContent = 'This
should be blue'; +root.setAttribute('data-hook', 'target'); diff --git a/packages/vite/test/fixtures/vite-app/src/stuff.st.css b/packages/vite/test/fixtures/vite-app/src/stuff.st.css new file mode 100644 index 000000000..b0acb82c9 --- /dev/null +++ b/packages/vite/test/fixtures/vite-app/src/stuff.st.css @@ -0,0 +1,3 @@ +.root { + background-color: #1337AF; +} diff --git a/packages/vite/test/tsconfig.json b/packages/vite/test/tsconfig.json new file mode 100644 index 000000000..4fe1e9185 --- /dev/null +++ b/packages/vite/test/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../dist/test", + "types": ["node", "externals", "mocha"] + }, + "references": [ + { "path": "../../core/src" }, + { "path": "../../cli/src" }, + { "path": "../../e2e-test-kit/src" }, + { "path": "../../core-test-kit/src" }, + { "path": "../../optimizer/src" }, + { "path": "../src" } + ] +} diff --git a/packages/vite/test/vite-build.spec.ts b/packages/vite/test/vite-build.spec.ts new file mode 100644 index 000000000..0f23d3d0d --- /dev/null +++ b/packages/vite/test/vite-build.spec.ts @@ -0,0 +1,81 @@ +import { dirname } from 'path'; +import { expect } from 'chai'; +import { type UserConfig as ViteConfig, build, preview } from 'vite'; +import { viteStylable } from '@stylable/vite'; +import playwright from 'playwright-core'; + +const project = 'vite-app'; +const projectDir = dirname(require.resolve(`@stylable/vite/test/fixtures/${project}/index.html`)); + +const viteConfig: ViteConfig = { + root: projectDir, + plugins: [viteStylable()], + logLevel: 'silent', + clearScreen: false, +}; + +async function viteBuildAndPreview() { + await build({ + configFile: false, + ...viteConfig, + }); + const vitePreviewServer = await preview({ + configFile: false, + ...viteConfig, + }); + + const viteAddress = vitePreviewServer.httpServer?.address(); + + if (!viteAddress) { + throw new Error('no preview server url for some reason'); + } + const url = + typeof viteAddress === 'string' ? viteAddress : `http://localhost:${viteAddress.port}/`; + + return { + stop() { + vitePreviewServer.httpServer.close(); + return Promise.resolve(); + }, + url, + }; +} + +describe('vite build', () => { + let vitePreviewServer: Awaited> | undefined; + const disposable = new Set<() => Promise | void>(); + before(async () => { + vitePreviewServer = await viteBuildAndPreview(); + }); + + after(async () => { + for (const dispose of disposable) { + await dispose(); + } + await vitePreviewServer?.stop(); + }); + + it('should render stylable-styled content in `vite build && vite preview`', async () => { + const page = await open(vitePreviewServer!.url, disposable); + + const bg = await page.evaluate(() => { + const elm = document.querySelector('[data-hook="target"]')!; + return window.getComputedStyle(elm).getPropertyValue('background-color'); + }); + + expect(bg).to.equal('rgb(19, 55, 175)'); + }); +}); + +async function open(url: string, dispose: Set<() => Promise | void>) { + const launchOptions = {}; + const browser = process.env.PLAYWRIGHT_SERVER + ? await playwright.chromium.connect(process.env.PLAYWRIGHT_SERVER, launchOptions) + : await playwright.chromium.launch(launchOptions); + + const browserContext = await browser.newContext(); + const page = await browserContext.newPage(); + await page.goto(url, { waitUntil: 'networkidle' }); + dispose.add(() => browser.close()); + return page; +} diff --git a/packages/vite/test/vite-dev.spec.ts b/packages/vite/test/vite-dev.spec.ts new file mode 100644 index 000000000..aac750ff9 --- /dev/null +++ b/packages/vite/test/vite-dev.spec.ts @@ -0,0 +1,77 @@ +import { dirname } from 'path'; +import { expect } from 'chai'; +import type { UserConfig as ViteConfig } from 'vite'; +import { createServer } from 'vite'; +import { viteStylable } from '@stylable/vite'; +import playwright from 'playwright-core'; + +const project = 'vite-app'; +const projectDir = dirname(require.resolve(`@stylable/vite/test/fixtures/${project}/index.html`)); + +const viteConfig: ViteConfig = { + root: projectDir, + plugins: [viteStylable()], + logLevel: 'silent', + clearScreen: false, +}; + +async function viteDev() { + const viteServer = await createServer({ + configFile: false, + ...viteConfig, + }); + await viteServer.listen(); + const viteAddress = viteServer.httpServer?.address(); + + if (!viteAddress) { + throw new Error('no dev server url for some reason'); + } + const url = + typeof viteAddress === 'string' ? viteAddress : `http://localhost:${viteAddress.port}/`; + + return { + async stop() { + await viteServer.close(); + }, + url, + }; +} + +describe('vite dev', () => { + let viteDevServer: Awaited> | undefined; + const disposable = new Set<() => Promise | void>(); + before(async () => { + viteDevServer = await viteDev(); + }); + + after(async () => { + for (const dispose of disposable) { + await dispose(); + } + await viteDevServer?.stop(); + }); + + it('should render stylable-styled content in `vite dev`', async () => { + const page = await open(viteDevServer!.url, disposable); + + const bg = await page.evaluate(() => { + const elm = document.querySelector('[data-hook="target"]')!; + return window.getComputedStyle(elm).getPropertyValue('background-color'); + }); + + expect(bg).to.equal('rgb(19, 55, 175)'); + }); +}); + +async function open(url: string, dispose: Set<() => Promise | void>) { + const launchOptions = {}; + const browser = process.env.PLAYWRIGHT_SERVER + ? await playwright.chromium.connect(process.env.PLAYWRIGHT_SERVER, launchOptions) + : await playwright.chromium.launch(launchOptions); + + const browserContext = await browser.newContext(); + const page = await browserContext.newPage(); + await page.goto(url, { waitUntil: 'networkidle' }); + dispose.add(() => browser.close()); + return page; +} diff --git a/tsconfig.json b/tsconfig.json index 12f2f4e44..e88afa6e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -45,6 +45,8 @@ { "path": "./packages/webpack-plugin/src" }, { "path": "./packages/webpack-plugin/test" }, { "path": "./packages/esbuild/src" }, - { "path": "./packages/esbuild/test" } + { "path": "./packages/esbuild/test" }, + { "path": "./packages/vite/src" }, + { "path": "./packages/vite/test" } ] }