diff --git a/.gitignore b/.gitignore index 64aa2a7..87d6015 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ node_modules/ dist/ dist-webpack/ dist-auto-stable/ +dist-bridge/ +dist-bridge-webpack/ coverage/ .c8/ .duel-cache/ diff --git a/docs/loader.md b/docs/loader.md index a302090..81b14a9 100644 --- a/docs/loader.md +++ b/docs/loader.md @@ -57,6 +57,71 @@ Pass `autoStable` to duplicate every matching class selector with a deterministi - CSS Modules: exports and generated class strings include both the hashed class and the stable class so you can reference either at runtime. - `autoStable` forces a LightningCSS pass; use `include`/`exclude` to scope which class tokens are duplicated. +### Bridge loader (CSS Modules) + +When you want `knightedCss` to reflect the **hashed class names produced by your existing +CSS Modules pipeline**, use the companion loader `@knighted/css/loader-bridge`. It runs +after your Sass/CSS modules loaders and simply wraps their output (no reprocessing). + +```js +// rspack.config.js or webpack.config.js +export default { + module: { + rules: [ + { + test: /\.module\.(css|scss|sass)$/, + oneOf: [ + { + resourceQuery: /knighted-css/, + use: [ + { + loader: '@knighted/css/loader-bridge', + }, + { + loader: 'css-loader', + options: { + exportType: 'string', + modules: true, + }, + }, + 'sass-loader', + ], + }, + { + use: [ + { + loader: 'css-loader', + options: { + modules: true, + }, + }, + 'sass-loader', + ], + }, + ], + }, + ], + }, +} +``` + +```ts +import { knightedCss, knightedCssModules } from './card.module.scss?knighted-css' + +// knightedCss uses the same hashed selectors as the DOM +shadowRoot.adoptedStyleSheets[0].replaceSync(knightedCss) + +// optional convenience mapping of the CSS module locals +console.log(knightedCssModules) +``` + +> [!NOTE] +> The bridge loader does not generate `stableSelectors`. It simply re-exports the +> upstream output and resolves `knightedCss` by calling the upstream module’s +> `toString()` or using its default string export when present. For CSS Modules +> pipelines, configure the `?knighted-css` branch to emit a string (for example, +> `css-loader` with `exportType: 'string'`) so `knightedCss` is populated. + ### Combined imports Need the component exports **and** the compiled CSS from a single import? Use `?knighted-css&combined` and narrow the result with `KnightedCssCombinedModule` to keep TypeScript happy: diff --git a/package-lock.json b/package-lock.json index 967c285..0ff23b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,7 +74,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2496,7 +2495,6 @@ "integrity": "sha512-FolcIAH5FW4J2FET+qwjd1kNeFbCkd0VLuIHO0thyolEjaPSxw5qxG67DA7BZGm6PVcoiSgPLks1DL6eZ8c+fA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.21.6", "@rspack/binding": "1.6.8", @@ -2541,6 +2539,244 @@ "dev": true, "license": "MIT" }, + "node_modules/@swc/core": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.10.tgz", + "integrity": "sha512-udNofxftduMUEv7nqahl2nvodCiCDQ4Ge0ebzsEm6P8s0RC2tBM0Hqx0nNF5J/6t9uagFJyWIDjXy3IIWMHDJw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.10", + "@swc/core-darwin-x64": "1.15.10", + "@swc/core-linux-arm-gnueabihf": "1.15.10", + "@swc/core-linux-arm64-gnu": "1.15.10", + "@swc/core-linux-arm64-musl": "1.15.10", + "@swc/core-linux-x64-gnu": "1.15.10", + "@swc/core-linux-x64-musl": "1.15.10", + "@swc/core-win32-arm64-msvc": "1.15.10", + "@swc/core-win32-ia32-msvc": "1.15.10", + "@swc/core-win32-x64-msvc": "1.15.10" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.10.tgz", + "integrity": "sha512-U72pGqmJYbjrLhMndIemZ7u9Q9owcJczGxwtfJlz/WwMaGYAV/g4nkGiUVk/+QSX8sFCAjanovcU1IUsP2YulA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.10.tgz", + "integrity": "sha512-NZpDXtwHH083L40xdyj1sY31MIwLgOxKfZEAGCI8xHXdHa+GWvEiVdGiu4qhkJctoHFzAEc7ZX3GN5phuJcPuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.10.tgz", + "integrity": "sha512-ioieF5iuRziUF1HkH1gg1r93e055dAdeBAPGAk40VjqpL5/igPJ/WxFHGvc6WMLhUubSJI4S0AiZAAhEAp1jDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.10.tgz", + "integrity": "sha512-tD6BClOrxSsNus9cJL7Gxdv7z7Y2hlyvZd9l0NQz+YXzmTWqnfzLpg16ovEI7gknH2AgDBB5ywOsqu8hUgSeEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.10.tgz", + "integrity": "sha512-4uAHO3nbfbrTcmO/9YcVweTQdx5fN3l7ewwl5AEK4yoC4wXmoBTEPHAVdKNe4r9+xrTgd4BgyPsy0409OjjlMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.10.tgz", + "integrity": "sha512-W0h9ONNw1pVIA0cN7wtboOSTl4Jk3tHq+w2cMPQudu9/+3xoCxpFb9ZdehwCAk29IsvdWzGzY6P7dDVTyFwoqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.10.tgz", + "integrity": "sha512-XQNZlLZB62S8nAbw7pqoqwy91Ldy2RpaMRqdRN3T+tAg6Xg6FywXRKCsLh6IQOadr4p1+lGnqM/Wn35z5a/0Vw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.10.tgz", + "integrity": "sha512-qnAGrRv5Nj/DATxAmCnJQRXXQqnJwR0trxLndhoHoxGci9MuguNIjWahS0gw8YZFjgTinbTxOwzatkoySihnmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.10.tgz", + "integrity": "sha512-i4X/q8QSvzVlaRtv1xfnfl+hVKpCfiJ+9th484rh937fiEZKxZGf51C+uO0lfKDP1FfnT6C1yBYwHy7FLBVXFw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.10.tgz", + "integrity": "sha512-HvY8XUFuoTXn6lSccDLYFlXv1SU/PzYi4PyUqGT++WfTnbw/68N/7BdUZqglGRwiSqr0qhYt/EhmBpULj0J9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@ts-graphviz/adapter": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@ts-graphviz/adapter/-/adapter-2.0.6.tgz", @@ -2829,7 +3065,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3076,7 +3311,6 @@ "integrity": "sha512-/p0dwOjr0o8gE5BRQ5O9P0u/2DjUd6Zfga2JGmE4KaY7ZITWMszTzk4x4CPlM5cKkRr2ZGzbE6XkuPNfp9shSQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@emotion/hash": "^0.9.0", "@vanilla-extract/private": "^1.0.9", @@ -3098,7 +3332,6 @@ "integrity": "sha512-ILob4F9cEHXpbWAVt3Y2iaQJpqYq/c/5TJC8Fz58C2XmX3QW2Y589krvViiyJhQfydCGK3EbwPQhVFjQaBeKfg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.23.9", "@babel/plugin-syntax-typescript": "^7.23.3", @@ -3560,7 +3793,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3600,7 +3832,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3955,7 +4186,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4649,6 +4879,55 @@ "node": ">= 8" } }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/css-what": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", @@ -6273,6 +6552,19 @@ "node": ">=0.10.0" } }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6821,7 +7113,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -7275,6 +7566,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mini-css-extract-plugin": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.0.tgz", + "integrity": "sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -8360,7 +8672,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8370,6 +8681,90 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/postcss-values-parser": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz", @@ -8602,7 +8997,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8892,7 +9286,6 @@ "integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -9745,6 +10138,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swc-loader": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.7.tgz", + "integrity": "sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "peerDependencies": { + "@swc/core": "^1.2.147", + "webpack": ">=2" + } + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -9992,7 +10399,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10089,50 +10495,6 @@ "node": ">=18" } }, - "node_modules/ts-loader": { - "version": "9.5.4", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", - "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -10151,8 +10513,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.21.0", @@ -10210,7 +10571,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10419,7 +10779,6 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -10528,7 +10887,6 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -11073,7 +11431,7 @@ }, "packages/css": { "name": "@knighted/css", - "version": "1.1.0-rc.5", + "version": "1.1.0-rc.6", "license": "MIT", "dependencies": { "es-module-lexer": "^2.0.0", @@ -11345,7 +11703,7 @@ "name": "@knighted/css-playwright-fixture", "version": "0.0.0", "dependencies": { - "@knighted/css": "1.1.0-rc.5", + "@knighted/css": "1.1.0-rc.6", "@knighted/jsx": "^1.7.3", "lit": "^3.2.1", "react": "^19.0.0", @@ -11355,7 +11713,9 @@ "@types/react": "^19.0.4", "@types/react-dom": "^19.0.2", "@vanilla-extract/webpack-plugin": "^2.3.15", - "ts-loader": "^9.5.1", + "css-loader": "^7.1.2", + "mini-css-extract-plugin": "^2.10.0", + "swc-loader": "^0.2.7", "webpack": "^5.97.1", "webpack-cli": "^5.1.4" } diff --git a/packages/css/loader-queries.d.ts b/packages/css/loader-queries.d.ts index b601908..9e562c9 100644 --- a/packages/css/loader-queries.d.ts +++ b/packages/css/loader-queries.d.ts @@ -4,6 +4,7 @@ */ declare module '*?knighted-css' { export const knightedCss: string + export const knightedCssModules: Readonly> | undefined export default knightedCss } @@ -11,6 +12,7 @@ type KnightedCssStableSelectorMap = Readonly> declare module '*?knighted-css&types' { export const knightedCss: string + export const knightedCssModules: Readonly> | undefined export const stableSelectors: KnightedCssStableSelectorMap export default knightedCss } @@ -32,24 +34,28 @@ declare module '*?knighted-css&combined' { const combined: KnightedCssCombinedModule> export default combined export const knightedCss: string + export const knightedCssModules: Readonly> | undefined } declare module '*?knighted-css&combined&named-only' { const combined: KnightedCssCombinedModule> export default combined export const knightedCss: string + export const knightedCssModules: Readonly> | undefined } declare module '*?knighted-css&combined&no-default' { const combined: KnightedCssCombinedModule> export default combined export const knightedCss: string + export const knightedCssModules: Readonly> | undefined } declare module '*?knighted-css&combined&types' { const combined: KnightedCssCombinedModule> export default combined export const knightedCss: string + export const knightedCssModules: Readonly> | undefined export const stableSelectors: KnightedCssStableSelectorMap } @@ -57,6 +63,7 @@ declare module '*?knighted-css&combined&named-only&types' { const combined: KnightedCssCombinedModule> export default combined export const knightedCss: string + export const knightedCssModules: Readonly> | undefined export const stableSelectors: KnightedCssStableSelectorMap } @@ -64,5 +71,6 @@ declare module '*?knighted-css&combined&no-default&types' { const combined: KnightedCssCombinedModule> export default combined export const knightedCss: string + export const knightedCssModules: Readonly> | undefined export const stableSelectors: KnightedCssStableSelectorMap } diff --git a/packages/css/package.json b/packages/css/package.json index 6b128eb..b1c75fd 100644 --- a/packages/css/package.json +++ b/packages/css/package.json @@ -1,6 +1,6 @@ { "name": "@knighted/css", - "version": "1.1.0-rc.5", + "version": "1.1.0-rc.6", "description": "A build-time utility that traverses JavaScript/TypeScript module dependency graphs to extract, compile, and optimize all imported CSS into a single, in-memory string.", "type": "module", "main": "./dist/css.js", @@ -10,6 +10,9 @@ "loader": [ "./dist/loader.d.ts" ], + "loader-bridge": [ + "./dist/loaderBridge.d.ts" + ], "loader-helpers": [ "./dist/loader-helpers.d.ts" ], @@ -35,6 +38,11 @@ "import": "./dist/loader.js", "require": "./dist/cjs/loader.cjs" }, + "./loader-bridge": { + "types": "./dist/loaderBridge.d.ts", + "import": "./dist/loaderBridge.js", + "require": "./dist/cjs/loaderBridge.cjs" + }, "./loader-helpers": { "types": "./dist/loader-helpers.d.ts", "import": "./dist/loader-helpers.js", diff --git a/packages/css/src/loaderBridge.ts b/packages/css/src/loaderBridge.ts new file mode 100644 index 0000000..65725a6 --- /dev/null +++ b/packages/css/src/loaderBridge.ts @@ -0,0 +1,425 @@ +import path from 'node:path' + +import type { + LoaderContext, + LoaderDefinitionFunction, + PitchLoaderDefinitionFunction, +} from 'webpack' + +import { + buildSanitizedQuery, + hasCombinedQuery, + hasNamedOnlyQueryFlag, + hasQueryFlag, + shouldEmitCombinedDefault, + TYPES_QUERY_FLAG, +} from './loaderInternals.js' + +export interface KnightedCssBridgeLoaderOptions { + emitCssModules?: boolean +} + +type BridgeModuleLike = { + default?: unknown + locals?: Record +} + +const DEFAULT_EXPORT_NAME = 'knightedCss' + +const loader: LoaderDefinitionFunction = function loader( + source, +) { + return source +} + +export const pitch: PitchLoaderDefinitionFunction = + function pitch(remainingRequest) { + if (isJsLikeResource(this.resourcePath) && hasCombinedQuery(this.resourceQuery)) { + const callback = this.async() + if (!callback) { + return createCombinedJsBridgeModuleSync(this, remainingRequest) + } + readResourceSource(this) + .then(source => { + const cssRequests = collectCssModuleRequests(source).map(request => + buildBridgeCssRequest(request), + ) + const upstreamRequest = buildUpstreamRequest(remainingRequest) + callback( + null, + createCombinedJsBridgeModule({ + upstreamRequest: upstreamRequest || '', + cssRequests, + emitDefault: false, + }), + ) + }) + .catch(error => callback(error as Error)) + return + } + const localsRequest = buildProxyRequest(this) + const upstreamRequest = buildUpstreamRequest(remainingRequest) + const { emitCssModules } = resolveLoaderOptions(this) + const combined = hasCombinedQuery(this.resourceQuery) + const skipSyntheticDefault = hasNamedOnlyQueryFlag(this.resourceQuery) + + if (hasQueryFlag(this.resourceQuery, TYPES_QUERY_FLAG)) { + emitKnightedWarning( + this, + 'The bridge loader does not generate stableSelectors. Remove the "types" query flag.', + ) + } + + const emitDefault = combined + ? shouldEmitCombinedDefault({ + detection: 'unknown', + request: localsRequest, + skipSyntheticDefault, + }) + : false + + return createBridgeModule({ + localsRequest, + upstreamRequest: upstreamRequest || localsRequest, + combined, + emitDefault, + emitCssModules, + }) + } +;(loader as LoaderDefinitionFunction & { pitch?: typeof pitch }).pitch = pitch + +export default loader + +function resolveLoaderOptions( + ctx: LoaderContext, +): Required { + const rawOptions = ( + typeof ctx.getOptions === 'function' ? ctx.getOptions() : {} + ) as KnightedCssBridgeLoaderOptions + return { + emitCssModules: rawOptions.emitCssModules !== false, + } +} + +function readResourceSource( + ctx: LoaderContext, +): Promise { + return new Promise((resolve, reject) => { + ctx.fs.readFile(ctx.resourcePath, (error, data) => { + if (error) { + reject(error) + return + } + if (!data) { + reject(new Error(`Unable to read ${ctx.resourcePath}`)) + return + } + resolve(data.toString('utf8')) + }) + }) +} + +function collectCssModuleRequests(source: string): string[] { + const matches = new Set() + const importPattern = + /(?:import|export)\s+(?:[^'"\n]+\s+from\s+)?['"]([^'"\n]+?\.module\.(?:css|scss|sass|less)(?:\?[^'"\n]+)?)['"]/g + let match: RegExpExecArray | null + while ((match = importPattern.exec(source))) { + if (match[1]) { + matches.add(match[1]) + } + } + return Array.from(matches) +} + +function buildBridgeCssRequest(specifier: string): string { + if (specifier.includes('knighted-css')) { + return specifier + } + const [resource, query] = specifier.split('?') + if (query) { + return `${resource}?${query}&knighted-css` + } + return `${specifier}?knighted-css` +} + +interface CombinedJsBridgeOptions { + upstreamRequest: string + cssRequests: string[] + emitDefault: boolean +} + +function createCombinedJsBridgeModuleSync( + ctx: LoaderContext, + remainingRequest?: string, +): string { + const upstreamRequest = buildUpstreamRequest(remainingRequest) + return createCombinedJsBridgeModule({ + upstreamRequest: upstreamRequest || '', + cssRequests: [], + emitDefault: false, + }) +} + +function createCombinedJsBridgeModule(options: CombinedJsBridgeOptions): string { + const upstreamLiteral = JSON.stringify(options.upstreamRequest) + const cssImports = options.cssRequests.map((request, index) => { + const literal = JSON.stringify(request) + return `import { knightedCss as __knightedCss${index}, knightedCssModules as __knightedCssModules${index} } from ${literal};` + }) + const cssValues = options.cssRequests.map((_, index) => `__knightedCss${index}`) + const cssModulesValues = options.cssRequests.map( + (_, index) => `__knightedCssModules${index}`, + ) + const lines = [ + `import * as __knightedUpstream from ${upstreamLiteral};`, + ...cssImports, + options.emitDefault + ? "const __knightedDefault = Object.prototype.hasOwnProperty.call(__knightedUpstream, 'default') ? __knightedUpstream['default'] : undefined;" + : '', + `const __knightedCss = [${cssValues.join(', ')}].filter(Boolean).join('\\n');`, + `const __knightedCssModules = Object.assign({}, ...[${cssModulesValues.join( + ', ', + )}].filter(Boolean));`, + `export const ${DEFAULT_EXPORT_NAME} = __knightedCss;`, + 'export const knightedCssModules = __knightedCssModules;', + `export * from ${upstreamLiteral};`, + ] + if (options.emitDefault) { + lines.push('export default __knightedDefault;') + } + return lines.filter(Boolean).join('\n') +} + +function isJsLikeResource(resourcePath: string): boolean { + return /\.[cm]?[jt]sx?$/.test(resourcePath) +} + +function resolveCssText(primary: unknown, module?: BridgeModuleLike): string { + const candidates: unknown[] = [primary, module, module?.default] + for (const candidate of candidates) { + if (typeof candidate === 'string') { + return candidate + } + if ( + candidate && + typeof (candidate as { toString?: unknown }).toString === 'function' + ) { + const text = String((candidate as { toString: () => string }).toString()) + if (text && text !== '[object Object]' && text !== '[object Module]') { + return text + } + } + } + return '' +} + +function resolveCssModules( + primary: unknown, + module?: BridgeModuleLike, +): Record | undefined { + const candidates: unknown[] = [primary, module, module?.default] + for (const candidate of candidates) { + if (!candidate || typeof candidate !== 'object') continue + if (!('locals' in candidate)) continue + const locals = (candidate as { locals?: unknown }).locals + if (!locals || typeof locals !== 'object') continue + return locals as Record + } + const isStringMapLocal = (value: object): value is Record => { + const entries = Object.entries(value) + if (entries.length === 0) return false + return entries.every(([, entry]) => typeof entry === 'string') + } + for (const candidate of candidates) { + if (!candidate || typeof candidate !== 'object') continue + if (isStringMapLocal(candidate)) return candidate + } + const collectNamedExportsLocal = ( + value: unknown, + ): Record | undefined => { + if (!value || typeof value !== 'object') return undefined + const output: Record = {} + for (const [key, entry] of Object.entries(value as Record)) { + if (key === 'default' || key === '__esModule') continue + if (typeof entry === 'string') { + output[key] = entry + } + } + return Object.keys(output).length > 0 ? output : undefined + } + return collectNamedExportsLocal(module) +} + + + +interface BridgeModuleOptions { + localsRequest: string + upstreamRequest: string + combined: boolean + emitDefault: boolean + emitCssModules: boolean +} + +function createBridgeModule(options: BridgeModuleOptions): string { + const localsLiteral = JSON.stringify(options.localsRequest) + const upstreamLiteral = JSON.stringify(options.upstreamRequest) + const lines = [ + `import * as __knightedLocals from ${localsLiteral};`, + `import * as __knightedUpstream from ${upstreamLiteral};`, + `const __knightedDefault =\ntypeof __knightedUpstream.default !== 'undefined'\n ? __knightedUpstream.default\n : __knightedUpstream;`, + `const __knightedResolveCss = ${resolveCssText.toString()};`, + `const __knightedResolveCssModules = ${resolveCssModules.toString()};`, + `const __knightedLocalsExport =\n __knightedResolveCssModules(__knightedLocals, __knightedLocals) ??\n __knightedLocals;`, + `const __knightedCss = __knightedResolveCss(__knightedDefault, __knightedUpstream);`, + `export const ${DEFAULT_EXPORT_NAME} = __knightedCss;`, + ] + + if (options.emitCssModules) { + lines.push( + `const __knightedCssModules = __knightedLocalsExport ?? __knightedResolveCssModules(\n __knightedDefault,\n __knightedUpstream,\n);`, + 'export const knightedCssModules = __knightedCssModules;', + ) + } + + if (options.combined) { + lines.push(`export * from ${localsLiteral};`) + if (options.emitDefault) { + lines.push('export default __knightedLocalsExport;') + } + } else { + lines.push('export default __knightedCss;') + } + + return lines.join('\n') +} + +function buildUpstreamRequest(remainingRequest?: string): string { + if (!remainingRequest) { + return '' + } + const request = remainingRequest.startsWith('!') + ? remainingRequest + : `!!${remainingRequest}` + return request +} + +function buildProxyRequest(ctx: LoaderContext): string { + const sanitizedQuery = buildSanitizedQuery(ctx.resourceQuery) + const rawRequest = getRawRequest(ctx) + if (rawRequest) { + return rebuildProxyRequestFromRaw(ctx, rawRequest, sanitizedQuery) + } + const request = `${ctx.resourcePath}${sanitizedQuery}` + return contextifyRequest(ctx, request) +} + +function rebuildProxyRequestFromRaw( + ctx: LoaderContext, + rawRequest: string, + sanitizedQuery: string, +): string { + const stripped = stripResourceQuery(rawRequest) + const loaderDelimiter = stripped.lastIndexOf('!') + const loaderPrefix = loaderDelimiter >= 0 ? stripped.slice(0, loaderDelimiter + 1) : '' + let resource = loaderDelimiter >= 0 ? stripped.slice(loaderDelimiter + 1) : stripped + if (isRelativeSpecifier(resource)) { + resource = makeResourceRelativeToContext(ctx, ctx.resourcePath) + } + return `${loaderPrefix}${resource}${sanitizedQuery}` +} + +function getRawRequest( + ctx: LoaderContext, +): string | undefined { + const mod = ( + ctx as LoaderContext & { + _module?: { rawRequest?: string } + } + )._module + const request = mod?.rawRequest + if (typeof request === 'string' && request.length > 0) { + return request + } + return undefined +} + +function stripResourceQuery(request: string): string { + const idx = request.indexOf('?') + return idx >= 0 ? request.slice(0, idx) : request +} + +function contextifyRequest( + ctx: LoaderContext, + request: string, +): string { + const context = ctx.context ?? ctx.rootContext ?? process.cwd() + if (ctx.utils && typeof ctx.utils.contextify === 'function') { + return ctx.utils.contextify(context, request) + } + return rebuildRelativeRequest(context, request) +} + +function rebuildRelativeRequest(context: string, request: string): string { + const queryIndex = request.indexOf('?') + const resourcePath = queryIndex >= 0 ? request.slice(0, queryIndex) : request + const query = queryIndex >= 0 ? request.slice(queryIndex) : '' + const relative = ensureDotPrefixedRelative( + path.relative(context, resourcePath), + resourcePath, + ) + return `${relative}${query}` +} + +function makeResourceRelativeToContext( + ctx: LoaderContext, + resourcePath: string, +): string { + const context = ctx.context ?? path.dirname(resourcePath) + if (ctx.utils && typeof ctx.utils.contextify === 'function') { + const result = ctx.utils.contextify(context, resourcePath) + return stripResourceQuery(result) + } + return ensureDotPrefixedRelative(path.relative(context, resourcePath), resourcePath) +} + +function ensureDotPrefixedRelative(relativePath: string, resourcePath: string): string { + const fallback = relativePath.length > 0 ? relativePath : path.basename(resourcePath) + const normalized = normalizeToPosix(fallback) + if (normalized.startsWith('./') || normalized.startsWith('../')) { + return normalized + } + return `./${normalized}` +} + +function normalizeToPosix(filePath: string): string { + return filePath.split(path.sep).join('/') +} + +function isRelativeSpecifier(specifier: string): boolean { + return specifier.startsWith('./') || specifier.startsWith('../') +} + +function emitKnightedWarning( + ctx: LoaderContext, + message: string, +): void { + const formatted = `\x1b[33m@knighted/css warning\x1b[0m ${message}` + if (typeof ctx.emitWarning === 'function') { + ctx.emitWarning(new Error(formatted)) + return + } + // eslint-disable-next-line no-console + console.warn(formatted) +} + +export const __loaderBridgeInternals = { + collectCssModuleRequests, + buildBridgeCssRequest, + createCombinedJsBridgeModule, + isJsLikeResource, + resolveCssModules, + resolveCssText, + buildProxyRequest, + createBridgeModule, +} diff --git a/packages/css/test/loaderBridge.test.ts b/packages/css/test/loaderBridge.test.ts new file mode 100644 index 0000000..f5a7cdc --- /dev/null +++ b/packages/css/test/loaderBridge.test.ts @@ -0,0 +1,375 @@ +import assert from 'node:assert/strict' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import test from 'node:test' +import { + __loaderBridgeInternals, + pitch, + type KnightedCssBridgeLoaderOptions, +} from '../src/loaderBridge.js' +import type { LoaderContext } from 'webpack' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +type MockLoaderContext = Partial> & { + warnings: string[] + _module?: { rawRequest?: string } +} + +function createMockContext( + overrides: Partial> = {}, +): MockLoaderContext { + const warnings: string[] = [] + return { + resourcePath: + overrides.resourcePath ?? + path.resolve(__dirname, 'fixtures/dialects/basic/styles.css'), + rootContext: overrides.rootContext ?? path.resolve(__dirname, '..'), + getOptions: overrides.getOptions ?? (() => ({})), + resourceQuery: overrides.resourceQuery, + context: overrides.context, + utils: overrides.utils, + loadModule: + overrides.loadModule as LoaderContext['loadModule'], + emitWarning: err => warnings.push(err.message), + warnings, + ...overrides, + } +} + +/* + * Helper to call pitch loader with only the required remainingRequest parameter. + * The pitch function's signature requires previousRequest and data parameters per + * the webpack PitchLoaderDefinitionFunction interface, but our implementation + * doesn't use them, so we omit them in tests for clarity. + */ +function callPitch( + ctx: LoaderContext, + remainingRequest: string, +): ReturnType { + return (pitch as (this: typeof ctx, remainingRequest: string) => ReturnType).call( + ctx, + remainingRequest, + ) +} + +test('resolveCssText prefers string default export', () => { + const module = { default: '.card{color:red}' } + assert.equal( + __loaderBridgeInternals.resolveCssText(module.default, module), + '.card{color:red}', + ) +}) + +test('resolveCssText falls back to toString result', () => { + const module = { + default: { + toString: () => '.badge{display:block}', + }, + } + assert.equal( + __loaderBridgeInternals.resolveCssText(module.default, module), + '.badge{display:block}', + ) +}) + +test('resolveCssText handles cjs module default', () => { + const module = { + toString: () => '.pill{padding:4px}', + } + const bridgeModule = module as unknown as { + default?: unknown + locals?: Record + } + assert.equal( + __loaderBridgeInternals.resolveCssText(bridgeModule, bridgeModule), + '.pill{padding:4px}', + ) +}) + +test('resolveCssText ignores object string coercions', () => { + const module = { + default: { + toString: () => '[object Module]', + }, + } + assert.equal(__loaderBridgeInternals.resolveCssText(module.default, module), '') +}) + +test('resolveCssModules finds locals on default export', () => { + const module = { + default: { + locals: { panel: 'panel_hash' }, + }, + } + assert.deepEqual(__loaderBridgeInternals.resolveCssModules(module.default, module), { + panel: 'panel_hash', + }) +}) + +test('resolveCssModules finds locals on module export', () => { + const module = { + locals: { card: 'card_hash' }, + } + assert.deepEqual(__loaderBridgeInternals.resolveCssModules(module, module), { + card: 'card_hash', + }) +}) + +test('pitch returns combined module wrapper when combined flag is present', async () => { + const ctx = createMockContext({ + resourceQuery: '?knighted-css&combined', + _module: { + rawRequest: './styles.module.css?knighted-css&combined', + } as unknown as LoaderContext['_module'], + }) + + const result = await callPitch( + ctx as LoaderContext, + './styles.module.css?knighted-css&combined', + ) + + const output = String(result ?? '') + assert.match(output, /export \* from/) + assert.match(output, /export default __knightedLocalsExport/) + assert.match(output, /export const knightedCss = /) +}) + +test('pitch omits knightedCssModules when emitCssModules is false', async () => { + const ctx = createMockContext({ + resourceQuery: '?knighted-css', + getOptions: () => ({ emitCssModules: false }), + }) + + const result = await callPitch( + ctx as LoaderContext, + './styles.module.css?knighted-css', + ) + + const output = String(result ?? '') + assert.match(output, /export default __knightedCss/) + assert.ok(!/knightedCssModules/.test(output)) +}) + +test('pitch warns when types query is used', async () => { + const ctx = createMockContext({ + resourceQuery: '?knighted-css&types', + }) + + await callPitch( + ctx as LoaderContext, + './styles.module.css?knighted-css&types', + ) + + assert.equal(ctx.warnings.length, 1) + assert.match(ctx.warnings[0] ?? '', /does not generate stableSelectors/) +}) + +test('collectCssModuleRequests finds css module imports', () => { + const source = ` + import styles from './card.module.css' + import './other.module.scss?inline' + export { tokens } from "./tokens.module.less" + import './global.css' + ` + assert.deepEqual(__loaderBridgeInternals.collectCssModuleRequests(source).sort(), [ + './card.module.css', + './other.module.scss?inline', + './tokens.module.less', + ]) +}) + +test('buildBridgeCssRequest appends knighted-css query', () => { + assert.equal( + __loaderBridgeInternals.buildBridgeCssRequest('./card.module.css'), + './card.module.css?knighted-css', + ) + assert.equal( + __loaderBridgeInternals.buildBridgeCssRequest('./card.module.css?inline'), + './card.module.css?inline&knighted-css', + ) + assert.equal( + __loaderBridgeInternals.buildBridgeCssRequest('./card.module.css?knighted-css'), + './card.module.css?knighted-css', + ) +}) + +test('isJsLikeResource detects js/ts resources', () => { + assert.equal(__loaderBridgeInternals.isJsLikeResource('file.tsx'), true) + assert.equal(__loaderBridgeInternals.isJsLikeResource('file.css'), false) +}) + +test('createCombinedJsBridgeModule joins css strings with newline', () => { + const output = __loaderBridgeInternals.createCombinedJsBridgeModule({ + upstreamRequest: '!!./card.js?knighted-css&combined', + cssRequests: ['./card.module.css?knighted-css'], + emitDefault: true, + }) + assert.match(output, /join\(['"]\\n['"]\)/) + assert.match(output, /export const knightedCss = /) + assert.match(output, /export default __knightedDefault/) +}) + +test('createCombinedJsBridgeModule omits default when disabled', () => { + const output = __loaderBridgeInternals.createCombinedJsBridgeModule({ + upstreamRequest: '!!./card.js?knighted-css&combined', + cssRequests: ['./card.module.css?knighted-css'], + emitDefault: false, + }) + assert.ok(!/export default __knightedDefault/.test(output)) +}) + +test('pitch handles combined js modules and collects css modules', async () => { + const source = `import styles from './card.module.css'\nimport './other.module.scss?inline'` + const ctx = createMockContext({ + resourcePath: path.resolve(__dirname, 'fixtures/bridge/bridge-card.tsx'), + resourceQuery: '?knighted-css&combined', + }) as LoaderContext & { + fs: LoaderContext['fs'] + async: () => (error: Error | null, result?: string) => void + } + + const result = await new Promise((resolve, reject) => { + const readFile = (_path: string, cb: (err: Error | null, data?: Buffer) => void) => + cb(null, Buffer.from(source)) + ctx.fs = { + readFile, + } as unknown as LoaderContext['fs'] + ctx.async = () => (error, output) => { + if (error) { + reject(error) + return + } + resolve(String(output ?? '')) + } + pitch.call(ctx, './bridge-card.tsx?knighted-css&combined', '', {}) + }) + + assert.match(result, /export \* from/) + assert.match(result, /card\.module\.css\?knighted-css/) + assert.match(result, /other\.module\.scss\?inline&knighted-css/) +}) + +test('pitch combined js returns sync module when async callback is missing', async () => { + const ctx = createMockContext({ + resourcePath: path.resolve(__dirname, 'fixtures/bridge/bridge-card.tsx'), + resourceQuery: '?knighted-css&combined', + }) as unknown as LoaderContext + ;(ctx as unknown as { async?: () => undefined }).async = () => undefined + const result = await pitch.call( + ctx as LoaderContext, + './bridge-card.tsx?knighted-css&combined', + '', + {}, + ) + const output = String(result ?? '') + assert.match(output, /export const knightedCss = /) + assert.match(output, /export \* from/) +}) + +test('pitch combined js surfaces read errors', async () => { + const ctx = createMockContext({ + resourcePath: path.resolve(__dirname, 'fixtures/bridge/bridge-card.tsx'), + resourceQuery: '?knighted-css&combined', + }) as LoaderContext & { + fs: LoaderContext['fs'] + async: () => (error: Error | null, result?: string) => void + } + + const error = await new Promise((resolve, reject) => { + const readFile = (_path: string, cb: (err: Error | null, data?: Buffer) => void) => + cb(new Error('read failed')) + ctx.fs = { + readFile, + } as unknown as LoaderContext['fs'] + ctx.async = () => (err, _output) => { + if (!err) { + reject(new Error('Expected error')) + return + } + resolve(err) + } + pitch.call(ctx, './bridge-card.tsx?knighted-css&combined', '', {}) + }) + + assert.match(error.message, /read failed/) +}) + +test('pitch combined js errors when no data is returned', async () => { + const ctx = createMockContext({ + resourcePath: path.resolve(__dirname, 'fixtures/bridge/bridge-card.tsx'), + resourceQuery: '?knighted-css&combined', + }) as LoaderContext & { + fs: LoaderContext['fs'] + async: () => (error: Error | null, result?: string) => void + } + + const error = await new Promise((resolve, reject) => { + const readFile = (_path: string, cb: (err: Error | null, data?: Buffer) => void) => + cb(null) + ctx.fs = { + readFile, + } as unknown as LoaderContext['fs'] + ctx.async = () => (err, _output) => { + if (!err) { + reject(new Error('Expected error')) + return + } + resolve(err) + } + pitch.call(ctx, './bridge-card.tsx?knighted-css&combined', '', {}) + }) + + assert.match(error.message, /Unable to read/) +}) + +test('resolveCssModules returns string maps', () => { + const module = { card: 'card_hash', title: 'title_hash' } + const bridgeModule = module as unknown as { + default?: unknown + locals?: Record + } + assert.deepEqual( + __loaderBridgeInternals.resolveCssModules(module, bridgeModule), + module, + ) +}) + +test('resolveCssModules falls back to named exports', () => { + const module = { card: 'card_hash', __esModule: true } + const bridgeModule = module as unknown as { + default?: unknown + locals?: Record + } + assert.deepEqual(__loaderBridgeInternals.resolveCssModules(module, bridgeModule), { + card: 'card_hash', + }) +}) + +test('buildProxyRequest prefers raw requests', () => { + const ctx = createMockContext({ + resourcePath: path.resolve(__dirname, 'fixtures/dialects/basic/styles.css'), + resourceQuery: '?knighted-css', + _module: { + rawRequest: '!!sass-loader!./styles.css?knighted-css', + } as unknown as LoaderContext['_module'], + }) as LoaderContext + + const request = __loaderBridgeInternals.buildProxyRequest(ctx) + assert.match(request, /sass-loader!\.\/styles\.css/) +}) + +test('buildProxyRequest uses contextify when available', () => { + const ctx = createMockContext({ + resourcePath: path.resolve(__dirname, 'fixtures/dialects/basic/styles.css'), + resourceQuery: '?knighted-css', + context: path.resolve(__dirname, 'fixtures/dialects/basic'), + utils: { + contextify: (_context: string, request: string) => `./${path.basename(request)}`, + } as unknown as LoaderContext['utils'], + }) as LoaderContext + + const request = __loaderBridgeInternals.buildProxyRequest(ctx) + assert.equal(request, './styles.css') +}) diff --git a/packages/playwright/.knighted-css-debug/selector-modules.json b/packages/playwright/.knighted-css-debug/selector-modules.json deleted file mode 100644 index 7570624..0000000 --- a/packages/playwright/.knighted-css-debug/selector-modules.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "/Users/morgan/knighted/css/packages/playwright/src/hash-imports-workspace/apps/hash-import-demo/src/workspace-bridge/hash-imports.scss": { - "file": "/Users/morgan/knighted/css/packages/playwright/src/hash-imports-workspace/apps/hash-import-demo/src/workspace-bridge/hash-imports.scss.knighted-css.ts", - "hash": "2cb05b82a4776b703d53194253d1659decddd9c7" - } -} diff --git a/packages/playwright/bridge-webpack.html b/packages/playwright/bridge-webpack.html new file mode 100644 index 0000000..e8d3d85 --- /dev/null +++ b/packages/playwright/bridge-webpack.html @@ -0,0 +1,12 @@ + + + + + Bridge Loader Fixture (Webpack) + + + +
+ + + diff --git a/packages/playwright/bridge.html b/packages/playwright/bridge.html new file mode 100644 index 0000000..896e65c --- /dev/null +++ b/packages/playwright/bridge.html @@ -0,0 +1,12 @@ + + + + + Bridge Loader Fixture (Rspack) + + + +
+ + + diff --git a/packages/playwright/package.json b/packages/playwright/package.json index 161993a..18dc014 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "build": "npm run build:rspack && npm run build:auto-stable && npm run build:webpack && npm run build:ssr", + "build": "npm run build:rspack && npm run build:auto-stable && npm run build:webpack && npm run build:ssr && npm run build:bridge:rspack && npm run build:bridge:webpack", "types": "npm run types:base && npm run types:auto-stable", "types:base": "knighted-css-generate-types --root . --include src --out-dir .knighted-css", "types:auto-stable": "knighted-css-generate-types --root . --include src/auto-stable --auto-stable --out-dir .knighted-css-auto", @@ -12,16 +12,22 @@ "build:auto-stable": "rspack --config rspack.auto-stable.config.js", "build:webpack": "webpack --config webpack.config.js", "build:ssr": "tsx scripts/render-ssr-preview.ts", + "build:bridge:rspack": "rspack --config rspack.bridge.config.js", + "build:bridge:webpack": "webpack --config webpack.bridge.config.js", "precheck-types": "npm run types", "check-types": "tsc --noEmit", "preview": "npm run build && http-server . -p 4174", "preview:auto-stable": "npm run build:auto-stable && http-server . -p 4175", + "preview:bridge": "npm run build:bridge:rspack && http-server . -p 4176 -o /bridge.html", + "preview:bridge-webpack": "npm run build:bridge:webpack && http-server . -p 4177 -o /bridge-webpack.html", + "prepreview:bridge": "npm run build -w @knighted/css", + "prepreview:bridge-webpack": "npm run build -w @knighted/css", "serve": "http-server dist -p 4174", "test": "playwright test", "pretest": "npm run types && npm run build" }, "dependencies": { - "@knighted/css": "1.1.0-rc.5", + "@knighted/css": "1.1.0-rc.6", "@knighted/jsx": "^1.7.3", "lit": "^3.2.1", "react": "^19.0.0", @@ -31,7 +37,9 @@ "@types/react": "^19.0.4", "@types/react-dom": "^19.0.2", "@vanilla-extract/webpack-plugin": "^2.3.15", - "ts-loader": "^9.5.1", + "css-loader": "^7.1.2", + "mini-css-extract-plugin": "^2.10.0", + "swc-loader": "^0.2.7", "webpack": "^5.97.1", "webpack-cli": "^5.1.4" } diff --git a/packages/playwright/playwright.config.ts b/packages/playwright/playwright.config.ts index b60df8a..303b07e 100644 --- a/packages/playwright/playwright.config.ts +++ b/packages/playwright/playwright.config.ts @@ -17,7 +17,7 @@ if (isCI) { export default defineConfig({ testDir: 'test', - timeout: 20_000, + timeout: 5_000, retries: isCI ? 1 : 0, expect: { timeout: 10_000, diff --git a/packages/playwright/rspack.bridge.config.js b/packages/playwright/rspack.bridge.config.js new file mode 100644 index 0000000..b45e1d4 --- /dev/null +++ b/packages/playwright/rspack.bridge.config.js @@ -0,0 +1,124 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { CssExtractRspackPlugin, ProvidePlugin } from '@rspack/core' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +export default { + mode: 'development', + context: __dirname, + entry: './src/bridge/bridge-entry.ts', + output: { + path: path.resolve(__dirname, 'dist-bridge'), + filename: 'bridge-bundle.js', + }, + resolve: { + extensions: ['.tsx', '.ts', '.js', '.css'], + extensionAlias: { + '.js': ['.js', '.ts', '.tsx'], + }, + }, + module: { + rules: [ + { + test: /\.module\.css$/, + oneOf: [ + { + resourceQuery: /knighted-css/, + type: 'javascript/auto', + use: [ + { + loader: '@knighted/css/loader-bridge', + }, + { + loader: 'css-loader', + options: { + exportType: 'string', + modules: { + namedExport: true, + }, + }, + }, + ], + }, + { + use: [ + { + loader: CssExtractRspackPlugin.loader, + }, + { + loader: 'css-loader', + options: { + modules: { + namedExport: false, + }, + }, + }, + ], + }, + ], + }, + { + test: /\.[jt]sx?$/, + resourceQuery: /knighted-css/, + exclude: /\.css\.ts$/, + use: [ + { + loader: '@knighted/css/loader-bridge', + }, + { + loader: '@knighted/jsx/loader', + options: { + mode: 'react', + }, + }, + { + loader: 'builtin:swc-loader', + options: { + jsc: { + target: 'es2022', + parser: { + syntax: 'typescript', + tsx: true, + }, + }, + }, + }, + ], + }, + { + test: /\.tsx?$/, + exclude: /node_modules/, + use: [ + { + loader: '@knighted/jsx/loader', + options: { + mode: 'react', + }, + }, + { + loader: 'builtin:swc-loader', + options: { + jsc: { + target: 'es2022', + parser: { + syntax: 'typescript', + tsx: true, + }, + }, + }, + }, + ], + }, + ], + }, + plugins: [ + new CssExtractRspackPlugin({ + filename: 'bridge-bundle.css', + }), + new ProvidePlugin({ + React: 'react', + }), + ], +} diff --git a/packages/playwright/src/bridge/bridge-card.tsx b/packages/playwright/src/bridge/bridge-card.tsx new file mode 100644 index 0000000..cc61643 --- /dev/null +++ b/packages/playwright/src/bridge/bridge-card.tsx @@ -0,0 +1,18 @@ +import styles from './styles.module.css' +import { BRIDGE_CARD_TEST_ID } from './constants.js' + +type BridgeCardProps = { + location: 'light' | 'shadow' +} + +export function BridgeCard({ location }: BridgeCardProps) { + const locationLabel = location === 'shadow' ? 'Shadow DOM' : 'Light DOM' + return ( +
+

Loader bridge

+

+ {locationLabel} styling using existing CSS module class names. +

+
+ ) +} diff --git a/packages/playwright/src/bridge/bridge-entry.ts b/packages/playwright/src/bridge/bridge-entry.ts new file mode 100644 index 0000000..e3d39f2 --- /dev/null +++ b/packages/playwright/src/bridge/bridge-entry.ts @@ -0,0 +1,6 @@ +import { renderBridgeDemo } from './index.js' + +if (typeof document !== 'undefined') { + const root = document.getElementById('bridge-app') ?? document.body + renderBridgeDemo(root) +} diff --git a/packages/playwright/src/bridge/constants.ts b/packages/playwright/src/bridge/constants.ts new file mode 100644 index 0000000..71f9589 --- /dev/null +++ b/packages/playwright/src/bridge/constants.ts @@ -0,0 +1,4 @@ +export const BRIDGE_HOST_TAG = 'knighted-bridge-host' +export const BRIDGE_HOST_TEST_ID = 'bridge-host' +export const BRIDGE_CARD_TEST_ID = 'bridge-card' +export const BRIDGE_MARKER_TEST_ID = 'bridge-marker' diff --git a/packages/playwright/src/bridge/index.ts b/packages/playwright/src/bridge/index.ts new file mode 100644 index 0000000..740d2f5 --- /dev/null +++ b/packages/playwright/src/bridge/index.ts @@ -0,0 +1,21 @@ +import { reactJsx } from '@knighted/jsx/react' +import { createRoot } from 'react-dom/client' + +import { BridgeCard } from './bridge-card.js' +import { BRIDGE_HOST_TAG, BRIDGE_HOST_TEST_ID } from './constants.js' +import { BridgeHost, ensureBridgeHostDefined } from './lit-host.js' + +export function renderBridgeDemo(root: HTMLElement): void { + ensureBridgeHostDefined() + const mountPoint = root ?? document.body + + const lightMount = document.createElement('section') + lightMount.setAttribute('data-section', 'bridge-light') + mountPoint.appendChild(lightMount) + + createRoot(lightMount).render(reactJsx`<${BridgeCard} location="light" />`) + + const host = document.createElement(BRIDGE_HOST_TAG) as BridgeHost + host.dataset.testid = BRIDGE_HOST_TEST_ID + mountPoint.appendChild(host) +} diff --git a/packages/playwright/src/bridge/lit-host.ts b/packages/playwright/src/bridge/lit-host.ts new file mode 100644 index 0000000..2519491 --- /dev/null +++ b/packages/playwright/src/bridge/lit-host.ts @@ -0,0 +1,63 @@ +import { reactJsx } from '@knighted/jsx/react' +import { createRoot, type Root } from 'react-dom/client' +import { LitElement, css, html, unsafeCSS } from 'lit' +import { asKnightedCssCombinedModule } from '@knighted/css/loader-helpers' + +import * as bridgeModule from './bridge-card.js?knighted-css&combined' +import { BRIDGE_HOST_TAG, BRIDGE_MARKER_TEST_ID } from './constants.js' + +const { BridgeCard, knightedCss } = + asKnightedCssCombinedModule(bridgeModule) +const hostShell = css` + :host { + display: block; + padding: 1.5rem; + border-radius: 1.25rem; + background: #0b1120; + box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.2); + } +` + +export class BridgeHost extends LitElement { + static styles = [hostShell, unsafeCSS(knightedCss)] + #reactRoot?: Root + + firstUpdated(): void { + this.#mountReact() + } + + disconnectedCallback(): void { + this.#reactRoot?.unmount() + super.disconnectedCallback() + } + + #mountReact(): void { + if (!this.#reactRoot) { + const outlet = this.renderRoot.querySelector( + '[data-react-root]', + ) as HTMLDivElement | null + if (!outlet) return + this.#reactRoot = createRoot(outlet) + } + this.#renderReactTree() + } + + #renderReactTree(): void { + if (!this.#reactRoot) return + this.#reactRoot.render(reactJsx`<${BridgeCard} location="shadow" />`) + } + + render() { + return html`
+ ` + } +} + +export function ensureBridgeHostDefined(): void { + if (!customElements.get(BRIDGE_HOST_TAG)) { + customElements.define(BRIDGE_HOST_TAG, BridgeHost) + } +} diff --git a/packages/playwright/src/bridge/styles.module.css b/packages/playwright/src/bridge/styles.module.css new file mode 100644 index 0000000..85aae14 --- /dev/null +++ b/packages/playwright/src/bridge/styles.module.css @@ -0,0 +1,18 @@ +.card { + border-radius: 24px; + background: rgb(20, 30, 55); + color: rgb(226, 232, 240); + padding: 1.25rem; + box-shadow: 0 12px 28px rgba(15, 23, 42, 0.35); +} + +.title { + margin: 0 0 0.35rem; + font-size: 1.1rem; + letter-spacing: 0.01em; +} + +.copy { + margin: 0; + opacity: 0.85; +} diff --git a/packages/playwright/test/bridge-loader.spec.ts b/packages/playwright/test/bridge-loader.spec.ts new file mode 100644 index 0000000..2d99b4c --- /dev/null +++ b/packages/playwright/test/bridge-loader.spec.ts @@ -0,0 +1,67 @@ +import { expect, test } from '@playwright/test' + +import { BRIDGE_CARD_TEST_ID, BRIDGE_HOST_TEST_ID } from '../src/bridge/constants.js' + +const pages = [ + { label: 'rspack', url: '/bridge.html' }, + { label: 'webpack', url: '/bridge-webpack.html' }, +] + +for (const target of pages) { + test.describe(`Loader bridge demo (${target.label})`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(target.url) + }) + + test('shadow DOM uses hashed class names from CSS modules', async ({ page }) => { + const host = page.getByTestId(BRIDGE_HOST_TEST_ID) + await expect(host).toBeVisible() + + const cardHandle = await page.waitForFunction( + ({ hostId, cardId }) => { + const hostEl = document.querySelector(`[data-testid="${hostId}"]`) + return hostEl?.shadowRoot?.querySelector(`[data-testid="${cardId}"]`) + }, + { hostId: BRIDGE_HOST_TEST_ID, cardId: BRIDGE_CARD_TEST_ID }, + ) + + const card = cardHandle.asElement() + if (!card) throw new Error('Bridge card was not rendered') + + const metrics = await card.evaluate(node => { + const el = node as HTMLElement + const style = getComputedStyle(el) + return { + className: el.className, + background: style.getPropertyValue('background-color').trim(), + color: style.getPropertyValue('color').trim(), + } + }) + + await cardHandle.dispose() + + expect(metrics.className).not.toBe('') + expect(metrics.background).toBe('rgb(20, 30, 55)') + expect(metrics.color).toBe('rgb(226, 232, 240)') + + const markerHandle = await page.waitForFunction( + ({ hostId, markerId }) => { + const hostEl = document.querySelector(`[data-testid="${hostId}"]`) + return hostEl?.shadowRoot?.querySelector(`[data-testid="${markerId}"]`) + }, + { hostId: BRIDGE_HOST_TEST_ID, markerId: 'bridge-marker' }, + ) + + const marker = markerHandle.asElement() + if (!marker) throw new Error('Bridge marker was not rendered') + + const cssLength = await marker.evaluate(node => + Number((node as HTMLElement).dataset.cssLength ?? 0), + ) + + await markerHandle.dispose() + + expect(cssLength).toBeGreaterThan(0) + }) + }) +} diff --git a/packages/playwright/webpack.bridge.config.js b/packages/playwright/webpack.bridge.config.js new file mode 100644 index 0000000..2e60add --- /dev/null +++ b/packages/playwright/webpack.bridge.config.js @@ -0,0 +1,126 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import webpack from 'webpack' +import MiniCssExtractPlugin from 'mini-css-extract-plugin' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +export default { + mode: 'development', + context: __dirname, + entry: './src/bridge/bridge-entry.ts', + output: { + filename: 'bridge-webpack-bundle.js', + path: path.resolve(__dirname, 'dist-bridge-webpack'), + clean: true, + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + extensionAlias: { + '.js': ['.js', '.ts', '.tsx'], + }, + }, + module: { + rules: [ + { + test: /\.module\.css$/, + oneOf: [ + { + resourceQuery: /knighted-css/, + type: 'javascript/auto', + use: [ + { + loader: '@knighted/css/loader-bridge', + }, + { + loader: 'css-loader', + options: { + exportType: 'string', + modules: { + namedExport: true, + }, + }, + }, + ], + }, + { + use: [ + MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + options: { + modules: { + namedExport: false, + }, + }, + }, + ], + }, + ], + }, + { + test: /\.[jt]sx?$/, + resourceQuery: /knighted-css/, + exclude: /\.css\.ts$/, + use: [ + { + loader: '@knighted/css/loader-bridge', + }, + { + loader: '@knighted/jsx/loader', + options: { + mode: 'react', + }, + }, + { + loader: 'swc-loader', + options: { + jsc: { + target: 'es2022', + parser: { + syntax: 'typescript', + tsx: true, + }, + }, + }, + }, + ], + }, + { + test: /\.tsx?$/, + exclude: /node_modules/, + use: [ + { + loader: 'swc-loader', + options: { + jsc: { + target: 'es2022', + parser: { + syntax: 'typescript', + tsx: true, + }, + }, + }, + }, + { + loader: '@knighted/jsx/loader', + options: { + mode: 'react', + }, + }, + ], + }, + ], + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: 'bridge-webpack-bundle.css', + }), + new webpack.ProvidePlugin({ + React: 'react', + }), + ], + devtool: 'source-map', +} diff --git a/packages/playwright/webpack.config.js b/packages/playwright/webpack.config.js index c44c8ea..3f52645 100644 --- a/packages/playwright/webpack.config.js +++ b/packages/playwright/webpack.config.js @@ -6,7 +6,6 @@ import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) -const tsconfig = path.resolve(__dirname, 'tsconfig.json') export default { mode: 'development', @@ -31,13 +30,13 @@ export default { use: [ VanillaExtractPlugin.loader, { - loader: 'ts-loader', + loader: 'swc-loader', options: { - configFile: tsconfig, - transpileOnly: true, - compilerOptions: { - module: 'esnext', - moduleResolution: 'bundler', + jsc: { + target: 'es2022', + parser: { + syntax: 'typescript', + }, }, }, }, @@ -56,13 +55,14 @@ export default { }, }, { - loader: 'ts-loader', + loader: 'swc-loader', options: { - configFile: tsconfig, - transpileOnly: true, - compilerOptions: { - module: 'esnext', - moduleResolution: 'bundler', + jsc: { + target: 'es2022', + parser: { + syntax: 'typescript', + tsx: true, + }, }, }, }, @@ -73,13 +73,14 @@ export default { exclude: [/node_modules/, /\.css\.ts$/], use: [ { - loader: 'ts-loader', + loader: 'swc-loader', options: { - configFile: tsconfig, - transpileOnly: true, - compilerOptions: { - module: 'esnext', - moduleResolution: 'bundler', + jsc: { + target: 'es2022', + parser: { + syntax: 'typescript', + tsx: true, + }, }, }, },