diff --git a/.changepacks/changepack_log_w74-native-engine.json b/.changepacks/changepack_log_w74-native-engine.json
new file mode 100644
index 000000000..21802622e
--- /dev/null
+++ b/.changepacks/changepack_log_w74-native-engine.json
@@ -0,0 +1,12 @@
+{
+ "changes": {
+ "packages/plugin-utils/package.json": "Patch",
+ "packages/webpack-plugin/package.json": "Patch",
+ "packages/rsbuild-plugin/package.json": "Patch",
+ "packages/vite-plugin/package.json": "Patch",
+ "packages/bun-plugin/package.json": "Patch",
+ "packages/next-plugin/package.json": "Patch"
+ },
+ "note": "Add shared native/WASM compiler selection, structural compiler types and exact native platform-load diagnostics for build plugins.",
+ "date": "2026-10-30T17:29:00.000Z"
+}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index bb13c06ff..abbacd756 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- main
+ - feat/napi-binding
# Pull requests and every job below run read-only. Only the `release` and
# `deploy-pages` jobs get write permissions, and they run on pushes to main
diff --git a/apps/vinext-rsc-css-repro/vite.config.ts b/apps/vinext-rsc-css-repro/vite.config.ts
index 9f163aa1a..d6059775e 100644
--- a/apps/vinext-rsc-css-repro/vite.config.ts
+++ b/apps/vinext-rsc-css-repro/vite.config.ts
@@ -2,9 +2,19 @@ import { DevupUI } from '@devup-ui/vite-plugin'
import vinext from 'vinext'
import { defineConfig } from 'vite'
+const engine = process.env.DEVUP_ENGINE
+switch (engine) {
+ case undefined:
+ case 'native':
+ case 'wasm':
+ break
+ default:
+ throw new TypeError(`Invalid DEVUP_ENGINE: ${JSON.stringify(engine)}`)
+}
+
export default defineConfig({
plugins: [
- DevupUI({ singleCss: process.env.DEVUP_SINGLE_CSS === '1' }),
+ DevupUI({ engine, singleCss: process.env.DEVUP_SINGLE_CSS === '1' }),
vinext(),
],
})
diff --git a/bun.lock b/bun.lock
index cdf8adeff..ccfda13b1 100644
--- a/bun.lock
+++ b/bun.lock
@@ -570,9 +570,6 @@
"@types/bun": "^1.4.4",
"typescript": "^7.0.2",
},
- "peerDependencies": {
- "@devup-ui/wasm": "*",
- },
},
"packages/components": {
"name": "@devup-ui/components",
@@ -631,7 +628,6 @@
"typescript": "^7.0.2",
},
"peerDependencies": {
- "@devup-ui/wasm": "*",
"@devup-ui/webpack-plugin": "*",
"next": "*",
},
@@ -639,6 +635,10 @@
"packages/plugin-utils": {
"name": "@devup-ui/plugin-utils",
"version": "1.0.17",
+ "dependencies": {
+ "@devup-ui/binding": "workspace:^",
+ "@devup-ui/wasm": "workspace:^",
+ },
"devDependencies": {
"typescript": "^7.0.2",
},
@@ -689,7 +689,6 @@
"typescript": "^7.0.2",
},
"peerDependencies": {
- "@devup-ui/wasm": "*",
"@rsbuild/core": "*",
},
},
@@ -706,7 +705,6 @@
"vite": "^8.3.5",
},
"peerDependencies": {
- "@devup-ui/wasm": "*",
"vite": "*",
},
},
@@ -721,9 +719,6 @@
"@types/webpack": "^5.28.5",
"typescript": "^7.0.2",
},
- "peerDependencies": {
- "@devup-ui/wasm": "*",
- },
},
},
"packages": {
@@ -989,6 +984,12 @@
"@dual-bundle/import-meta-resolve": ["@dual-bundle/import-meta-resolve@4.2.1", "", {}, "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg=="],
+ "@eloqnt/config": ["@eloqnt/config@0.1.1", "", {}, "sha512-YRDigSaiJEzKDfzTth5kG+L/PKfB3mo8h7yNzpXExPfvQmcD05WoZT57PdhJE0jI+Vva6eb0OCU8hnHHgdME3g=="],
+
+ "@eloqnt/format-json": ["@eloqnt/format-json@0.1.0", "", { "dependencies": { "@eloqnt/config": "^0.1.0" } }, "sha512-4Pkb+HyzgWNJXoU48aJhUyNZSjZvMGooPCNG1ZFmBm55xTiNNPQCNXH66Ob6sXS6RvbWZrRpCf+Uo/Jz9K9g+A=="],
+
+ "@eloqnt/format-po": ["@eloqnt/format-po@0.1.1", "", { "dependencies": { "@eloqnt/config": "^0.1.1", "po-parser": "^2.2.0" } }, "sha512-aLtmIoqXGEaeTT0RcsLN8sZ3aIrsT/c/dVs28zrLhDjO3+vA3KNP+r98N0xq8w/k36+/GIUt4uZNbqcA71GVWw=="],
+
"@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
@@ -1109,6 +1110,14 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
+ "@formatjs/fast-memoize": ["@formatjs/fast-memoize@3.1.7", "", {}, "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA=="],
+
+ "@formatjs/icu-messageformat-parser": ["@formatjs/icu-messageformat-parser@3.5.22", "", { "dependencies": { "@formatjs/icu-skeleton-parser": "2.1.12" } }, "sha512-YuwN+dDKgssfuYwtOyGQD0CSn7K46g9cv1Fp0VgKpjZDDFvv8b8crzP990IcJhRPWHhKzCGsbUSJVIguyyFA5A=="],
+
+ "@formatjs/icu-skeleton-parser": ["@formatjs/icu-skeleton-parser@2.1.12", "", {}, "sha512-ca7tZMLpfHX3sUD7hP6gE/qr6Iq89EDd3ZLeRhbsB7r0UD55DGJq+tmFLzrGrnx5StahUevQmQQVtbDRHsfDHA=="],
+
+ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.8.14", "", { "dependencies": { "@formatjs/fast-memoize": "3.1.7" } }, "sha512-8O7V38QmbB11+ryJ3KM/znrzXtiqgpXzqNoqDsndHZZ5/zDKTTL+cf5F0mzXkQEwhx8IlWnqYvEovf2S5EwLTw=="],
+
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@21.0.4", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^21.0.4" } }, "sha512-Bbr0hW9hhDhBf3Jp0EyDlkCbFgbr0z2jd3BMKYOYW+fOkPM4Nx195GMXyCtRCGEgoNn6i09FabXrSkNFm2ONzg=="],
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
@@ -1557,6 +1566,8 @@
"@rspack/plugin-react-refresh": ["@rspack/plugin-react-refresh@2.0.2", "", { "peerDependencies": { "@rspack/core": "^2.0.0", "react-refresh": ">=0.10.0 <1.0.0" }, "optionalPeers": ["@rspack/core"] }, "sha512-dGNZiCxQxgAUI9sah7gd8u+O7OJZRCmqtEJNDOd8xW5RqcieC86F7p5qcShyw6onH5pKf57evpr2VjGbaFGkZg=="],
+ "@schummar/icu-type-parser": ["@schummar/icu-type-parser@1.21.5", "", {}, "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw=="],
+
"@shuding/opentype.js": ["@shuding/opentype.js@1.4.0-beta.0", "", { "dependencies": { "fflate": "^0.7.3", "string.prototype.codepointat": "^0.2.1" }, "bin": { "ot": "bin/ot" } }, "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA=="],
"@storybook/addon-docs": ["@storybook/addon-docs@10.6.3", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.6.3", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0", "unplugin": "^2.3.5" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.3" }, "optionalPeers": ["@types/react"] }, "sha512-365f712au4w/xvFqEKY6TIvTeGGAY7FKRB8zP4CFa3VSv9oyxHaq/ublFg4YDbCeRagppwl1PZ6k4+PoUor3+A=="],
@@ -2175,6 +2186,8 @@
"confbox": ["confbox@0.3.1", "", {}, "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA=="],
+ "content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
+
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"core-js-compat": ["core-js-compat@3.51.0", "", { "dependencies": { "browserslist": "^4.29.3" } }, "sha512-52r6fkXgxv4GLMjmFcMWRx7X9bw071xTTdrDcdyPGRqU2oE+9pCG0bAOMifSBdLw0JRBZKEF0q3B55qPlLuE8g=="],
@@ -2461,6 +2474,8 @@
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
+ "icu-minify": ["icu-minify@4.14.9", "", { "dependencies": { "@formatjs/icu-messageformat-parser": "^3.4.0" } }, "sha512-PmO+fNnC3K3mfPKTnm3RrPERrhQZnjm1AlpyZQ0ShbXi8Y/kYZ3YOde8TEjCLz9k3P2DmzkBn8lykGrCAi1Ajw=="],
+
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@@ -2479,6 +2494,8 @@
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
+ "intl-messageformat": ["intl-messageformat@11.2.15", "", { "dependencies": { "@formatjs/fast-memoize": "3.1.7", "@formatjs/icu-messageformat-parser": "3.5.18" } }, "sha512-W4VNytsXyWgW5lvVchmsb9xgA5rSKqoflu3/IQ9UCtB0z4R+Bx9MoEkwFT/+30RCl/KzUoH0QGGe9lppF5SuvQ=="],
+
"invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="],
"ipaddr.js": ["ipaddr.js@2.5.0", "", {}, "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w=="],
@@ -2717,6 +2734,8 @@
"natural-orderby": ["natural-orderby@5.0.0", "", {}, "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg=="],
+ "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="],
+
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"next": ["next@16.4.3", "", { "dependencies": { "@next/env": "16.4.3", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.4.3", "@next/swc-darwin-x64": "16.4.3", "@next/swc-linux-arm64-gnu": "16.4.3", "@next/swc-linux-arm64-musl": "16.4.3", "@next/swc-linux-x64-gnu": "16.4.3", "@next/swc-linux-x64-musl": "16.4.3", "@next/swc-win32-arm64-msvc": "16.4.3", "@next/swc-win32-x64-msvc": "16.4.3", "sharp": "^0.35.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-OqxKTGi9u80FydydsMNYNjnuje9Au+kvHlt2GZ//ftdpDHm1dxKqAaENoOOTX5wKzn+9vaKUNIfZeyZUuLhCnA=="],
@@ -2735,6 +2754,10 @@
"next-example": ["next-example@workspace:apps/next"],
+ "next-intl": ["next-intl@4.14.9", "", { "dependencies": { "@eloqnt/config": "^0.1.1", "@eloqnt/format-json": "^0.1.0", "@eloqnt/format-po": "^0.1.1", "@formatjs/intl-localematcher": "^0.8.1", "@parcel/watcher": "^2.4.1", "@swc/core": "~1.16.0", "icu-minify": "^4.14.9", "negotiator": "^1.0.0", "next-intl-swc-plugin-extractor": "4.14.9", "use-intl": "^4.14.9" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-uauRtUC+TrYNA1OL/QcwL9irndLrI9jK3Wdaa9W0B5AeKh9cqVdNeT0IWgLnhBqBRrVwppVdI0M+C1+JdGPQ3w=="],
+
+ "next-intl-swc-plugin-extractor": ["next-intl-swc-plugin-extractor@4.14.9", "", {}, "sha512-8jAkmDwQJo6RDavBHm9QRl1YTn2WXf+rI4SVhMaZxZcxopnwZic6fKTXOoNIo3kzC+a6vJySkOkXmHTVFLAYtg=="],
+
"next-kuma-ui-benchmark": ["next-kuma-ui-benchmark@workspace:benchmark/next-kuma-ui"],
"next-mui-benchmark": ["next-mui-benchmark@workspace:benchmark/next-mui"],
@@ -2841,6 +2864,8 @@
"playwright-core": ["playwright-core@1.64.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ=="],
+ "po-parser": ["po-parser@2.2.0", "", {}, "sha512-NdTrKgh0oO7+y+RFX8KKhOB438x/j94UGXEFzl/7pHH0JjWSn42iJ9tgmPksIO6n1JKDHmNDcejPJfKspljB9g=="],
+
"postcss": ["postcss@8.5.31", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
@@ -3147,6 +3172,8 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+ "use-intl": ["use-intl@4.14.9", "", { "dependencies": { "@formatjs/fast-memoize": "^3.1.0", "@schummar/icu-type-parser": "1.21.5", "icu-minify": "^4.14.9", "intl-messageformat": "^11.1.0" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-37ktvdCCz+6JY2VFPl/GV69Ds4ahiUT2c2xEZZRZYxdgGLxFsUbdxzjQVRm3YMIOUU0VPs6zvlifb4Yjk4IxIg=="],
+
"use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
@@ -3775,6 +3802,8 @@
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+ "intl-messageformat/@formatjs/icu-messageformat-parser": ["@formatjs/icu-messageformat-parser@3.5.18", "", { "dependencies": { "@formatjs/icu-skeleton-parser": "2.1.11" } }, "sha512-wX1efcL8d7K5QapOTRsP/OLTA/kpwe4LwqWyNxDNg5iRs8i9jJbCyxB5P8jc4KOW5oiLzz2DJr+BRY5xMV9+bA=="],
+
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"jsonc-eslint-parser/verkit": ["verkit@0.3.2", "", {}, "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg=="],
@@ -5801,6 +5830,8 @@
"eslint-plugin-react-hooks/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+ "intl-messageformat/@formatjs/icu-messageformat-parser/@formatjs/icu-skeleton-parser": ["@formatjs/icu-skeleton-parser@2.1.11", "", {}, "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA=="],
+
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"parse-json/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -6832,49 +6863,5 @@
"yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"pkg-dir/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
-
- "@eloqnt/config": ["@eloqnt/config@0.1.1", "", {}, "sha512-YRDigSaiJEzKDfzTth5kG+L/PKfB3mo8h7yNzpXExPfvQmcD05WoZT57PdhJE0jI+Vva6eb0OCU8hnHHgdME3g=="],
-
- "@eloqnt/format-json": ["@eloqnt/format-json@0.1.0", "", { "dependencies": { "@eloqnt/config": "^0.1.0" } }, "sha512-4Pkb+HyzgWNJXoU48aJhUyNZSjZvMGooPCNG1ZFmBm55xTiNNPQCNXH66Ob6sXS6RvbWZrRpCf+Uo/Jz9K9g+A=="],
-
- "@eloqnt/format-po": ["@eloqnt/format-po@0.1.1", "", { "dependencies": { "@eloqnt/config": "^0.1.1", "po-parser": "^2.2.0" } }, "sha512-aLtmIoqXGEaeTT0RcsLN8sZ3aIrsT/c/dVs28zrLhDjO3+vA3KNP+r98N0xq8w/k36+/GIUt4uZNbqcA71GVWw=="],
-
- "@formatjs/fast-memoize": ["@formatjs/fast-memoize@3.1.7", "", {}, "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA=="],
-
- "@formatjs/icu-messageformat-parser": ["@formatjs/icu-messageformat-parser@3.5.22", "", { "dependencies": { "@formatjs/icu-skeleton-parser": "2.1.12" } }, "sha512-YuwN+dDKgssfuYwtOyGQD0CSn7K46g9cv1Fp0VgKpjZDDFvv8b8crzP990IcJhRPWHhKzCGsbUSJVIguyyFA5A=="],
-
- "@formatjs/icu-skeleton-parser": ["@formatjs/icu-skeleton-parser@2.1.12", "", {}, "sha512-ca7tZMLpfHX3sUD7hP6gE/qr6Iq89EDd3ZLeRhbsB7r0UD55DGJq+tmFLzrGrnx5StahUevQmQQVtbDRHsfDHA=="],
-
- "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.8.14", "", { "dependencies": { "@formatjs/fast-memoize": "3.1.7" } }, "sha512-8O7V38QmbB11+ryJ3KM/znrzXtiqgpXzqNoqDsndHZZ5/zDKTTL+cf5F0mzXkQEwhx8IlWnqYvEovf2S5EwLTw=="],
-
- "@schummar/icu-type-parser": ["@schummar/icu-type-parser@1.21.5", "", {}, "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw=="],
-
- "@typescript-eslint/rule-tester": ["@typescript-eslint/rule-tester@8.72.0", "", { "dependencies": { "@typescript-eslint/parser": "8.72.0", "@typescript-eslint/typescript-estree": "8.72.0", "@typescript-eslint/utils": "8.72.0", "ajv": "^6.12.6", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", "semver": "^7.7.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3DkhIwJJ1EgssS9r613P8mqraY/c0uSg9e5rQdYPKf71poFqIrN/6agYEtTzI9SvVLgFqfbCwpKLr0zRBij0UQ=="],
-
- "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
-
- "ajv-keywords/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
-
- "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
-
- "content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
-
- "icu-minify": ["icu-minify@4.14.9", "", { "dependencies": { "@formatjs/icu-messageformat-parser": "^3.4.0" } }, "sha512-PmO+fNnC3K3mfPKTnm3RrPERrhQZnjm1AlpyZQ0ShbXi8Y/kYZ3YOde8TEjCLz9k3P2DmzkBn8lykGrCAi1Ajw=="],
-
- "intl-messageformat": ["intl-messageformat@11.2.15", "", { "dependencies": { "@formatjs/fast-memoize": "3.1.7", "@formatjs/icu-messageformat-parser": "3.5.18" } }, "sha512-W4VNytsXyWgW5lvVchmsb9xgA5rSKqoflu3/IQ9UCtB0z4R+Bx9MoEkwFT/+30RCl/KzUoH0QGGe9lppF5SuvQ=="],
-
- "intl-messageformat/@formatjs/icu-messageformat-parser": ["@formatjs/icu-messageformat-parser@3.5.18", "", { "dependencies": { "@formatjs/icu-skeleton-parser": "2.1.11" } }, "sha512-wX1efcL8d7K5QapOTRsP/OLTA/kpwe4LwqWyNxDNg5iRs8i9jJbCyxB5P8jc4KOW5oiLzz2DJr+BRY5xMV9+bA=="],
-
- "intl-messageformat/@formatjs/icu-messageformat-parser/@formatjs/icu-skeleton-parser": ["@formatjs/icu-skeleton-parser@2.1.11", "", {}, "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA=="],
-
- "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="],
-
- "next-intl": ["next-intl@4.14.9", "", { "dependencies": { "@eloqnt/config": "^0.1.1", "@eloqnt/format-json": "^0.1.0", "@eloqnt/format-po": "^0.1.1", "@formatjs/intl-localematcher": "^0.8.1", "@parcel/watcher": "^2.4.1", "@swc/core": "~1.16.0", "icu-minify": "^4.14.9", "negotiator": "^1.0.0", "next-intl-swc-plugin-extractor": "4.14.9", "use-intl": "^4.14.9" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-uauRtUC+TrYNA1OL/QcwL9irndLrI9jK3Wdaa9W0B5AeKh9cqVdNeT0IWgLnhBqBRrVwppVdI0M+C1+JdGPQ3w=="],
-
- "next-intl-swc-plugin-extractor": ["next-intl-swc-plugin-extractor@4.14.9", "", {}, "sha512-8jAkmDwQJo6RDavBHm9QRl1YTn2WXf+rI4SVhMaZxZcxopnwZic6fKTXOoNIo3kzC+a6vJySkOkXmHTVFLAYtg=="],
-
- "po-parser": ["po-parser@2.2.0", "", {}, "sha512-NdTrKgh0oO7+y+RFX8KKhOB438x/j94UGXEFzl/7pHH0JjWSn42iJ9tgmPksIO6n1JKDHmNDcejPJfKspljB9g=="],
-
- "use-intl": ["use-intl@4.14.9", "", { "dependencies": { "@formatjs/fast-memoize": "^3.1.0", "@schummar/icu-type-parser": "1.21.5", "icu-minify": "^4.14.9", "intl-messageformat": "^11.1.0" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-37ktvdCCz+6JY2VFPl/GV69Ds4ahiUT2c2xEZZRZYxdgGLxFsUbdxzjQVRm3YMIOUU0VPs6zvlifb4Yjk4IxIg=="],
}
}
diff --git a/bun.setup.ts b/bun.setup.ts
index f0939bb02..3a13af6ad 100644
--- a/bun.setup.ts
+++ b/bun.setup.ts
@@ -3,11 +3,12 @@ import './bun.matrix'
import { afterEach, beforeEach, spyOn } from 'bun:test'
import { resetWasmForTesting } from './packages/next-plugin/src/wasm'
-import { createModuleResolver } from './packages/plugin-utils/src'
+import { createModuleResolver, loadCompiler } from './packages/plugin-utils/src'
const { resetStateForTesting, setDebug, setModuleResolver } =
await import('./bindings/devup-ui-wasm/pkg/index.js')
const testModuleResolver = createModuleResolver()
+const nativeCompiler = loadCompiler()
const nativeBunPlugin = Bun.plugin
export const bunRegistration = Object.assign(spyOn(Bun, 'plugin'), {
clearAll: nativeBunPlugin.clearAll,
@@ -21,6 +22,9 @@ try {
}
function resetTestState() {
+ nativeCompiler.resetStateForTesting()
+ nativeCompiler.setDebug(true)
+ nativeCompiler.setModuleResolver(testModuleResolver)
resetStateForTesting()
setDebug(true)
setModuleResolver(testModuleResolver)
diff --git a/e2e/binding/package/build-host.mjs b/e2e/binding/package/build-host.mjs
new file mode 100644
index 000000000..dc024c282
--- /dev/null
+++ b/e2e/binding/package/build-host.mjs
@@ -0,0 +1,99 @@
+import { execFileSync } from 'node:child_process'
+import { resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+import { targets } from './matrix.mjs'
+
+const root = fileURLToPath(new URL('../../../', import.meta.url))
+const actualHost = {
+ platform: process.platform,
+ arch: process.arch,
+ report: process.report.getReport,
+}
+const actualIo = {
+ run: execFileSync,
+ now: () => performance.now(),
+ emit: (record) => console.info(JSON.stringify(record)),
+}
+
+export class HostBuildTargetError extends Error {
+ constructor(host, cause) {
+ super(
+ `Unsupported or indeterminate native host: ${host.platform}/${host.arch}`,
+ { cause },
+ )
+ this.name = 'HostBuildTargetError'
+ this.platform = host.platform
+ this.arch = host.arch
+ }
+}
+
+export function selectHostTarget(host = actualHost) {
+ const candidates = targets.filter(
+ (row) => row.os === host.platform && row.cpu === host.arch,
+ )
+ if (candidates.length === 0) throw new HostBuildTargetError(host)
+ if (host.platform !== 'linux') return candidates[0]
+ try {
+ const report = host.report()
+ if (
+ typeof report !== 'object' ||
+ report === null ||
+ typeof report.header !== 'object' ||
+ report.header === null
+ ) {
+ throw new HostBuildTargetError(host)
+ }
+ const libc = report.header.glibcVersionRuntime ? 'glibc' : 'musl'
+ return candidates.find((row) => row.libc === libc)
+ } catch (cause) {
+ if (cause instanceof HostBuildTargetError) throw cause
+ throw new HostBuildTargetError(host, cause)
+ }
+}
+
+export function buildHost(host = actualHost, io = actualIo) {
+ const row = selectHostTarget(host)
+ if (!row) throw new HostBuildTargetError(host)
+ const context = {
+ event: 'devup.host-native-build',
+ host: `${host.platform}/${host.arch}`,
+ target: row.target,
+ suffix: row.suffix,
+ node: process.version,
+ job: process.env.GITHUB_JOB ?? null,
+ run: process.env.GITHUB_RUN_ID ?? null,
+ }
+ const started = io.now()
+ io.emit({ ...context, phase: 'start' })
+ let status = 'failed'
+ try {
+ io.run(
+ process.execPath,
+ [
+ resolve(root, 'bindings/devup-ui-binding/build.mjs'),
+ '--target',
+ row.target,
+ ],
+ {
+ cwd: root,
+ env: process.env,
+ stdio: 'inherit',
+ },
+ )
+ status = 'success'
+ } finally {
+ io.emit({
+ ...context,
+ phase: 'finish',
+ status,
+ elapsedMs: io.now() - started,
+ })
+ }
+}
+
+if (
+ process.argv[1] &&
+ resolve(process.argv[1]) === fileURLToPath(import.meta.url)
+)
+ buildHost()
diff --git a/e2e/binding/package/build-host.test.mjs b/e2e/binding/package/build-host.test.mjs
new file mode 100644
index 000000000..f7ad1d529
--- /dev/null
+++ b/e2e/binding/package/build-host.test.mjs
@@ -0,0 +1,166 @@
+import assert from 'node:assert/strict'
+import { resolve } from 'node:path'
+import { test } from 'node:test'
+import { fileURLToPath } from 'node:url'
+
+import {
+ buildHost,
+ HostBuildTargetError,
+ selectHostTarget,
+} from './build-host.mjs'
+import { targets } from './matrix.mjs'
+
+const root = fileURLToPath(new URL('../../../', import.meta.url))
+
+for (const row of targets) {
+ test(`selects canonical host triple for ${row.suffix}`, () => {
+ // Given
+ let reports = 0
+ const host = {
+ platform: row.os,
+ arch: row.cpu,
+ report() {
+ reports++
+ return {
+ header: row.libc === 'glibc' ? { glibcVersionRuntime: '2.39' } : {},
+ }
+ },
+ }
+ // When
+ const selected = selectHostTarget(host)
+ // Then
+ assert.equal(selected, row)
+ assert.equal(reports, row.os === 'linux' ? 1 : 0)
+ })
+}
+
+for (const [platform, arch] of [
+ ['freebsd', 'x64'],
+ ['win32', 'ia32'],
+ ['linux', 'riscv64'],
+]) {
+ test(`rejects unsupported ${platform}/${arch} before any spawn`, () => {
+ // Given
+ const calls = []
+ const host = { platform, arch, report: () => calls.push('report') }
+ const io = {
+ run: () => calls.push('run'),
+ now: () => calls.push('clock'),
+ emit: () => calls.push('emit'),
+ }
+ // When / Then
+ assert.throws(() => buildHost(host, io), HostBuildTargetError)
+ assert.deepEqual(calls, [])
+ })
+}
+
+test('preserves a Linux report failure without spawning', () => {
+ // Given
+ const failure = new Error('report unavailable')
+ const calls = []
+ const host = {
+ platform: 'linux',
+ arch: 'arm64',
+ report() {
+ throw failure
+ },
+ }
+ const io = {
+ run: () => calls.push('run'),
+ now: () => 0,
+ emit: () => calls.push('emit'),
+ }
+ // When / Then
+ assert.throws(
+ () => buildHost(host, io),
+ (error) => error instanceof HostBuildTargetError && error.cause === failure,
+ )
+ assert.deepEqual(calls, [])
+})
+
+for (const report of [null, '', {}, { header: null }, { header: 'bad' }]) {
+ test(`rejects indeterminate report ${JSON.stringify(report)}`, () => {
+ // Given
+ const host = { platform: 'linux', arch: 'x64', report: () => report }
+ // When / Then
+ assert.throws(() => selectHostTarget(host), HostBuildTargetError)
+ })
+}
+
+test('delegates to the existing driver and emits measured completion', () => {
+ // Given
+ const calls = []
+ const records = []
+ let ticks = 0
+ const host = {
+ platform: 'win32',
+ arch: 'arm64',
+ report() {
+ assert.fail('non-Linux host')
+ },
+ }
+ const io = {
+ run: (...args) => calls.push(args),
+ now: () => 100 + ticks++ * 75,
+ emit: (record) => records.push(record),
+ }
+ // When
+ buildHost(host, io)
+ // Then
+ assert.deepEqual(calls, [
+ [
+ process.execPath,
+ [
+ resolve(root, 'bindings/devup-ui-binding/build.mjs'),
+ '--target',
+ 'aarch64-pc-windows-msvc',
+ ],
+ {
+ cwd: root,
+ env: process.env,
+ stdio: 'inherit',
+ },
+ ],
+ ])
+ assert.equal(records[0].phase, 'start')
+ assert.deepEqual(records[1], {
+ ...records[0],
+ phase: 'finish',
+ status: 'success',
+ elapsedMs: 75,
+ })
+ assert.equal(records[1].host, 'win32/arm64')
+ assert.equal(records[1].target, 'aarch64-pc-windows-msvc')
+})
+
+test('propagates driver failure and emits a measured failed finish', () => {
+ // Given
+ const failure = new Error('driver failed')
+ const records = []
+ let ticks = 0
+ const host = {
+ platform: 'darwin',
+ arch: 'x64',
+ report() {
+ assert.fail('non-Linux host')
+ },
+ }
+ const io = {
+ run() {
+ throw failure
+ },
+ now: () => ticks++ * 25,
+ emit: (record) => records.push(record),
+ }
+ // When / Then
+ assert.throws(
+ () => buildHost(host, io),
+ (error) => error === failure,
+ )
+ assert.deepEqual(records[1], {
+ ...records[0],
+ phase: 'finish',
+ status: 'failed',
+ elapsedMs: 25,
+ })
+})
diff --git a/e2e/plugins/engine-consumer.mjs b/e2e/plugins/engine-consumer.mjs
new file mode 100644
index 000000000..eae588f6a
--- /dev/null
+++ b/e2e/plugins/engine-consumer.mjs
@@ -0,0 +1,3080 @@
+import assert from 'node:assert/strict'
+import { randomUUID } from 'node:crypto'
+import {
+ mkdirSync,
+ readdirSync,
+ readFileSync,
+ realpathSync,
+ rmSync,
+ writeFileSync,
+} from 'node:fs'
+import Module, { createRequire } from 'node:module'
+import { dirname, join, relative, resolve } from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+import { inspect } from 'node:util'
+
+export function viteRealpath(filename) {
+ try {
+ return realpathSync.native(filename)
+ } catch (error) {
+ if (error?.code === 'EISDIR') return realpathSync(filename)
+ throw error
+ }
+}
+
+export async function collectViteCases(cases, visit) {
+ const results = []
+ for (const name of cases) {
+ try {
+ results.push({ name, ok: true, value: await visit(name) })
+ } catch (error) {
+ results.push({ name, ok: false, error: errorEvidence(error) })
+ }
+ }
+ return results
+}
+
+export function completedReceipt(receipts, completed, id) {
+ assert(
+ completed.has(id) && receipts.has(id),
+ `Missing complete requested-boundary receipt: ${id}`,
+ )
+ return receipts.get(id)
+}
+
+export const webpackDistFiles = [
+ 'index.cjs',
+ 'index.mjs',
+ 'loader.cjs',
+ 'loader.mjs',
+ 'css-loader.cjs',
+ 'css-loader.mjs',
+]
+
+export const initialLayerGate = `
+const pending = new Map()
+let requested = false
+let open = false
+function releaseReady() {
+ if (!requested || !pending.has('A') || !pending.has('B')) return
+ open = true
+ for (const name of ['A', 'B']) pending.get(name)()
+ pending.clear()
+}
+module.exports = function(source) {
+ const layer = /[\\\\/]([AB])\\.css\\.ts$/.exec(this.resourcePath)?.[1]
+ if (!open && layer) {
+ const callback = this.async()
+ pending.set(layer, () => callback(null, source))
+ releaseReady()
+ return
+ }
+ return source
+}
+module.exports.release = () => {
+ requested = true
+ releaseReady()
+}
+`
+
+export function assertWebpackDist(directory) {
+ for (const name of webpackDistFiles) {
+ const code = readFileSync(join(directory, name), 'utf8')
+ assert.doesNotMatch(code, /['"]@devup-ui\/(?:wasm|binding)(?:['"/])/, name)
+ assert.match(code, /@devup-ui\/plugin-utils/, name)
+ }
+}
+
+export function assertRsbuildDist(directory) {
+ for (const name of ['index.cjs', 'index.mjs']) {
+ const code = readFileSync(join(directory, name), 'utf8')
+ assert.doesNotMatch(code, /['"]@devup-ui\/(?:wasm|binding)(?:['"/])/)
+ assert.match(code, /@devup-ui\/plugin-utils/)
+ }
+}
+
+export function assertBunDist(directory) {
+ assertRsbuildDist(directory)
+ for (const name of ['register.cjs', 'register.mjs']) {
+ const code = readFileSync(join(directory, name), 'utf8')
+ assert.doesNotMatch(code, /['"]@devup-ui\/(?:wasm|binding)(?:['"/])/)
+ assert.match(code, /@devup-ui\/plugin-utils/)
+ }
+}
+
+export function assertRawParity(expected, actual) {
+ assert.deepEqual(comparisonData(actual), comparisonData(expected))
+}
+
+export function assertViteParity(expected, actual, oppositeSingleCss = false) {
+ // Opposite admission changes intermediate return CSS, never published CSS.
+ if (!oppositeSingleCss) return assertRawParity(expected, actual)
+ const project = (receipt) => {
+ const copy = structuredClone(receipt)
+ for (const phase of copy.phases)
+ for (const output of Object.values(phase.outputs)) {
+ assert(Object.hasOwn(output, 'css'))
+ delete output.css
+ }
+ return copy
+ }
+ assertRawParity(project(expected), project(actual))
+}
+
+// Native/native AND WASM/WASM A/A reproduce this intermediate drift.
+// Only extraction-return CSS at generations[*].outputs[*], never published CSS.
+const rsbuildIntermediateFields = {
+ receipt: 'compilationProofs',
+ output: 'css',
+}
+
+export function assertRsbuildParity(expected, actual) {
+ const omit = (record, field) => {
+ assert(Object.hasOwn(record, field), `Missing raw Rsbuild ${field}`)
+ return Object.fromEntries(
+ Object.entries(record).filter(([key]) => key !== field),
+ )
+ }
+ const project = (receipt) => ({
+ ...omit(receipt, rsbuildIntermediateFields.receipt),
+ generations: receipt.generations.map((generation) => ({
+ ...generation,
+ outputs: Object.fromEntries(
+ Object.entries(generation.outputs).map(([file, output]) => [
+ file,
+ omit(output, rsbuildIntermediateFields.output),
+ ]),
+ ),
+ })),
+ })
+ assertRawParity(project(expected), project(actual))
+}
+
+// Identity tokens are process-local proofs, never function source or symbol text.
+// Cross-process identity-bearing diagnostics deliberately fail closed.
+const identityScope = randomUUID()
+const identities = new Map()
+
+function identity(value) {
+ if (!identities.has(value)) identities.set(value, identities.size)
+ return { scope: identityScope, id: identities.get(value) }
+}
+
+function prototypeNames(value) {
+ const names = []
+ for (
+ let proto = Object.getPrototypeOf(value);
+ proto;
+ proto = Object.getPrototypeOf(proto)
+ ) {
+ names.push(proto.constructor?.name)
+ }
+ return names
+}
+
+// Assignment audit: loader.ts preserves Errors; both bindings construct realm
+// Errors without assigning these runtime creation fields. Descriptors and key
+// order still compare exactly; `code` and diagnostic locations are not exempt.
+const runtimeErrorKeys = new Set([
+ 'stack',
+ 'originalLine',
+ 'originalColumn',
+ 'line',
+ 'column',
+ 'sourceURL',
+])
+const optionalBunCreationKeys = new Set(
+ process.versions.bun ? ['originalLine', 'originalColumn'] : [],
+)
+
+function assertOptionalCreationData(property, kind, valueType) {
+ assert.equal(kind, 'data')
+ assert.equal(property.configurable, true)
+ assert.equal(property.enumerable, false)
+ assert.equal(property.writable, true)
+ assert.equal(valueType, 'number')
+}
+
+function diagnosticKeys(value) {
+ return Reflect.ownKeys(value).filter((key) => {
+ if (!(value instanceof Error) || !optionalBunCreationKeys.has(key))
+ return true
+ const property = Object.getOwnPropertyDescriptor(value, key)
+ assertOptionalCreationData(
+ property,
+ 'value' in property ? 'data' : 'accessor',
+ typeof property.value,
+ )
+ return false
+ })
+}
+
+function stackPrefix(stack, strict = true) {
+ if (typeof stack !== 'string') {
+ if (strict) assert.fail('Runtime stack must be a string')
+ return undefined
+ }
+ const lines = stack.split('\n')
+ const frame = lines.findIndex(
+ (line) => /^\s+at\s/.test(line) || /@.*:\d+(?::\d+)?$/.test(line),
+ )
+ if (frame < 0) {
+ if (strict)
+ assert.fail('Unrecognized runtime stack shape; inspect raw evidence')
+ return undefined
+ }
+ return lines.slice(0, frame).join('\n')
+}
+
+export function errorEvidence(error) {
+ const seen = new Map()
+ const encode = (value) => {
+ if (value === null) return { type: 'null' }
+ const type = typeof value
+ switch (type) {
+ case 'undefined':
+ return { type }
+ case 'number':
+ return {
+ type,
+ value: Object.is(value, -0) ? '-0' : String(value),
+ }
+ case 'bigint':
+ return { type, value: String(value) }
+ case 'string':
+ case 'boolean':
+ return { type, value }
+ case 'symbol':
+ return {
+ type,
+ identity: identity(value),
+ description: encode(value.description),
+ registryKey: encode(Symbol.keyFor(value)),
+ }
+ case 'function':
+ case 'object':
+ break
+ default:
+ assert.fail(`Unsupported diagnostic value type: ${type}`)
+ }
+ if (seen.has(value)) return { reference: seen.get(value) }
+ const id = seen.size
+ seen.set(value, id)
+ const isError = value instanceof Error
+ const functionValue = type === 'function'
+ const exotic =
+ !isError &&
+ !functionValue &&
+ !Array.isArray(value) &&
+ Object.getPrototypeOf(value) !== Object.prototype &&
+ Object.getPrototypeOf(value) !== null
+ return {
+ id,
+ type: isError ? 'error' : type,
+ ...(functionValue || exotic ? { identity: identity(value) } : {}),
+ ...(functionValue
+ ? { source: Function.prototype.toString.call(value) }
+ : {}),
+ ...(exotic
+ ? {
+ inspection: inspect(value, {
+ showHidden: true,
+ depth: null,
+ getters: false,
+ customInspect: false,
+ maxArrayLength: null,
+ maxStringLength: null,
+ }),
+ }
+ : {}),
+ constructor: encode(value.constructor?.name),
+ prototypes: prototypeNames(value).map(encode),
+ properties: Reflect.ownKeys(value).map((key) => {
+ const descriptor = Object.getOwnPropertyDescriptor(value, key)
+ const runtime = isError && runtimeErrorKeys.has(key)
+ const result = {
+ key: encode(key),
+ configurable: descriptor.configurable,
+ enumerable: descriptor.enumerable,
+ kind: 'value' in descriptor ? 'data' : 'accessor',
+ runtime,
+ }
+ if ('value' in descriptor) {
+ result.writable = descriptor.writable
+ result.value = encode(descriptor.value)
+ } else {
+ result.get = encode(descriptor.get)
+ result.set = encode(descriptor.set)
+ }
+ if (runtime) {
+ // Retain both the raw descriptor and the runtime-observed value.
+ const observed = Reflect.get(value, key)
+ result.observed = encode(observed)
+ result.observedType = typeof observed
+ if (key === 'stack') result.prefix = stackPrefix(observed, false)
+ }
+ return result
+ }),
+ }
+ }
+ return { diagnostic: 'full-error-v1', raw: encode(error) }
+}
+
+function comparisonData(value) {
+ if (value === null || typeof value !== 'object') return value
+ if (Array.isArray(value)) return value.map(comparisonData)
+ if (value.diagnostic === 'full-error-v1') {
+ const project = (node) => {
+ if (node === null || typeof node !== 'object') return node
+ if (Array.isArray(node)) return node.map(project)
+ if (node.type === 'error') {
+ assert(
+ node.properties.some(
+ ({ key, runtime }) =>
+ key.type === 'string' && key.value === 'stack' && runtime,
+ ),
+ 'Missing runtime stack on Error; inspect raw evidence',
+ )
+ const properties = node.properties.filter((property) => {
+ if (
+ !property.runtime ||
+ property.key.type !== 'string' ||
+ !optionalBunCreationKeys.has(property.key.value)
+ )
+ return true
+ assertOptionalCreationData(
+ property,
+ property.kind,
+ property.value?.type,
+ )
+ assert.equal(property.observedType, 'number')
+ return false
+ })
+ return Object.fromEntries(
+ Object.entries(node).map(([key, item]) => [
+ key,
+ project(key === 'properties' ? properties : item),
+ ]),
+ )
+ }
+ if (node.runtime === true) {
+ const { value, get, set, observed, ...contract } = node
+ if (node.key.type === 'string' && node.key.value === 'stack') {
+ assert.equal(node.observedType, 'string')
+ assert.equal(
+ typeof node.prefix,
+ 'string',
+ 'Unrecognized runtime stack',
+ )
+ }
+ // Only runtime-created values/accessors are excluded, not descriptors.
+ return {
+ ...contract,
+ valueType: value?.type,
+ getType: get?.type,
+ setType: set?.type,
+ }
+ }
+ return Object.fromEntries(
+ Object.entries(node).map(([key, item]) => [key, project(item)]),
+ )
+ }
+ return project(value.raw)
+ }
+ return Object.fromEntries(
+ Object.entries(value).map(([key, item]) => [key, comparisonData(item)]),
+ )
+}
+
+export function assertErrorParity(expected, actual) {
+ if (
+ expected?.diagnostic === 'full-error-v1' ||
+ actual?.diagnostic === 'full-error-v1'
+ ) {
+ assert.equal(expected?.diagnostic, 'full-error-v1')
+ assert.equal(actual?.diagnostic, 'full-error-v1')
+ return assertRawParity(expected, actual)
+ }
+ const left = new Map()
+ const right = new Map()
+ const compare = (a, b) => {
+ assert.equal(typeof a, typeof b)
+ if (a === null || b === null || typeof a !== 'object') {
+ // Includes symbol values, functions and accessor functions: live identity.
+ assert.equal(a, b)
+ return
+ }
+ if (left.has(a) || right.has(b)) {
+ assert.equal(left.get(a), b)
+ assert.equal(right.get(b), a)
+ return
+ }
+ left.set(a, b)
+ right.set(b, a)
+ assert.equal(a.constructor?.name, b.constructor?.name)
+ assert.deepEqual(prototypeNames(a), prototypeNames(b))
+ assert.equal(a instanceof Error, b instanceof Error)
+ const keys = diagnosticKeys(a)
+ // Symbol keys are compared by identity, not description or registry text.
+ assert.deepEqual(keys, diagnosticKeys(b))
+ if (a instanceof Error) {
+ assert(Object.hasOwn(a, 'stack'))
+ assert(Object.hasOwn(b, 'stack'))
+ } else if (
+ !Array.isArray(a) &&
+ Object.getPrototypeOf(a) !== Object.prototype &&
+ Object.getPrototypeOf(a) !== null
+ ) {
+ // Compare built-in internal slots as well as their own descriptors below.
+ assert.deepEqual(a, b)
+ }
+ for (const key of keys) {
+ const x = Object.getOwnPropertyDescriptor(a, key)
+ const y = Object.getOwnPropertyDescriptor(b, key)
+ assert.equal(x.configurable, y.configurable)
+ assert.equal(x.enumerable, y.enumerable)
+ assert.equal('value' in x, 'value' in y)
+ assert.equal(x.writable, y.writable)
+ if (a instanceof Error && runtimeErrorKeys.has(key)) {
+ assert.equal(typeof x.value, typeof y.value)
+ assert.equal(typeof x.get, typeof y.get)
+ assert.equal(typeof x.set, typeof y.set)
+ const av = Reflect.get(a, key)
+ const bv = Reflect.get(b, key)
+ assert.equal(typeof av, typeof bv)
+ if (key === 'stack') assert.equal(stackPrefix(av), stackPrefix(bv))
+ } else if ('value' in x) {
+ compare(x.value, y.value)
+ } else {
+ assert.equal(x.get, y.get)
+ assert.equal(x.set, y.set)
+ }
+ }
+ }
+ compare(expected, actual)
+}
+
+export function diagnosticHook(hook, receive) {
+ return async function (...args) {
+ try {
+ return await hook.apply(this, args)
+ } catch (error) {
+ receive(errorEvidence(error), error)
+ throw error
+ }
+ }
+}
+
+export function observeRejection(work) {
+ const settled = work.then(
+ (value) => () => value,
+ (error) => () => {
+ throw error
+ },
+ )
+ return async () => (await settled)()
+}
+
+function put(path, content) {
+ mkdirSync(dirname(path), { recursive: true })
+ writeFileSync(path, content)
+}
+
+function tree(directory) {
+ return Object.fromEntries(
+ readdirSync(directory, { recursive: true, withFileTypes: true })
+ .filter((entry) => entry.isFile())
+ .map((entry) => {
+ const absolute = join(entry.parentPath, entry.name)
+ return [absolute, readFileSync(absolute, 'utf8')]
+ })
+ .sort(([a], [b]) => a.localeCompare(b)),
+ )
+}
+
+export function assertViteBuckets(phase, color) {
+ for (const name of ['A', 'B']) {
+ const code = phase.requested.transforms[`/src/${name}.js`].code
+ const urls = [
+ ...code.matchAll(/["']([^"'\n]*devup-ui(?:-\d+)?\.css[^"'\n]*)["']/g),
+ ].map((match) => match[1])
+ const numbered = urls.filter((url) => /devup-ui-\d+\.css/.test(url))
+ const buckets = numbered.length ? numbered : urls
+ assert(buckets.length > 0, `Missing mapped ${name} CSS import`)
+ for (const url of buckets) {
+ const file = url.split('?')[0].split('/').at(-1)
+ const disk = Object.entries(phase.disk).find(([path]) =>
+ path.replaceAll('\\', '/').endsWith(`/${file}`),
+ )
+ assert(disk, `Missing mapped disk bucket ${file}`)
+ assert.match(disk[1], color)
+ assert.match(phase.requested.http[`${url}&direct`].body, color)
+ }
+ }
+}
+
+async function consumeVite(context) {
+ const {
+ directory,
+ scenario,
+ selection,
+ singleCss,
+ destination,
+ create,
+ selected,
+ outputs,
+ calls,
+ utils,
+ observations,
+ } = context
+ const phases = []
+ let failure
+ let clock = 2000000000000
+ Date.now = () => clock
+ const { build } = await import('vite')
+ const save = () => {
+ const replacer = (_, value) =>
+ value === undefined ? { undefined: true } : value
+ put(
+ `${destination}.trace.json`,
+ JSON.stringify({ observations }, replacer, 2),
+ )
+ put(destination, JSON.stringify({ phases, failure }, replacer, 2))
+ }
+ const prepare = () => {
+ for (const name of ['src', 'df', 'dist'])
+ rmSync(join(directory, name), { recursive: true, force: true })
+ put(join(directory, 'devup.json'), '{"extends":["./theme.json"]}')
+ put(
+ join(directory, 'theme.json'),
+ '{"theme":{"length":{"default":{"size":"17px"}}}}',
+ )
+ put(
+ join(directory, 'index.html'),
+ '',
+ )
+ put(join(directory, 'src/tokens.js'), 'export const color="maroon";')
+ for (const name of ['A', 'B']) {
+ put(
+ join(directory, `src/${name}.js`),
+ `import {css} from '@devup-ui/react'; import {color} from './tokens.js'; export const cls=css({color,w:'$size',insetX:'19px',p:${name === 'A' ? 1 : 2}}); if(import.meta.hot) import.meta.hot.accept();`,
+ )
+ }
+ put(
+ join(directory, 'src/entry.js'),
+ 'import {cls as a} from "./A.js"; import {cls as b} from "./B.js"; document.body.className=a+" "+b;',
+ )
+ }
+ const snapshot = (name, requested = {}) => {
+ const disk = tree(join(directory, 'df'))
+ const state = {
+ css: selected.getCss(null, false),
+ sheet: selected.exportSheet(),
+ classes: selected.exportClassMap(),
+ files: selected.exportFileMap(),
+ canonical: selected.exportCanonicalMap(),
+ residual: selected.getUnreservedClassSlotCount(),
+ prefix: selected.getPrefix(),
+ debug: selected.isDebug(),
+ theme: selected.getDefaultTheme(),
+ }
+ const phase = {
+ name,
+ requested,
+ disk,
+ state,
+ outputs: Object.fromEntries(
+ [...outputs].sort(([a], [b]) => a.localeCompare(b)),
+ ),
+ calls: { ...calls },
+ }
+ phases.push(phase)
+ save()
+ for (const [path, css] of Object.entries(disk)) {
+ if (path.endsWith('.css'))
+ assert.equal(
+ css,
+ selected.getCss(utils.getFileNumByFilename(path), false),
+ path,
+ )
+ }
+ assert.equal(state.residual, 0)
+ return phase
+ }
+ const config = () => {
+ const arrival = Promise.withResolvers()
+ const first = scenario === 'opposite' ? 'B' : 'A'
+ return {
+ root: directory,
+ configFile: false,
+ logLevel: 'error',
+ plugins: [
+ {
+ name: 'vite-arrival-gate',
+ enforce: 'pre',
+ async transform(_code, id) {
+ if (/\/src\/[AB]\.js$/.test(id) && !id.endsWith(`/${first}.js`))
+ await arrival.promise
+ },
+ },
+ ...create({
+ ...selection,
+ singleCss,
+ atomHoist: 0,
+ shorthands: { insetX: ['left', 'right'] },
+ }),
+ {
+ name: 'vite-arrival-release',
+ enforce: 'post',
+ transform(_code, id) {
+ if (id.endsWith(`/src/${first}.js`)) arrival.resolve()
+ },
+ },
+ ],
+ optimizeDeps: { noDiscovery: true, include: [] },
+ server: {
+ host: '127.0.0.1',
+ port: Number(process.env.DEVUP_ENGINE_VITE_PORT),
+ strictPort: true,
+ },
+ build: {
+ minify: false,
+ cssMinify: false,
+ outDir: 'dist',
+ rolldownOptions: {
+ output: {
+ entryFileNames: '[name].js',
+ chunkFileNames: '[name].js',
+ assetFileNames: '[name][extname]',
+ },
+ },
+ },
+ }
+ }
+ try {
+ prepare()
+ if (scenario === 'late') {
+ const retained = create({ ...selection, singleCss })[0]
+ await retained.closeBundle()
+ const other = create({
+ engine: selection.engine === 'wasm' ? 'native' : 'wasm',
+ })
+ await other[0].closeBundle()
+ const before = calls.getCss
+ retained.load(join(directory, 'df/devup-ui/devup-ui.css'))
+ assert.equal(calls.getCss, before + 1)
+ await retained.closeBundle()
+ assert.equal(utils.getBuildOwnership().running, 0)
+ return { late: true, observations, calls: { ...calls } }
+ }
+ for (const sourcemap of [false, true]) {
+ selected.resetStateForTesting()
+ outputs.clear()
+ const options = config()
+ const result = await build({
+ ...options,
+ build: { ...options.build, sourcemap },
+ })
+ assert(!Array.isArray(result) && 'output' in result)
+ const assets = Object.fromEntries(
+ result.output.map((item) => [
+ item.fileName,
+ item.type === 'chunk'
+ ? { code: item.code, map: item.map }
+ : { source: String(item.source) },
+ ]),
+ )
+ snapshot(`build-map-${sourcemap}`, {
+ assets,
+ emitted: tree(join(directory, 'dist')),
+ })
+ assert.equal(
+ Object.keys(assets).some((name) => name.endsWith('.js.map')),
+ sourcemap,
+ )
+ const publishedCss = result.output
+ .filter(
+ (item) => item.type === 'asset' && item.fileName.endsWith('.css'),
+ )
+ .map((item) => String(item.source))
+ .join('\n')
+ assert(publishedCss.length > 0)
+ for (const name of ['A', 'B'])
+ assert(
+ outputs.has(join(directory, `src/${name}.js`).replaceAll('\\', '/')),
+ )
+ for (const [id, output] of outputs) {
+ if (/\/src\/[AB]\.js$/.test(id))
+ assert.match(output.code, /\bcls\s*=\s*["'][^"']+["']/)
+ for (const match of output.code.matchAll(
+ /\bcls\s*=\s*["']([^"']+)["']/g,
+ )) {
+ if (/\/src\/[AB]\.js$/.test(id)) {
+ const css = assets[output.cssFile.split('/').at(-1)]?.source
+ for (const side of ['left', 'right'])
+ assert(
+ match[1]
+ .split(' ')
+ .some((name) => css?.includes(`.${name}{${side}:19px}`)),
+ )
+ }
+ for (const name of match[1].split(' '))
+ assert(
+ publishedCss.includes(`.${name}`),
+ `Missing published class ${name}`,
+ )
+ }
+ }
+ for (const item of result.output) {
+ if (item.type === 'chunk')
+ assert.doesNotMatch(
+ item.code,
+ /@devup-ui\/(?:wasm|binding|plugin-utils)|Cannot run on the runtime/,
+ )
+ }
+ assert.equal(utils.getBuildOwnership().running, 0)
+ }
+ return { phases }
+ } catch (error) {
+ failure = errorEvidence(error)
+ throw error
+ } finally {
+ save()
+ assert.equal(utils.getBuildOwnership().running, 0)
+ }
+}
+
+async function viteErrors(context) {
+ const { directory, create, singleCss, utils, destination } = context
+ const { createServer } = await import('vite')
+ const pairs = []
+ const results = await collectViteCases(
+ ['located', 'resolver', 'registration', 'reservation'],
+ async (kind) => {
+ const errors = []
+ const bundler = []
+ const raw = []
+ const disks = []
+ const violations = []
+ const causes = []
+ const identities = []
+ for (const engine of ['native', 'wasm']) {
+ const cause = new Error(`Vite ${kind} cause`, {
+ cause: new Error('nested cause'),
+ })
+ Object.defineProperty(cause, 'hidden', { value: 'resolver detail' })
+ causes.push(errorEvidence(cause))
+ const backend = utils.loadCompiler(engine)
+ backend.resetStateForTesting()
+ const originals = new Map()
+ const immediate = []
+ let server, owner, before
+ for (const name of ['src', 'df', 'dist'])
+ rmSync(join(directory, name), { recursive: true, force: true })
+ put(join(directory, 'devup.json'), '{}')
+ put(
+ join(directory, 'src/error.js'),
+ 'import {css} from "@devup-ui/react"; import {color} from "./throws.js"; export const x=css({color});',
+ )
+ put(join(directory, 'src/throws.js'), 'export const color="maroon";')
+ const inject = (name, replacement) => {
+ originals.set(name, backend[name])
+ backend[name] = replacement
+ }
+ try {
+ const plugins = create({
+ engine,
+ singleCss,
+ atomHoist: 0,
+ extractCss: true,
+ })
+ owner = plugins[0]
+ for (const name of ['configResolved', 'buildStart', 'transform']) {
+ owner[name] = diagnosticHook(owner[name], (evidence, error) => {
+ errors.push(error)
+ immediate.push({ hook: name, evidence })
+ })
+ }
+ if (kind === 'resolver') {
+ const setter = backend.setModuleResolver
+ inject('setModuleResolver', () =>
+ setter(() => {
+ throw cause
+ }),
+ )
+ }
+ if (kind === 'registration')
+ inject('registerTheme', () => {
+ throw cause
+ })
+ if (kind === 'reservation')
+ inject('reserveClassSlots', () => {
+ throw cause
+ })
+ const invalid = {
+ name: 'vite-located-error',
+ enforce: 'pre',
+ transform(code, id) {
+ if (id.endsWith('/error.js') && kind === 'located')
+ return 'import {Box} from "@devup-ui/react"; export const x=;'
+ return code
+ },
+ }
+ server = await createServer({
+ root: directory,
+ configFile: false,
+ cacheDir: join(directory, '.vite'),
+ logLevel: 'error',
+ plugins: [invalid, ...plugins],
+ optimizeDeps: { noDiscovery: true, include: [] },
+ })
+ await server.environments.client.pluginContainer.buildStart()
+ before = tree(join(directory, 'df'))
+ try {
+ await server.environments.client.transformRequest('/src/error.js')
+ } catch (error) {
+ bundler.push(error)
+ }
+ assert.deepEqual(tree(join(directory, 'df')), before)
+ } catch (error) {
+ bundler.push(error)
+ if (!errors.includes(error)) violations.push(errorEvidence(error))
+ } finally {
+ raw.push(immediate)
+ identities.push({
+ thrown: errors.at(-1) === cause,
+ cause: errors.at(-1)?.cause === cause,
+ })
+ disks.push({ engine, before, after: tree(join(directory, 'df')) })
+ try {
+ try {
+ await server?.close()
+ } finally {
+ await owner?.closeBundle()
+ }
+ } catch (error) {
+ violations.push(errorEvidence(error))
+ } finally {
+ for (const [name, method] of originals) backend[name] = method
+ backend.resetStateForTesting()
+ }
+ }
+ }
+ const pair = {
+ kind,
+ raw,
+ disks,
+ violations,
+ bundler: bundler.map(errorEvidence),
+ causes,
+ identities,
+ }
+ pairs.push(pair)
+ if (errors.length === 2) {
+ const keys = raw.map((rows) =>
+ rows
+ .at(-1)
+ .evidence.raw.properties.map(({ key }) => JSON.stringify(key)),
+ )
+ pair.nativeOnly = keys[0].filter((key) => !keys[1].includes(key))
+ if (pair.nativeOnly.length) pair.owner = '#796'
+ }
+ put(destination, JSON.stringify({ pairs }, null, 2))
+ assert.deepEqual(violations, [])
+ assert.equal(errors.length, 2)
+ if (kind === 'registration')
+ for (const disk of disks)
+ assert(!Object.keys(disk.after).some((file) => file.endsWith('.css')))
+ assertErrorParity(raw[0].at(-1).evidence, raw[1].at(-1).evidence)
+ assertRawParity(raw[0], raw[1])
+ assertRawParity(identities[0], identities[1])
+ if (kind === 'resolver')
+ assert.deepEqual(identities[0], { thrown: false, cause: false })
+ if (kind === 'registration' || kind === 'reservation')
+ for (const observed of identities) assert.equal(observed.thrown, true)
+ return pair
+ },
+ )
+ put(destination, JSON.stringify({ pairs, results }, null, 2))
+ assert(
+ results.every((result) => result.ok),
+ 'Vite error diagnostics failed; inspect all pairs',
+ )
+ return { pairs, results }
+}
+
+async function consumeRsbuild(context) {
+ const {
+ directory,
+ scenario,
+ selection,
+ singleCss,
+ destination,
+ require,
+ create,
+ selected,
+ outputs,
+ calls,
+ utils,
+ } = context
+ const { createRsbuild } = require('@rsbuild/core')
+ const sourcePath = join(directory, 'src/entry.js')
+ const received = []
+ const callbacks = []
+ const served = new Map()
+ const generations = []
+ const compilationProofs = []
+ const proofs = new WeakMap()
+ let handledSource, pendingSource, failure, port, watchReady
+ let owner, closeBuild, lastCompilation
+ let additionalPasses = 0
+ let deleted = 0
+ let layersReleased = false
+ let completed
+ const nextCompile = (
+ source = readFileSync(join(directory, 'src/B.css.ts'), 'utf8'),
+ ) => {
+ pendingSource = source
+ return new Promise((resolve, reject) => {
+ completed = { resolve, reject }
+ if (failure) reject(failure)
+ })
+ }
+ const hold = scenario === 'additional'
+ const ordered = hold || scenario === 'dev'
+ const sourceOrder = ['base.css.ts', 'A.css.ts', 'B.css.ts'].map((name) =>
+ join(directory, 'src', name),
+ )
+ const stages = new Map(
+ sourceOrder.map((path) => [path, Promise.withResolvers()]),
+ )
+ const pendingCss = new Set()
+ const unblock = () => {
+ for (const stage of stages.values()) stage.resolve()
+ if (hold) require(gateFile).release()
+ }
+ const gateFile = join(directory, 'hold.cjs')
+ put(gateFile, initialLayerGate)
+ if (hold) put(join(directory, 'src/empty.js'), 'export const empty = 1;')
+ const saveFailure = (error, stats) => {
+ const compilations = stats
+ ? (stats.stats ?? [stats]).map((child) => child.compilation)
+ : lastCompilation
+ ? [lastCompilation]
+ : []
+ put(
+ destination,
+ JSON.stringify(
+ {
+ failure: errorEvidence(error),
+ compilationProofs,
+ received: received.map(errorEvidence),
+ bundlerDiagnostics: compilations.map((compilation) =>
+ compilation.errors.map(errorEvidence),
+ ),
+ },
+ null,
+ 2,
+ ),
+ )
+ }
+ const assetsOf = (compilation) =>
+ Object.fromEntries(
+ compilation
+ .getAssets()
+ .map(({ name, source }) => [name, String(source.source())])
+ .sort(([a], [b]) => a.localeCompare(b)),
+ )
+ const snapshot = (stats, source = handledSource, boundary = true) => {
+ const revisionColor = source.match(/borderColor:\s*["']([^"']+)["']/)[1]
+ if (stats.hasErrors()) saveFailure(undefined, stats)
+ assert(!stats.hasErrors(), stats.toString({ all: false, errors: true }))
+ const children = stats.stats ?? [stats]
+ assert.equal(children.length, 1)
+ const assets = assetsOf(children[0].compilation)
+ const css = selected.getCss(null, false)
+ assert.match(css, /cyan/)
+ assert.match(
+ css,
+ revisionColor === 'goldenrod' ? /goldenrod/ : /rebeccapurple/,
+ )
+ for (const [path, value] of served) {
+ assert.equal(
+ value,
+ selected.getCss(utils.getFileNumByFilename(path), true),
+ )
+ }
+ if (boundary) assert(served.size > 0)
+ const shippedCss = Object.entries(assets)
+ .filter(([name]) => name.endsWith('.css'))
+ .map(([, value]) => value)
+ .join('\n')
+ assert.match(shippedCss, /border-color:\s*#0ff\b/)
+ const border = {
+ goldenrod: '#daa520',
+ rebeccapurple: '#639',
+ teal: '(?:teal|#008080)',
+ }[revisionColor]
+ assert.match(shippedCss, new RegExp(`border-color:\\s*${border}\\b`))
+ assert.match(
+ shippedCss,
+ new RegExp(
+ `body\\s*\\{[^}]*background-color:\\s*${revisionColor === 'goldenrod' ? '#daa520' : '#639'}\\b`,
+ ),
+ )
+ const b = outputs
+ .get(join(directory, 'src/B.css.ts'))
+ .code.match(/\blayer\s*=\s*["']([^"']+)["']/)
+ assert(b)
+ for (const cls of b[1].split(' ')) {
+ assert.match(
+ shippedCss,
+ new RegExp(`\\.${cls}\\s*\\{[^}]*border-color:\\s*${border}\\b`),
+ )
+ assert(
+ Object.entries(assets).some(
+ ([name, code]) => name.endsWith('.js') && code.includes(cls),
+ ),
+ )
+ }
+ for (const [name, value] of Object.entries(assets)) {
+ if (name.endsWith('.js')) {
+ assert.doesNotMatch(
+ value,
+ /Cannot run on the runtime|@devup-ui\/(?:binding|wasm)/,
+ )
+ }
+ }
+ if (!boundary) return
+ return {
+ assets,
+ outputs: Object.fromEntries(
+ [...outputs].sort(([a], [b]) => a.localeCompare(b)),
+ ),
+ css,
+ served: [...served].sort(([a], [b]) => a.localeCompare(b)),
+ sheet: selected.exportSheet(),
+ classes: selected.exportClassMap(),
+ files: selected.exportFileMap(),
+ canonical: selected.exportCanonicalMap(),
+ disk: tree(join(directory, 'df')),
+ residual: selected.getUnreservedClassSlotCount(),
+ }
+ }
+ const compiled = ({ stats }) => {
+ try {
+ assert(!stats.hasErrors(), stats.toString({ all: false, errors: true }))
+ const compilation = stats.compilation
+ const proof = proofs.get(compilation)
+ proof.needAdditionalPass = compilation.needAdditionalPass
+ proof.remaining = assetsOf(compilation)
+ if (compilation.needAdditionalPass) {
+ assert(Object.values(proof.removed).some((bytes) => bytes.length > 0))
+ assert.deepEqual(proof.remaining, {})
+ assert.deepEqual(proof.emitted, {})
+ return
+ }
+ const boundary = Boolean(
+ completed && !watchReady && proof.source === pendingSource && !failure,
+ )
+ const generation = snapshot(stats, proof.source, boundary)
+ assert.deepEqual(proof.remaining, proof.emitted)
+ if (!boundary) return
+ watchReady = () => {
+ completed.resolve({
+ ...generation,
+ source: proof.source,
+ emitted: proof.emitted,
+ })
+ completed = undefined
+ watchReady = undefined
+ }
+ } catch (error) {
+ failure ??= error
+ saveFailure(failure, stats)
+ if (!completed) throw failure
+ completed.reject(failure)
+ }
+ }
+ const configure = async (options) => {
+ const instance = create(options)
+ return createRsbuild({
+ cwd: directory,
+ config: {
+ mode: scenario === 'dev' ? 'development' : 'production',
+ plugins: [
+ {
+ ...instance,
+ async setup(api) {
+ await instance.setup({
+ ...api,
+ onCloseBuild(fn) {
+ closeBuild = fn
+ api.onCloseBuild(fn)
+ },
+ transform(options, handler) {
+ callbacks.push(handler)
+ api.transform(options, async (input) => {
+ try {
+ const path = input.resourcePath
+ const index = sourceOrder.indexOf(path)
+ if (ordered && index > 0)
+ await stages.get(sourceOrder[index - 1]).promise
+ if (ordered && path.endsWith('.css'))
+ await stages.get(
+ sourceOrder[hold && !layersReleased ? 0 : 2],
+ ).promise
+ const result = await handler(input)
+ if (
+ hold &&
+ !layersReleased &&
+ (path === sourcePath || index === 0)
+ )
+ pendingCss.add(
+ resolve(dirname(path), outputs.get(path).cssFile),
+ )
+ stages.get(path)?.resolve()
+ if (
+ input.resourcePath === join(directory, 'src/B.css.ts')
+ ) {
+ handledSource = input.code
+ proofs.get(lastCompilation).source = handledSource
+ }
+ if (input.resourcePath.endsWith('.css')) {
+ served.set(input.resourcePath, result)
+ }
+ return result
+ } catch (error) {
+ unblock()
+ received.push(error)
+ saveFailure(error)
+ throw error
+ }
+ })
+ },
+ })
+ },
+ },
+ {
+ name: 'engine-proof',
+ setup(api) {
+ api.modifyRspackConfig((config) => {
+ if (hold) {
+ config.entry = () => ({ main: { import: [sourcePath] } })
+ config.module.rules.push({
+ test: /\.css\.ts$/,
+ enforce: 'pre',
+ use: [gateFile],
+ })
+ }
+ config.plugins.push({
+ apply(compiler) {
+ owner = compiler
+ const watchFileSystem = compiler.watchFileSystem
+ const watch = watchFileSystem.watch.bind(watchFileSystem)
+ watchFileSystem.watch = (...args) => {
+ const result = watch(...args)
+ watchReady?.()
+ return result
+ }
+ if (scenario !== 'errors')
+ compiler.hooks.done.tap('EngineProof', (stats) =>
+ compiled({ stats }),
+ )
+ compiler.hooks.failed.tap('EngineProof', (error) => {
+ unblock()
+ failure ??= error
+ saveFailure(failure)
+ completed?.reject(failure)
+ })
+ compiler.hooks.afterEmit.tap(
+ 'EngineProof',
+ (compilation) => {
+ proofs.get(compilation).emitted = Object.freeze(
+ assetsOf(compilation),
+ )
+ },
+ )
+ compiler.hooks.additionalPass.tap('EngineProof', () => {
+ additionalPasses++
+ proofs.get(lastCompilation).additionalPass = true
+ })
+ compiler.hooks.thisCompilation.tap(
+ 'EngineProof',
+ (compilation) => {
+ lastCompilation = compilation
+ const proof = { source: handledSource, removed: {} }
+ proofs.set(compilation, proof)
+ compilationProofs.push(proof)
+ served.clear()
+ const deleteAsset =
+ compilation.deleteAsset.bind(compilation)
+ compilation.deleteAsset = (name) => {
+ deleted++
+ Object.assign(proof.removed, assetsOf(compilation))
+ deleteAsset(name)
+ }
+ compilation.hooks.succeedModule.tap(
+ 'EngineProof',
+ (module) => {
+ if (
+ hold &&
+ !layersReleased &&
+ module
+ .identifier()
+ .includes(
+ compiler.rspack.CssExtractRspackPlugin.loader,
+ ) &&
+ pendingCss.delete(module.resource) &&
+ !pendingCss.size
+ ) {
+ layersReleased = true
+ require(gateFile).release()
+ }
+ },
+ )
+ },
+ )
+ },
+ })
+ })
+ },
+ },
+ ],
+ source: { entry: { main: hold ? './src/empty.js' : './src/entry.js' } },
+ output: {
+ distPath: { root: 'dist' },
+ minify: false,
+ filenameHash: false,
+ sourceMap: { js: 'source-map', css: true },
+ },
+ performance: { printFileSize: false },
+ html: { template: false },
+ server: { port: 0, host: '127.0.0.1', printUrls: false },
+ dev: { hmr: false, liveReload: false, lazyCompilation: false },
+ tools: {
+ rspack: {
+ cache: false,
+ // df exclusion isolates main follow-up (l), not a product fix.
+ watchOptions: { ignored: /[\\/](?:df|node_modules)[\\/]/ },
+ optimization: { minimize: false, concatenateModules: false },
+ },
+ },
+ },
+ })
+ }
+ if (scenario === 'errors') {
+ const userCause = new Error('user resolver callback failure', {
+ cause: new Error('nested user resolver cause'),
+ })
+ Object.defineProperty(userCause, 'hidden', {
+ value: 'user resolver detail',
+ })
+ const pairs = []
+ for (const callbackFailure of [false, true]) {
+ const errors = []
+ const invoked = []
+ const bundlerDiagnostics = []
+ for (const engine of ['native', 'wasm']) {
+ const backend = utils.loadCompiler(engine)
+ backend.resetStateForTesting()
+ const setResolver = backend.setModuleResolver
+ let compiler
+ let callbackCalls = 0
+ received.length = 0
+ try {
+ if (callbackFailure) {
+ backend.setModuleResolver = () =>
+ setResolver(() => {
+ callbackCalls++
+ throw userCause
+ })
+ }
+ put(
+ sourcePath,
+ callbackFailure
+ ? 'import { css } from "@devup-ui/react"; import { color } from "./throws.js"; export const x = css({ color });'
+ : 'import { Box } from "@devup-ui/react"; export const x = ;',
+ )
+ const rsbuild = await configure({ engine, singleCss, atomHoist: 0 })
+ compiler = await rsbuild.createCompiler()
+ const stats = await new Promise((resolve, reject) =>
+ compiler.run((error, stats) =>
+ error ? reject(error) : resolve(stats),
+ ),
+ )
+ saveFailure(undefined, stats)
+ bundlerDiagnostics.push(
+ (stats.stats ?? [stats]).map((child) =>
+ child.compilation.errors.map(errorEvidence),
+ ),
+ )
+ assert(stats.hasErrors())
+ assert.equal(received.length, 1)
+ errors.push(received[0])
+ invoked.push(callbackCalls)
+ } catch (error) {
+ saveFailure(error)
+ throw error
+ } finally {
+ try {
+ if (compiler)
+ await new Promise((resolve, reject) =>
+ compiler.close((error) => (error ? reject(error) : resolve())),
+ )
+ } finally {
+ closeBuild?.()
+ backend.setModuleResolver = setResolver
+ backend.resetStateForTesting()
+ }
+ }
+ }
+ pairs.push({
+ callbackFailure,
+ invoked,
+ bundlerDiagnostics,
+ native: errorEvidence(errors[0]),
+ wasm: errorEvidence(errors[1]),
+ nativeThrownIdentity: errors[0] === userCause,
+ wasmThrownIdentity: errors[1] === userCause,
+ nativeCauseIdentity: errors[0]?.cause === userCause,
+ wasmCauseIdentity: errors[1]?.cause === userCause,
+ })
+ put(
+ destination,
+ JSON.stringify({ pairs, userCause: errorEvidence(userCause) }, null, 2),
+ )
+ assertErrorParity(errors[0], errors[1])
+ if (callbackFailure) {
+ for (const count of invoked) assert(count > 0)
+ }
+ }
+ return { pairs, userCause: errorEvidence(userCause) }
+ }
+ let result, server
+ const cleanup = async () => {
+ try {
+ try {
+ if (server) await server.close()
+ } finally {
+ try {
+ if (result) await result.close()
+ } finally {
+ closeBuild?.()
+ }
+ }
+ } catch (error) {
+ saveFailure(error)
+ throw error
+ }
+ }
+ try {
+ const rsbuild = await configure({ ...selection, singleCss, atomHoist: 0 })
+ if (scenario === 'dev' || scenario === 'watch') {
+ const first = nextCompile()
+ if (scenario === 'dev') {
+ server = await rsbuild.createDevServer()
+ ;({ port } = await server.listen())
+ generations.push(await first)
+ const cssAssets = Object.keys(generations[0].assets).filter((name) =>
+ name.endsWith('.css'),
+ )
+ for (const name of cssAssets) {
+ const response = await fetch(`http://127.0.0.1:${port}/${name}`, {
+ signal: AbortSignal.timeout(15000),
+ })
+ assert.equal(response.status, 200)
+ assert.equal(await response.text(), generations[0].assets[name])
+ }
+ } else {
+ result = await rsbuild.build({ watch: true })
+ generations.push(await first)
+ }
+ for (const color of ['rebeccapurple', 'teal']) {
+ const source = `import { globalCss, css } from "@devup-ui/react"; globalCss({ body: { backgroundColor: "rebeccapurple" } }); export const layer = css({ borderColor: "${color}" });`
+ const before = compilationProofs.length
+ const revision = nextCompile(source)
+ put(join(directory, 'src/B.css.ts'), source)
+ const generation = await revision
+ generations.push(generation)
+ const suppressed = compilationProofs
+ .slice(before)
+ .filter(
+ (proof) => proof.source === source && proof.needAdditionalPass,
+ )
+ assert.equal(suppressed.length, scenario === 'dev' ? 1 : 0)
+ for (const proof of suppressed) assert(proof.additionalPass)
+ if (scenario === 'dev') {
+ generation.http = {}
+ for (const [name, bytes] of Object.entries(generation.emitted)) {
+ if (!/\.(?:js|css)$/.test(name)) continue
+ const response = await fetch(`http://127.0.0.1:${port}/${name}`, {
+ signal: AbortSignal.timeout(15000),
+ })
+ assert.equal(response.status, 200)
+ generation.http[name] = await response.text()
+ assert.equal(generation.http[name], bytes)
+ }
+ }
+ }
+ } else {
+ result = await rsbuild.build()
+ generations.push(snapshot(result.stats))
+ }
+ assert.equal(owner.watchMode, scenario === 'dev' || scenario === 'watch')
+ } catch (error) {
+ saveFailure(error)
+ throw error
+ } finally {
+ await cleanup()
+ }
+ try {
+ if (hold) {
+ assert(layersReleased)
+ assert(additionalPasses > 0)
+ assert(deleted > 0)
+ }
+ if (scenario === 'late') {
+ create({ engine: selection.engine === 'wasm' ? 'native' : 'wasm' })
+ const before = calls.getCss
+ const css = await callbacks[0]({
+ resourcePath: join(directory, 'df/devup-ui/devup-ui.css'),
+ environment: { name: 'web' },
+ })
+ assert.equal(css, selected.getCss(null, true))
+ assert(calls.getCss > before)
+ }
+ for (const name of [
+ 'registerShorthands',
+ 'registerTheme',
+ 'reserveClassSlots',
+ 'setModuleResolver',
+ 'codeExtract',
+ 'getCss',
+ ])
+ assert(calls[name] > 0, name)
+ const local = []
+ for (const code of [
+ 'export const plain = 1;',
+ readFileSync(sourcePath, 'utf8'),
+ ]) {
+ const dependencies = []
+ const output = await callbacks[1]({
+ code,
+ resourcePath: sourcePath,
+ addDependency: (file) => dependencies.push(file),
+ })
+ local.push({ ...output, dependencies })
+ }
+ assert.equal(owner.watchMode, false)
+ if (failure) throw failure
+ return { generations, local, additionalPasses, deleted, compilationProofs }
+ } catch (error) {
+ saveFailure(error)
+ throw error
+ }
+}
+
+export function prepareBunFixture(directory, customShorthands = true) {
+ for (const name of ['src', 'df', 'dist'])
+ rmSync(join(directory, name), { recursive: true, force: true })
+ put(
+ join(directory, 'devup.json'),
+ '{"theme":{"length":{"default":{"size":"17px"}}}}',
+ )
+ put(join(directory, 'src/tokens.ts'), 'export const color="teal";')
+ for (const name of ['A', 'B'])
+ put(
+ join(directory, `src/${name}.ts`),
+ `import {css} from '@devup-ui/react'; import {color} from './tokens.ts'; export const cls=css({color,w:'$size',${customShorthands ? "insetX:'19px'" : "left:'19px',right:'19px'"},p:${name === 'A' ? 1 : 2}});`,
+ )
+ put(
+ join(directory, 'src/Unused.ts'),
+ 'import {css} from "@devup-ui/react"; export const unused=css({color:"magenta"});',
+ )
+}
+
+async function consumeBun(context) {
+ const {
+ directory,
+ scenario,
+ selection,
+ create,
+ selected,
+ outputs,
+ calls,
+ utils,
+ plugin,
+ } = context
+ assert(process.versions.bun, 'Bun consumers require the real Bun runtime')
+ const Bun = await import('bun')
+ const options = { ...selection, shorthands: { insetX: ['left', 'right'] } }
+ const entries = ['A', 'B'].map((name) => join(directory, `src/${name}.ts`))
+ const callbacks = new Map()
+ const phases = []
+ const local = []
+ const assertShorthands = (classes, css, file) => {
+ assert(classes?.length, file)
+ for (const property of ['left', 'right'])
+ assert(
+ classes.some((name) =>
+ new RegExp(`\\.${name}\\s*\\{\\s*${property}:\\s*19px;?\\s*\\}`).test(
+ css,
+ ),
+ ),
+ `${file}: ${property}`,
+ )
+ }
+ const compilerState = (compiler) => ({
+ css: compiler.getCss(null, false),
+ sheet: compiler.exportSheet(),
+ classes: compiler.exportClassMap(),
+ files: compiler.exportFileMap(),
+ canonical: compiler.exportCanonicalMap(),
+ residual: compiler.getUnreservedClassSlotCount(),
+ prefix: compiler.getPrefix(),
+ debug: compiler.isDebug(),
+ theme: compiler.getDefaultTheme(),
+ })
+ const snapshot = (assets, sources) => {
+ const state = compilerState(selected)
+ assert.equal(state.residual, 0)
+ assert(!state.css.includes('magenta'))
+ for (const [name, source] of Object.entries(sources)) {
+ assert(typeof source === 'string' && source.length > 0, name)
+ assert.doesNotMatch(
+ source,
+ /Cannot run on the runtime|@devup-ui\/(?:wasm|binding)/,
+ )
+ }
+ const phase = {
+ state,
+ assets,
+ sources,
+ outputs: Object.fromEntries(
+ [...outputs].sort(([a], [b]) => a.localeCompare(b)),
+ ),
+ disk: {
+ df: tree(join(directory, 'df')),
+ dist:
+ scenario === 'runtime' || scenario === 'preload'
+ ? {}
+ : tree(join(directory, 'dist')),
+ },
+ calls: { ...calls },
+ }
+ phases.push(phase)
+ return phase
+ }
+ try {
+ if (scenario === 'runtime' || scenario === 'preload') {
+ if (scenario === 'runtime') await plugin.register(options)
+ const sources = {}
+ for (const file of entries) {
+ const module = await import(pathToFileURL(file).href)
+ assert(typeof module.cls === 'string' && module.cls.length > 0)
+ assertShorthands(
+ module.cls.split(/\s+/),
+ selected.getCss(null, false),
+ file,
+ )
+ sources[file] = module.cls
+ }
+ const phase = snapshot({}, sources)
+ assert.equal(
+ readFileSync(join(directory, 'df/devup-ui/devup-ui.css'), 'utf8'),
+ phase.state.css,
+ )
+ return { phases, local }
+ }
+ for (let round = 0; round < (scenario === 'repeat' ? 2 : 1); round++) {
+ outputs.clear()
+ for (const name of Object.keys(calls)) calls[name] = 0
+ const pending = new Map()
+ const sources = {}
+ const first = entries[scenario === 'opposite' ? 1 : 0]
+ const second = entries.find((file) => file !== first)
+ const genuine = create(options)
+ const observed = {
+ ...genuine,
+ setup(builder) {
+ return genuine.setup(
+ new Proxy(builder, {
+ get(target, key) {
+ if (key === 'onLoad')
+ return (filter, callback) => {
+ callbacks.set(filter.namespace ?? 'file', callback)
+ return target.onLoad(filter, async (args) => {
+ if (filter.namespace || !entries.includes(args.path))
+ return callback(args)
+ await new Promise((release) => {
+ assert(!pending.has(args.path))
+ pending.set(args.path, release)
+ if (pending.size === 2) pending.get(first)()
+ })
+ try {
+ const result = await callback(args)
+ assert(result && typeof result.contents === 'string')
+ sources[args.path] = result.contents
+ return result
+ } finally {
+ if (args.path === first) pending.get(second)()
+ }
+ })
+ }
+ const value = Reflect.get(target, key, target)
+ return typeof value === 'function' ? value.bind(target) : value
+ },
+ }),
+ )
+ },
+ }
+ const result = await Bun.build({
+ root: directory,
+ entrypoints: entries,
+ target: 'browser',
+ format: 'esm',
+ sourcemap: 'external',
+ external: ['react', 'react/jsx-runtime'],
+ outdir: join(directory, 'dist'),
+ plugins: [observed],
+ })
+ assert(result.success, result.logs.map(String).join('\n'))
+ assert.deepEqual(result.logs, [])
+ assert.equal(Object.keys(sources).length, 2)
+ const assets = {}
+ for (const output of result.outputs)
+ assets[relative(directory, output.path)] = await output.text()
+ const phase = snapshot(assets, sources)
+ const css = Object.entries(assets)
+ .filter(([name]) => name.endsWith('.css'))
+ .map(([, text]) => text)
+ .join('\n')
+ assert(css.includes('19px') && !css.includes('magenta'))
+ for (const file of entries) {
+ const classes = /\bcls\s*=\s*["']([^"']+)["']/
+ .exec(sources[file])?.[1]
+ .split(/\s+/)
+ .filter(Boolean)
+ assertShorthands(classes, css, file)
+ }
+ if (round) assertRawParity(phases[0], phase)
+ }
+ if (scenario === 'late' || scenario === 'errors') {
+ const callback = callbacks.get('file')
+ assert(callback)
+ const args = {
+ path: entries[0],
+ namespace: 'file',
+ loader: 'file',
+ defer: async () => {},
+ }
+ if (scenario === 'late') {
+ const other = utils.loadCompiler(
+ selection.engine === 'wasm' ? 'native' : 'wasm',
+ )
+ const before = compilerState(other)
+ create({ engine: selection.engine === 'wasm' ? 'native' : 'wasm' })
+ await callback(args)
+ const deferred = [...callbacks].find(
+ ([namespace]) => namespace !== 'file',
+ )?.[1]
+ assert(deferred)
+ assert.equal(
+ (await deferred(args)).contents,
+ selected.getCss(null, false),
+ )
+ const after = compilerState(other)
+ local.push({ unselected: { before, after } })
+ assertRawParity(before, after)
+ snapshot(phases[0].assets, phases[0].sources)
+ } else {
+ const originalSource = readFileSync(args.path, 'utf8')
+ try {
+ put(
+ args.path,
+ 'import {css} from "@devup-ui/react"; export const cls=css({color:window.name});',
+ )
+ let parseError
+ try {
+ await callback(args)
+ } catch (error) {
+ parseError = error
+ }
+ local.push({ parseError: errorEvidence(parseError) })
+ assert(parseError instanceof Error)
+ } finally {
+ put(args.path, originalSource)
+ }
+ const cause = new Error('compiler callback cause')
+ const failure = new Error('compiler callback failure', { cause })
+ const extract = selected.codeExtract
+ try {
+ selected.codeExtract = () => {
+ throw failure
+ }
+ await assert.rejects(
+ () => callback(args),
+ (error) => error === failure && error.cause === cause,
+ )
+ local.push({
+ error: errorEvidence(failure),
+ thrownIdentity: true,
+ causeIdentity: true,
+ })
+ } finally {
+ selected.codeExtract = extract
+ }
+ }
+ }
+ return { phases, local }
+ } finally {
+ Bun.plugin.clearAll()
+ }
+}
+
+async function consumeNextLoaders({
+ directory,
+ format,
+ selection,
+ singleCss,
+ selected,
+ utils,
+ outputs,
+ calls,
+ observations,
+}) {
+ const require = createRequire(join(directory, 'package.json'))
+ const modules = []
+ for (const name of ['loader', 'css-loader']) {
+ const specifier = `@devup-ui/next-plugin/${name}`
+ const entry =
+ format === 'require'
+ ? require.resolve(specifier)
+ : fileURLToPath(import.meta.resolve(specifier))
+ const root = dirname(dirname(realpathSync(entry)))
+ const manifest = JSON.parse(
+ readFileSync(join(root, 'package.json'), 'utf8'),
+ )
+ assert.equal(
+ realpathSync(entry),
+ realpathSync(join(root, manifest.exports[`./${name}`][format])),
+ )
+ modules.push(
+ format === 'require' ? require(specifier) : await import(specifier),
+ )
+ }
+ const [sourceModule, cssModule] = modules
+ const sourceLoader = sourceModule.default ?? sourceModule
+ const cssLoader = cssModule.default ?? cssModule
+ const other = utils.loadCompiler(
+ selection.engine === 'wasm' ? 'native' : 'wasm',
+ )
+ other.resetStateForTesting()
+ const state = (compiler) => ({
+ css: compiler.getCss(null, false),
+ sheet: compiler.exportSheet(),
+ classes: compiler.exportClassMap(),
+ files: compiler.exportFileMap(),
+ canonical: compiler.exportCanonicalMap(),
+ residual: compiler.getUnreservedClassSlotCount(),
+ prefix: compiler.getPrefix(),
+ debug: compiler.isDebug(),
+ theme: compiler.getDefaultTheme(),
+ })
+ const unselectedBefore = state(other)
+ selected.registerShorthands({ insetX: ['left', 'right'] })
+ const cssDir = join(directory, 'direct-css')
+ mkdirSync(cssDir, { recursive: true })
+ put(join(directory, 'src/tokens.js'), "export const tone = 'teal';")
+ const resourcePath = join(directory, 'src/direct.jsx')
+ const source =
+ "import {css} from '@devup-ui/react'; import {tone} from './tokens.js'; export const cls=css({color:tone,insetX:'19px',w:'$size',p:'1px'});"
+ put(resourcePath, source)
+ const options = {
+ ...selection,
+ package: '@devup-ui/react',
+ cssDir,
+ singleCss,
+ watch: false,
+ sheetFile: join(cssDir, 'sheet.json'),
+ classMapFile: join(cssDir, 'classMap.json'),
+ fileMapFile: join(cssDir, 'fileMap.json'),
+ themeFile: join(directory, 'direct-devup.json'),
+ defaultSheet: JSON.parse(selected.exportSheet()),
+ defaultClassMap: {},
+ defaultFileMap: {},
+ theme: { length: { default: { size: ['17px'] } } },
+ }
+ const dependencies = []
+ const sourceResult = await new Promise((resolve, reject) => {
+ sourceLoader.call(
+ {
+ resourcePath,
+ getOptions: () => options,
+ addDependency: (path) => dependencies.push(path),
+ async: () => (error, code, map) =>
+ error ? reject(error) : resolve({ code, map }),
+ },
+ Buffer.from(source),
+ )
+ // The asynchronous callback must retain the compiler selected on entry.
+ options.engine = selection.engine === 'wasm' ? 'native' : 'wasm'
+ })
+ options.engine = selection.engine
+ assert.doesNotMatch(sourceResult.code, /Cannot run on the runtime/)
+ assert(sourceResult.map && JSON.parse(sourceResult.map).version === 3)
+ assert(dependencies.includes(join(directory, 'src/tokens.js')))
+ const extraction = outputs.get('src/direct.jsx')
+ assert(extraction)
+ assert.equal(sourceResult.code, extraction.code)
+ assert.equal(sourceResult.map, extraction.map)
+ const fileNum = JSON.parse(selected.exportFileMap())['src/direct.jsx']
+ // Global single-CSS extraction need not allocate a per-file bucket number.
+ if (!singleCss) assert(Number.isInteger(fileNum))
+ assert.equal(typeof extraction.cssFile, 'string')
+ const cssPath = resolve(dirname(resourcePath), extraction.cssFile)
+ assert.equal(
+ cssPath,
+ join(cssDir, singleCss ? 'devup-ui.css' : `devup-ui-${fileNum}.css`),
+ )
+ const requestedFileNum = utils.getFileNumByFilename(cssPath)
+ assert.equal(requestedFileNum, singleCss ? null : fileNum)
+ const liveCss = selected.getCss(requestedFileNum, true)
+ const mainCss = selected.getCss(null, false)
+ for (const rule of [
+ 'color:teal',
+ 'left:19px',
+ 'right:19px',
+ '17px',
+ 'padding:1px',
+ ])
+ assert(
+ `${mainCss}\n${liveCss}`.includes(rule),
+ `Missing actual fallback CSS ${rule}`,
+ )
+ const cssCalls = []
+ const cssMap = { version: 3, sources: [], names: [], mappings: '' }
+ const meta = { token: 'next-production-map-meta' }
+ const productionSource = Buffer.from(liveCss)
+ const invokeCss = (watch, callback, source = productionSource) =>
+ cssLoader.call(
+ {
+ resourcePath: cssPath,
+ getOptions: () => ({ ...options, watch }),
+ callback,
+ },
+ source,
+ cssMap,
+ meta,
+ )
+ invokeCss(true, (error, css) => {
+ assert.equal(error, null)
+ assert.equal(css, liveCss)
+ cssCalls.push({ css })
+ })
+ invokeCss(false, (error, css, map, value) => {
+ assert.equal(error, null)
+ assert.equal(css, productionSource)
+ assert.equal(map, cssMap)
+ assert.equal(value, meta)
+ cssCalls.push({ css: css.toString(), map, meta: value })
+ })
+ const local = []
+ const cause = new Error('Next fallback compiler cause')
+ const failure = new Error('Next fallback compiler failure', { cause })
+ assert([undefined, 'authored-errors'].includes(process.argv[8]))
+ const authoredErrors = process.argv[8] === 'authored-errors'
+ if (authoredErrors) {
+ // Author the supplied fixture, never a projection of a received error.
+ // Ordinary realm-created Errors remain covered by the original 40 cells.
+ for (const error of [cause, failure]) {
+ for (const key of Reflect.ownKeys(error))
+ assert(['message', 'cause', ...runtimeErrorKeys].includes(key))
+ for (const key of runtimeErrorKeys) delete error[key]
+ Object.defineProperties(error, {
+ stack: {
+ value: `${error.name}: ${error.message}\n at nextLoaderFixture (next-loader-fixture:1:1)`,
+ configurable: true,
+ writable: true,
+ },
+ code: {
+ value: 'NEXT_SUPPLIED_ERROR',
+ configurable: true,
+ enumerable: true,
+ },
+ detail: {
+ value: { filename: 'src/direct.jsx' },
+ configurable: true,
+ enumerable: true,
+ },
+ })
+ assert(error instanceof Error)
+ assert.equal(error.constructor, Error)
+ assert.equal(Object.getPrototypeOf(error), Error.prototype)
+ }
+ Object.defineProperty(failure, 'causeAlias', {
+ value: cause,
+ enumerable: true,
+ })
+ }
+ // Materialize runtime-owned lazy stack fields before the immutable baseline.
+ errorEvidence(failure)
+ const inputError = errorEvidence(failure),
+ inputCause = errorEvidence(cause)
+ const unchangedError = (after) => assert.deepEqual(after, inputError)
+ const errorControls = []
+ if (authoredErrors) {
+ const property = (node, key) =>
+ node.properties.find((entry) => entry.key.value === key)
+ for (const [name, mutate] of [
+ [
+ 'constructor',
+ (node) => {
+ node.constructor.value += '-changed'
+ },
+ ],
+ [
+ 'prototype',
+ (node) => {
+ node.prototypes[0].value += '-changed'
+ },
+ ],
+ [
+ 'message',
+ (node) => {
+ property(node, 'message').value.value += '-changed'
+ },
+ ],
+ [
+ 'cause',
+ (node) => {
+ property(property(node, 'cause').value, 'message').value.value +=
+ '-changed'
+ },
+ ],
+ [
+ 'unknown own property',
+ (node) => {
+ node.properties.push({
+ ...structuredClone(property(node, 'code')),
+ key: { type: 'string', value: 'unknown' },
+ })
+ },
+ ],
+ [
+ 'authored descriptor',
+ (node) => {
+ property(node, 'code').writable = true
+ },
+ ],
+ [
+ 'cause alias',
+ (node) => {
+ property(node, 'causeAlias').value.reference = 999
+ },
+ ],
+ [
+ 'authored raw stack',
+ (node) => {
+ property(node, 'stack').value.value += '-changed'
+ },
+ ],
+ ]) {
+ const changed = structuredClone(inputError)
+ mutate(changed.raw)
+ assert.throws(() => unchangedError(changed), undefined, name)
+ if (name !== 'authored raw stack')
+ assert.throws(
+ () => assertRawParity(inputError, changed),
+ undefined,
+ name,
+ )
+ errorControls.push(name)
+ }
+ }
+ for (const name of ['codeExtract', 'getCss']) {
+ const original = selected[name]
+ let received
+ try {
+ selected[name] = () => {
+ throw failure
+ }
+ if (name === 'codeExtract') {
+ received = await new Promise((resolve) =>
+ sourceLoader.call(
+ {
+ resourcePath,
+ getOptions: () => options,
+ addDependency() {},
+ async: () => resolve,
+ },
+ Buffer.from(source),
+ ),
+ )
+ } else
+ invokeCss(true, (error) => {
+ received = error
+ })
+ assert.equal(received, failure)
+ assert.equal(received.cause, cause)
+ const error = errorEvidence(received)
+ unchangedError(error)
+ assert.deepEqual(errorEvidence(received.cause), inputCause)
+ local.push({
+ method: name,
+ inputError,
+ inputCause,
+ error,
+ thrownIdentity: true,
+ causeIdentity: true,
+ })
+ } finally {
+ selected[name] = original
+ }
+ }
+ let callbackCount = 0
+ assert.throws(
+ () =>
+ invokeCss(false, () => {
+ callbackCount++
+ throw failure
+ }),
+ (error) => error === failure,
+ )
+ assert.equal(callbackCount, 1)
+ const selectedState = state(selected)
+ assert.equal(selectedState.residual, 0)
+ assert(
+ selectedState.css !== unselectedBefore.css ||
+ selectedState.sheet !== unselectedBefore.sheet,
+ )
+ const unselectedAfter = state(other)
+ assert.deepEqual(unselectedAfter, unselectedBefore)
+ assert(calls.codeExtract > 0 && calls.getCss > 0)
+ return {
+ sourceResult,
+ dependencies,
+ mainCss,
+ liveCss,
+ cssCalls,
+ local,
+ errorFixture: authoredErrors ? 'authored' : 'ordinary',
+ errorControls,
+ callbackCount,
+ state: selectedState,
+ unselectedBefore,
+ unselectedAfter,
+ outputs: Object.fromEntries(outputs),
+ observations,
+ calls,
+ disk: tree(cssDir),
+ }
+}
+
+let nextCompilerCapture
+
+export async function captureNextCoordinator({
+ directory,
+ mode,
+ singleCss,
+ destination,
+}) {
+ const require = createRequire(join(directory, 'package.json'))
+ const fromNext = createRequire(
+ realpathSync(require.resolve('@devup-ui/next-plugin')),
+ )
+ const utils = fromNext('@devup-ui/plugin-utils')
+ const compiler = utils.loadCompiler(mode === 'default' ? undefined : mode)
+ const portFile = readFileSync(join(directory, 'df/coordinator.port'), 'utf8')
+ const [port, pid] = portFile.trim().split(/\r?\n/).map(Number)
+ assert(Number.isInteger(port) && port > 0 && port < 65536)
+ assert.equal(pid, process.pid)
+ assert.equal(typeof singleCss, 'boolean')
+ assert.equal(
+ nextCompilerCapture?.selected,
+ compiler,
+ 'HTTP witness must use the same physical compiler as the actual CLI capture',
+ )
+ const resources = ['src/A.jsx', 'src/B.jsx'].map((file) => {
+ const record = nextCompilerCapture.outputs.findLast(
+ ({ input }) => input[0] === file,
+ )
+ assert(record, `Missing actual CLI extraction for ${file}`)
+ assert.equal(typeof record.output.cssFile, 'string')
+ const resourcePath = resolve(
+ dirname(join(directory, file)),
+ record.output.cssFile,
+ )
+ assert.equal(dirname(resourcePath), join(directory, 'df/devup-ui'))
+ const fileNum = utils.getFileNumByFilename(resourcePath)
+ if (singleCss) {
+ assert.equal(fileNum, null)
+ assert.equal(resourcePath, join(directory, 'df/devup-ui/devup-ui.css'))
+ } else {
+ assert(Number.isInteger(fileNum) && fileNum >= 0)
+ assert.equal(
+ fileNum,
+ JSON.parse(compiler.exportFileMap())['src/app/page.jsx'],
+ )
+ }
+ return { file, cssFile: record.output.cssFile, resourcePath, fileNum }
+ })
+ assert.equal(resources[0].resourcePath, resources[1].resourcePath)
+ const fileNum = resources[0].fileNum
+ const requests = []
+ for (const [number, importMainCss] of [
+ [null, false],
+ [fileNum, true],
+ ]) {
+ const query = new URLSearchParams({
+ importMainCss: String(importMainCss),
+ waitForIdle: 'true',
+ })
+ if (number !== null) query.set('fileNum', String(number))
+ const url = `http://127.0.0.1:${port}/css?${query}`
+ const response = await fetch(url, { signal: AbortSignal.timeout(90000) })
+ const body = Buffer.from(await response.arrayBuffer())
+ const liveCss = compiler.getCss(number, importMainCss)
+ assert.equal(response.status, 200)
+ assert.equal(response.headers.get('content-type'), 'text/css')
+ assert.deepEqual(body, Buffer.from(liveCss))
+ requests.push({
+ fileNum: number,
+ importMainCss,
+ url,
+ status: response.status,
+ headers: Object.fromEntries(response.headers),
+ body: body.toString('base64'),
+ liveCss,
+ })
+ }
+ writeFileSync(
+ destination,
+ JSON.stringify({ portFile, pid, singleCss, resources, requests }, null, 2),
+ { flag: 'wx' },
+ )
+}
+
+export async function captureNextCompiler({ directory, mode, destination }) {
+ const require = createRequire(join(directory, 'package.json'))
+ if (
+ !require('node:worker_threads').isMainThread ||
+ !/[\\/]next[\\/]dist[\\/]bin[\\/]next(?:\.js)?$/.test(process.argv[1] ?? '')
+ )
+ return
+ const nextEntry = realpathSync(require.resolve('@devup-ui/next-plugin'))
+ const nextRequire = createRequire(nextEntry)
+ const webpackEntry = realpathSync(
+ nextRequire.resolve('@devup-ui/webpack-plugin'),
+ )
+ const webpackRequire = createRequire(webpackEntry)
+ const nextUtils = realpathSync(nextRequire.resolve('@devup-ui/plugin-utils'))
+ const webpackUtils = realpathSync(
+ webpackRequire.resolve('@devup-ui/plugin-utils'),
+ )
+ const fromUtils = createRequire(nextUtils)
+ const anchors = {
+ nextEntry,
+ webpackEntry,
+ nextUtils,
+ webpackUtils,
+ bindingEntry: realpathSync(fromUtils.resolve('@devup-ui/binding')),
+ wasmEntry: realpathSync(fromUtils.resolve('@devup-ui/wasm')),
+ }
+ writeFileSync(`${destination}.anchors.json`, JSON.stringify(anchors, null, 2))
+ assert.equal(
+ nextUtils,
+ webpackUtils,
+ 'Capture must wrap the delegated compiler instance',
+ )
+ const utils = nextRequire('@devup-ui/plugin-utils')
+ const selected = utils.loadCompiler(mode === 'default' ? undefined : mode)
+ const other = utils.loadCompiler(mode === 'wasm' ? 'native' : 'wasm')
+ selected.resetStateForTesting()
+ other.resetStateForTesting()
+ const state = (compiler) => ({
+ css: compiler.getCss(null, false),
+ sheet: compiler.exportSheet(),
+ classes: compiler.exportClassMap(),
+ files: compiler.exportFileMap(),
+ canonical: compiler.exportCanonicalMap(),
+ residual: compiler.getUnreservedClassSlotCount(),
+ prefix: compiler.getPrefix(),
+ debug: compiler.isDebug(),
+ theme: compiler.getDefaultTheme(),
+ })
+ const unselectedBefore = state(other),
+ calls = {},
+ outputs = []
+ nextCompilerCapture = { selected, outputs }
+ for (const name of [
+ 'registerShorthands',
+ 'registerTheme',
+ 'setModuleResolver',
+ 'reserveClassSlots',
+ 'codeExtract',
+ 'codeExtractWithoutSourceMap',
+ 'getCss',
+ 'exportSheet',
+ 'exportClassMap',
+ 'exportFileMap',
+ 'importCanonicalMap',
+ 'importFileRoutes',
+ 'setPrefix',
+ 'setDebug',
+ 'setAtomHoist',
+ ]) {
+ const original = selected[name]
+ calls[name] = 0
+ selected[name] = (...args) => {
+ calls[name]++
+ const result = original(...args)
+ if (name.startsWith('codeExtract'))
+ outputs.push({
+ input: args.slice(0, 2),
+ output: {
+ code: result.code,
+ css: result.css,
+ cssFile: result.cssFile,
+ map: result.map,
+ dependencies: result.dependencies,
+ updatedBaseStyle: result.updatedBaseStyle,
+ },
+ })
+ return result
+ }
+ }
+ process.once('exit', (exitCode) => {
+ const compiler = {
+ anchors,
+ state: state(selected),
+ fileCss: Object.fromEntries(
+ Object.values(JSON.parse(selected.exportFileMap())).map((number) => [
+ number,
+ selected.getCss(number, true),
+ ]),
+ ),
+ outputs,
+ calls,
+ unselectedBefore,
+ unselectedAfter: state(other),
+ pid: process.pid,
+ exitCode,
+ }
+ writeFileSync(
+ destination,
+ JSON.stringify(
+ compiler,
+ (_, value) => (value === undefined ? { undefined: true } : value),
+ 2,
+ ),
+ )
+ })
+}
+
+export async function consume({
+ directory,
+ scenario,
+ mode,
+ format,
+ singleCss,
+ destination,
+ adapter = 'webpack',
+}) {
+ const require = createRequire(join(directory, 'package.json'))
+ const selection = mode === 'default' ? {} : { engine: mode }
+ const resolvedEngine = mode === 'wasm' ? 'wasm' : 'native'
+ const packageName = `@devup-ui/${adapter}-plugin${adapter === 'bun' ? '/register' : ''}`
+ const entry =
+ format === 'require'
+ ? require.resolve(packageName)
+ : fileURLToPath(import.meta.resolve(packageName))
+ const physical =
+ adapter === 'vite' || adapter === 'bun' ? viteRealpath : realpathSync
+ const utilsRequire = createRequire(
+ adapter === 'vite' ? physical(entry) : entry,
+ )
+ const utils = utilsRequire('@devup-ui/plugin-utils')
+ const utilsRoot = dirname(
+ dirname(utilsRequire.resolve('@devup-ui/plugin-utils')),
+ )
+ const utilsManifest = JSON.parse(
+ readFileSync(join(utilsRoot, 'package.json'), 'utf8'),
+ )
+ const fromEsm = await import(
+ pathToFileURL(join(utilsRoot, utilsManifest.exports['.'].import)).href
+ )
+ const entryUtils = format === 'import' ? fromEsm : utils
+ const pluginRoot = physical(dirname(dirname(entry)))
+ const assertDist =
+ adapter === 'bun'
+ ? assertBunDist
+ : adapter === 'rsbuild' || adapter === 'vite'
+ ? assertRsbuildDist
+ : assertWebpackDist
+ assertDist(join(pluginRoot, 'dist'))
+ const manifest = JSON.parse(
+ readFileSync(join(pluginRoot, 'package.json'), 'utf8'),
+ )
+ assert.equal(
+ physical(entry),
+ physical(
+ join(
+ pluginRoot,
+ manifest.exports[adapter === 'bun' ? './register' : '.'][format],
+ ),
+ ),
+ )
+ assert(
+ !Object.values(manifest.dependencies).some((version) =>
+ version.startsWith('workspace:'),
+ ),
+ )
+ const load = Module._resolveFilename
+ const denied = new Error('unselected-backend-resolution')
+ let nativeFailure = false
+ let nextGuardWasmResolutions = 0
+ Module._resolveFilename = function (request, ...args) {
+ if (
+ nativeFailure &&
+ adapter === 'next' &&
+ (request === '@devup-ui/wasm' || request.startsWith('@devup-ui/wasm/'))
+ ) {
+ nextGuardWasmResolutions++
+ throw denied
+ }
+ if (scenario !== 'late' && scenario !== 'errors' && adapter !== 'next') {
+ const blocked =
+ resolvedEngine === 'wasm' ? '@devup-ui/binding' : '@devup-ui/wasm'
+ if (request === blocked || request.startsWith(`${blocked}/`)) {
+ throw denied
+ }
+ }
+ if (
+ nativeFailure &&
+ (request === '@devup-ui/binding' ||
+ request.startsWith('@devup-ui/binding-'))
+ ) {
+ throw denied
+ }
+ return load.call(this, request, ...args)
+ }
+ const previousCwd = process.cwd()
+ const now = Date.now
+ const originals = new Map()
+ let compiler
+ let watching
+ try {
+ process.chdir(directory)
+ if (adapter === 'webpack') Date.now = () => 1700000000000
+ const plugin =
+ format === 'require' ? require(packageName) : await import(packageName)
+ const create = (options) =>
+ adapter === 'next'
+ ? plugin.DevupUI({}, options)
+ : adapter === 'rsbuild' || adapter === 'vite' || adapter === 'bun'
+ ? plugin.DevupUI(options)
+ : new plugin.DevupUIWebpackPlugin(options)
+ if (scenario === 'errors' && adapter === 'webpack') {
+ const sourceModule =
+ format === 'import'
+ ? await import('@devup-ui/webpack-plugin/loader')
+ : require('@devup-ui/webpack-plugin/loader')
+ const sourceLoader = sourceModule.default ?? sourceModule
+ const userCause = Object.assign(
+ new Error('user resolver callback failure', {
+ cause: new Error('nested user resolver cause'),
+ }),
+ { code: 'user-resolver-code' },
+ )
+ Object.defineProperty(userCause, 'hidden', {
+ value: 'user resolver detail',
+ })
+ const pairs = []
+ const live = []
+ for (const callbackFailure of [false, true]) {
+ const received = []
+ const invoked = []
+ for (const engine of ['native', 'wasm']) {
+ const selected = utils.loadCompiler(engine)
+ selected.resetStateForTesting()
+ const setResolver = selected.setModuleResolver
+ let callbackCalls = 0
+ try {
+ if (callbackFailure) {
+ selected.setModuleResolver = () =>
+ setResolver(() => {
+ callbackCalls++
+ throw userCause
+ })
+ }
+ const error = await new Promise((resolve) => {
+ sourceLoader.call(
+ {
+ resourcePath: join(directory, 'src/error.tsx'),
+ getOptions: () => ({
+ engine,
+ package: '@devup-ui/react',
+ cssDir: join(directory, 'df/devup-ui'),
+ singleCss,
+ watch: false,
+ }),
+ addDependency() {},
+ async: () => (error) => resolve(error),
+ },
+ Buffer.from(
+ callbackFailure
+ ? 'import { css } from "@devup-ui/react"; import { color } from "./throws.js"; export const x = css({ color });'
+ : 'import { Box } from "@devup-ui/react"; export const x = ;',
+ ),
+ )
+ })
+ received.push(error)
+ invoked.push(callbackCalls)
+ } finally {
+ selected.setModuleResolver = setResolver
+ selected.resetStateForTesting()
+ }
+ }
+ pairs.push({
+ callbackFailure,
+ invoked,
+ native: errorEvidence(received[0]),
+ wasm: errorEvidence(received[1]),
+ nativeThrownIdentity: received[0] === userCause,
+ wasmThrownIdentity: received[1] === userCause,
+ nativeCauseIdentity: received[0]?.cause === userCause,
+ wasmCauseIdentity: received[1]?.cause === userCause,
+ })
+ live.push({ callbackFailure, invoked, received })
+ }
+ const evidence = { pairs, userCause: errorEvidence(userCause) }
+ // Save all fields before a native-only property or lost-cause assertion.
+ put(destination, JSON.stringify(evidence, null, 2))
+ for (const { callbackFailure, invoked, received } of live) {
+ for (const error of received) assert(error instanceof Error)
+ assertErrorParity(received[0], received[1])
+ if (callbackFailure) {
+ for (const calls of invoked) assert(calls > 0)
+ for (const error of received) {
+ assert.notEqual(error, userCause)
+ assert.equal(Object.hasOwn(error, 'cause'), false)
+ assert.equal(Object.hasOwn(error, 'code'), false)
+ assert.equal(Object.hasOwn(error, 'hidden'), false)
+ assert.match(error.message, /user resolver callback failure/)
+ }
+ }
+ }
+ return evidence
+ }
+ if (scenario === 'guards' && adapter === 'next') {
+ nativeFailure = true
+ const sourceModule =
+ format === 'require'
+ ? require('@devup-ui/next-plugin/loader')
+ : await import('@devup-ui/next-plugin/loader')
+ const loader = sourceModule.default ?? sourceModule
+ const diagnostics = []
+ for (const engine of ['invalid', undefined, 'native']) {
+ const error = await new Promise((resolve) =>
+ loader.call(
+ {
+ resourcePath: join(directory, 'src/A.jsx'),
+ getOptions: () => ({ engine }),
+ async: () => resolve,
+ },
+ Buffer.from('export const x = 1;'),
+ ),
+ )
+ diagnostics.push(errorEvidence(error))
+ put(destination, JSON.stringify({ diagnostics }, null, 2))
+ if (engine === 'invalid') assert.equal(error.name, 'InvalidEngineError')
+ else {
+ assert.equal(error.name, 'NativeCompilerLoadError')
+ assert.equal(error.cause, denied)
+ assert.equal(
+ error.platformPackage,
+ JSON.parse(
+ readFileSync(join(directory, '..', 'provenance.json'), 'utf8'),
+ ).host.name,
+ )
+ }
+ }
+ assert.equal(
+ nextGuardWasmResolutions,
+ 0,
+ 'Native failure must never attempt WASM fallback',
+ )
+ return { guards: true, diagnostics, denied: errorEvidence(denied) }
+ }
+ if (scenario === 'guards') {
+ nativeFailure = true
+ const diagnostics = []
+ const capture = (action) => {
+ try {
+ action()
+ } catch (error) {
+ diagnostics.push(errorEvidence(error))
+ put(destination, JSON.stringify({ diagnostics }, null, 2))
+ throw error
+ }
+ }
+ assert.throws(
+ () =>
+ capture(() =>
+ create({
+ shorthands: { bad: ['backgroundColour'] },
+ }),
+ ),
+ entryUtils.ShorthandConfigError,
+ )
+ assert.throws(
+ () =>
+ capture(() =>
+ create({
+ engine: 'invalid',
+ }),
+ ),
+ { name: 'InvalidEngineError' },
+ )
+ assert.throws(
+ () =>
+ capture(() =>
+ create({
+ engine: 'native',
+ }),
+ ),
+ (error) => {
+ assert.equal(error.name, 'NativeCompilerLoadError')
+ assert.equal(error.cause, denied)
+ assert.match(error.platformPackage, /^@devup-ui\/binding-/)
+ assert(error.message.includes(error.platformPackage))
+ assert(error.message.includes("engine: 'wasm'"))
+ return true
+ },
+ )
+ const instance = create({ engine: 'wasm' })
+ if (adapter === 'vite') await instance[0].closeBundle()
+ return { guards: true, diagnostics, denied: errorEvidence(denied) }
+ }
+ if (adapter === 'vite' && scenario === 'errors')
+ return await viteErrors({
+ directory,
+ create,
+ singleCss,
+ utils,
+ destination,
+ })
+ const selected = utils.loadCompiler(selection.engine)
+ assert.equal(fromEsm.loadCompiler(selection.engine), selected)
+ if (adapter !== 'bun' || scenario !== 'preload')
+ selected.resetStateForTesting()
+ const calls = {}
+ const outputs = new Map()
+ const errors = []
+ const observations = []
+ for (const name of [
+ 'registerShorthands',
+ 'registerTheme',
+ 'setModuleResolver',
+ 'reserveClassSlots',
+ 'codeExtract',
+ 'getCss',
+ 'exportSheet',
+ 'exportClassMap',
+ 'exportFileMap',
+ ...(adapter === 'vite' || adapter === 'next'
+ ? [
+ 'exportCanonicalMap',
+ 'seedFileMap',
+ 'setDebug',
+ 'setPrefix',
+ 'importCanonicalMap',
+ 'importFileRoutes',
+ 'setAtomHoist',
+ ]
+ : []),
+ ]) {
+ const method = selected[name]
+ originals.set(name, method)
+ calls[name] = 0
+ selected[name] = (...args) => {
+ calls[name]++
+ const scratch =
+ adapter === 'vite' && name === 'reserveClassSlots'
+ ? selected.exportSheet()
+ : undefined
+ const result = method(...args)
+ if (adapter === 'vite' && name === 'reserveClassSlots') {
+ observations.push({
+ reservation: args,
+ sheetBefore: scratch,
+ sheetAfter: selected.exportSheet(),
+ })
+ assert.equal(selected.exportSheet(), scratch)
+ }
+ if (name === 'codeExtract') {
+ outputs.set(args[0], {
+ code: result.code,
+ css: result.css,
+ cssFile: result.cssFile,
+ map: result.map,
+ dependencies: result.dependencies,
+ updatedBaseStyle: result.updatedBaseStyle,
+ })
+ if (adapter === 'vite' || adapter === 'next')
+ observations.push({
+ input: args.slice(0, 2),
+ output: outputs.get(args[0]),
+ })
+ }
+ return result
+ }
+ }
+ if (adapter === 'next')
+ return await consumeNextLoaders({
+ directory,
+ format,
+ selection,
+ singleCss,
+ selected,
+ utils,
+ outputs,
+ calls,
+ observations,
+ })
+ if (adapter === 'vite')
+ return await consumeVite({
+ directory,
+ scenario,
+ selection,
+ singleCss,
+ destination,
+ create,
+ selected,
+ outputs,
+ calls,
+ utils,
+ observations,
+ })
+ if (adapter === 'bun')
+ return await consumeBun({
+ directory,
+ scenario,
+ selection,
+ create,
+ selected,
+ outputs,
+ calls,
+ utils,
+ plugin,
+ })
+ rmSync(join(directory, 'df'), { recursive: true, force: true })
+ rmSync(join(directory, 'dist'), { recursive: true, force: true })
+ put(
+ join(directory, 'devup.json'),
+ JSON.stringify({
+ theme: { colors: { default: { primary: '#17395b' } } },
+ }),
+ )
+ put(join(directory, 'src/tokens.js'), 'export const color = "maroon";')
+ put(
+ join(directory, 'src/base.css.ts'),
+ 'import { css } from "@devup-ui/react"; export const base = css({ color: "navy" });',
+ )
+ const layer = (color) =>
+ `import { globalCss, css } from '@devup-ui/react'; globalCss({ body: { backgroundColor: '${color}' } }); export const layer = css({ borderColor: '${color}' });`
+ put(join(directory, 'src/A.css.ts'), layer('cyan'))
+ put(join(directory, 'src/B.css.ts'), layer('goldenrod'))
+ put(
+ join(directory, 'src/entry.js'),
+ 'import { css, keyframes } from "@devup-ui/react"; import { color } from "./tokens.js"; import { base } from "./base.css.ts"; import { layer as a } from "./A.css.ts"; import { layer as b } from "./B.css.ts"; const spin = keyframes({ from: { opacity: 0 }, to: { opacity: 1 } }); export const cls = css({ color, p: [1, null, 3], animation: `${spin} 1s linear` }); console.log(cls, base, a, b);',
+ )
+ if (adapter === 'rsbuild') {
+ return await consumeRsbuild({
+ directory,
+ scenario,
+ selection,
+ singleCss,
+ destination,
+ require,
+ create,
+ selected,
+ outputs,
+ calls,
+ utils,
+ })
+ }
+ const { webpack } = require('webpack')
+ const gateFile = join(directory, 'hold.cjs')
+ put(gateFile, initialLayerGate)
+ const watch = scenario === 'watch'
+ const holdLayers = watch || scenario === 'additional'
+ const instance = new plugin.DevupUIWebpackPlugin({
+ ...selection,
+ watch,
+ singleCss,
+ atomHoist: 0,
+ ...(scenario === 'additional' ? { productionPrewarmFiles: [] } : {}),
+ })
+ compiler = webpack({
+ mode: 'production',
+ context: directory,
+ cache: false,
+ devtool: false,
+ entry: './src/entry.js',
+ output: {
+ path: join(directory, 'dist'),
+ filename: 'main.js',
+ clean: true,
+ },
+ optimization: {
+ minimize: false,
+ concatenateModules: false,
+ usedExports: false,
+ },
+ resolve: { extensions: ['.js', '.ts'] },
+ experiments: { css: true },
+ module: { rules: [{ test: /\.css$/, type: 'css' }] },
+ plugins: [
+ instance,
+ ...(holdLayers
+ ? [
+ {
+ apply(owner) {
+ owner.options.module.rules.push({
+ test: /\.css\.ts$/,
+ enforce: 'pre',
+ use: [gateFile],
+ })
+ },
+ },
+ ]
+ : []),
+ ],
+ })
+ assert(compiler)
+ const rules = compiler.options.module.rules
+ .flatMap((rule) => rule.use || [])
+ .filter((use) => use.options?.engine)
+ assert.equal(rules.length, 2)
+ for (const [index, subpath] of ['./loader', './css-loader'].entries()) {
+ assert.equal(rules[index].options.engine, resolvedEngine)
+ assert.equal(
+ realpathSync(rules[index].loader),
+ realpathSync(join(pluginRoot, manifest.exports[subpath].require)),
+ )
+ const fromLoader = createRequire(rules[index].loader)(
+ '@devup-ui/plugin-utils',
+ ).loadCompiler(selection.engine)
+ assert.equal(fromLoader, selected)
+ }
+ if (scenario === 'wrong-loader') {
+ rules[0].options.engine = resolvedEngine === 'native' ? 'wasm' : 'native'
+ const stats = await new Promise((resolve, reject) =>
+ compiler.run((error, stats) =>
+ error ? reject(error) : resolve(stats),
+ ),
+ )
+ const diagnostics = stats.compilation.errors.map((error) =>
+ errorEvidence(error.error ?? error),
+ )
+ put(destination, JSON.stringify({ diagnostics }, null, 2))
+ assert(stats.hasErrors())
+ assert.match(
+ stats.toString({ all: false, errors: true }),
+ /unselected-backend-resolution/,
+ )
+ return { wrongLoaderRejected: true, diagnostics }
+ }
+ if (scenario === 'late') {
+ const done = compiler.hooks.done.taps.find(
+ ({ name }) => name === 'DevupUIWebpackPlugin',
+ )
+ assert(done)
+ await new Promise((resolve, reject) =>
+ compiler.close((error) => (error ? reject(error) : resolve())),
+ )
+ compiler = undefined
+ const other = resolvedEngine === 'native' ? 'wasm' : 'native'
+ new plugin.DevupUIWebpackPlugin({ engine: other })
+ const before = calls.registerTheme
+ instance.writeDataFiles()
+ assert.equal(calls.registerTheme, before + 1)
+ const cssCalls = calls.getCss
+ await done.fn({ compilation: {}, hasErrors: () => false })
+ assert(calls.getCss > cssCalls)
+ assert.equal(utils.loadCompiler(selection.engine), selected)
+ return { late: true }
+ }
+ let additionalPasses = 0
+ compiler.hooks.additionalPass.tap('EngineProof', () => {
+ additionalPasses++
+ })
+ compiler.hooks.thisCompilation.tap('EngineProof', (compilation) => {
+ compilation.hooks.succeedModule.tap('EngineProof', (module) => {
+ if (holdLayers && module.resource?.endsWith('.css')) {
+ require(gateFile).release()
+ }
+ })
+ })
+ const snapshot = (stats) => {
+ if (stats.hasErrors()) {
+ put(
+ destination,
+ JSON.stringify(
+ {
+ diagnostics: stats.compilation.errors.map((error) =>
+ errorEvidence(error.error ?? error),
+ ),
+ },
+ null,
+ 2,
+ ),
+ )
+ }
+ assert(!stats.hasErrors(), stats.toString({ all: false, errors: true }))
+ const files = selected.exportFileMap()
+ const css = [null, ...new Set(Object.values(JSON.parse(files)))].map(
+ (number) => [number, selected.getCss(number, true)],
+ )
+ const served = [
+ ...(globalThis.__devupUiServedCss?.get(stats.compilation) ?? []),
+ ].sort(([a], [b]) => a.localeCompare(b))
+ for (const [path, value] of served) {
+ assert.equal(
+ value,
+ selected.getCss(utils.getFileNumByFilename(path), true),
+ )
+ }
+ assert(served.length > 0)
+ const assets = tree(join(directory, 'dist'))
+ for (const [path, content] of Object.entries(assets)) {
+ if (path.endsWith('.js')) {
+ assert.doesNotMatch(
+ content,
+ /Cannot run on the runtime|@devup-ui\/(?:binding|wasm)/,
+ )
+ }
+ }
+ const shippedCss = Object.entries(assets)
+ .filter(([path]) => path.endsWith('.css'))
+ .map(([, content]) => content)
+ .join('\n')
+ assert.match(shippedCss, /cyan/)
+ assert.match(shippedCss, /goldenrod|rebeccapurple/)
+ return {
+ outputs: Object.fromEntries(
+ [...outputs].sort(([a], [b]) => a.localeCompare(b)),
+ ),
+ css,
+ served,
+ sheet: selected.exportSheet(),
+ classes: selected.exportClassMap(),
+ files,
+ canonical: selected.exportCanonicalMap(),
+ assets,
+ disk: tree(join(directory, 'df')),
+ residual: selected.getUnreservedClassSlotCount(),
+ }
+ }
+ const generations = []
+ if (watch) {
+ await new Promise((resolve, reject) => {
+ let revision = 0
+ watching = compiler.watch(
+ {
+ aggregateTimeout: 0,
+ ignored: /[\\/](?:df|dist)[\\/]/,
+ },
+ (error, stats) => {
+ if (error) {
+ reject(error)
+ return
+ }
+ try {
+ generations.push(snapshot(stats))
+ if (revision++ === 0) {
+ watching.suspend()
+ put(join(directory, 'src/B.css.ts'), layer('rebeccapurple'))
+ watching.invalidate()
+ watching.resume()
+ } else {
+ resolve()
+ }
+ } catch (error) {
+ reject(error)
+ }
+ },
+ )
+ })
+ await new Promise((resolve, reject) =>
+ watching.close((error) => (error ? reject(error) : resolve())),
+ )
+ watching = undefined
+ } else {
+ const stats = await new Promise((resolve, reject) =>
+ compiler.run((error, stats) =>
+ error ? reject(error) : resolve(stats),
+ ),
+ )
+ generations.push(snapshot(stats))
+ }
+ for (const name of [
+ 'registerShorthands',
+ 'registerTheme',
+ 'reserveClassSlots',
+ 'setModuleResolver',
+ 'codeExtract',
+ 'getCss',
+ 'exportSheet',
+ 'exportClassMap',
+ 'exportFileMap',
+ ]) {
+ assert(calls[name] > 0, name)
+ }
+ const sourceModule =
+ format === 'import'
+ ? await import('@devup-ui/webpack-plugin/loader')
+ : require('@devup-ui/webpack-plugin/loader')
+ const cssModule =
+ format === 'import'
+ ? await import('@devup-ui/webpack-plugin/css-loader')
+ : require('@devup-ui/webpack-plugin/css-loader')
+ const sourceLoader = sourceModule.default ?? sourceModule
+ const cssLoader = cssModule.default ?? cssModule
+ const local = []
+ const sourcePath = join(directory, 'src/entry.js')
+ for (const source of [
+ 'export const plain = 1;',
+ readFileSync(sourcePath, 'utf8'),
+ 'import { Box } from "@devup-ui/react"; export const x = ;',
+ ]) {
+ await new Promise((resolve) =>
+ sourceLoader.call(
+ {
+ resourcePath: sourcePath,
+ getOptions: () => rules[0].options,
+ addDependency() {},
+ async: () => (error, code, map) => {
+ if (error) {
+ const evidence = errorEvidence(error)
+ errors.push(evidence)
+ local.push({ error: evidence })
+ resolve()
+ return
+ }
+ local.push({ code, map })
+ resolve()
+ },
+ },
+ Buffer.from(source),
+ ),
+ )
+ }
+ assert.equal(errors.length, 1)
+ const cssPath = join(directory, 'df/devup-ui/devup-ui.css')
+ const cssCallbacks = []
+ cssLoader.call(
+ {
+ resourcePath: cssPath,
+ getOptions: () => rules[1].options,
+ cacheable(value) {
+ assert.equal(value, false)
+ },
+ callback(error, css, map, meta) {
+ assert.equal(error, null)
+ cssCallbacks.push({ css, map, meta })
+ },
+ },
+ Buffer.from(''),
+ {
+ version: 3,
+ sources: [],
+ names: [],
+ mappings: '',
+ },
+ { token: 'meta' },
+ )
+ assert.equal(cssCallbacks[0].css, selected.getCss(null, true))
+ if (scenario === 'additional') assert(additionalPasses > 0)
+ return { generations, local, cssCallbacks, errors, additionalPasses }
+ } finally {
+ try {
+ try {
+ if (watching) {
+ await new Promise((resolve, reject) =>
+ watching.close((error) => (error ? reject(error) : resolve())),
+ )
+ }
+ } finally {
+ if (compiler) {
+ await new Promise((resolve, reject) =>
+ compiler.close((error) => (error ? reject(error) : resolve())),
+ )
+ }
+ }
+ } finally {
+ Module._resolveFilename = load
+ Date.now = now
+ process.chdir(previousCwd)
+ if (originals.size) {
+ const selected = utils.loadCompiler(selection.engine)
+ for (const [name, method] of originals) {
+ selected[name] = method
+ }
+ selected.resetStateForTesting()
+ }
+ }
+ }
+}
+
+if (
+ process.argv[1] &&
+ (['vite', 'bun'].includes(process.argv[7]) ? viteRealpath : realpathSync)(
+ resolve(process.argv[1]),
+ ) ===
+ (['vite', 'bun'].includes(process.argv[7]) ? viteRealpath : realpathSync)(
+ fileURLToPath(import.meta.url),
+ )
+) {
+ const [scenario, mode, format, css, destination, adapter = 'webpack'] =
+ process.argv.slice(2)
+ assert(
+ [
+ 'build',
+ 'additional',
+ 'watch',
+ 'late',
+ 'guards',
+ 'wrong-loader',
+ 'errors',
+ 'dev',
+ 'lifecycle',
+ 'repeat',
+ 'opposite',
+ 'runtime',
+ 'preload',
+ ].includes(scenario),
+ )
+ assert(['default', 'native', 'wasm'].includes(mode))
+ assert(['require', 'import'].includes(format))
+ const result = await consume({
+ directory: process.cwd(),
+ scenario,
+ mode,
+ format,
+ singleCss: css === 'single',
+ destination,
+ adapter,
+ })
+ writeFileSync(
+ destination,
+ JSON.stringify(
+ result,
+ (_, value) => (value === undefined ? { undefined: true } : value),
+ 2,
+ ),
+ )
+}
diff --git a/e2e/plugins/engine-dev-cuts.mjs b/e2e/plugins/engine-dev-cuts.mjs
new file mode 100644
index 000000000..cbdd80962
--- /dev/null
+++ b/e2e/plugins/engine-dev-cuts.mjs
@@ -0,0 +1,650 @@
+import assert from 'node:assert/strict'
+import { AsyncLocalStorage } from 'node:async_hooks'
+import { spawnSync } from 'node:child_process'
+import {
+ mkdirSync,
+ readdirSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from 'node:fs'
+import { request } from 'node:http'
+import { createRequire } from 'node:module'
+import { dirname, join, resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+import {
+ diagnosticHook,
+ errorEvidence,
+ viteRealpath,
+} from './engine-consumer.mjs'
+
+const filename = fileURLToPath(import.meta.url)
+const json = (value) =>
+ JSON.stringify(
+ value,
+ (_, item) => (item === undefined ? { undefined: true } : item),
+ 2,
+ )
+const save = (path, value) => writeFileSync(path, json(value))
+const [command, ...args] = process.argv.slice(2)
+assert.equal(viteRealpath(resolve(process.argv[1])), viteRealpath(filename))
+assert.equal(process.versions.bun, undefined)
+if (command === 'matrix') {
+ const [root, port] = args
+ const summary = []
+ for (const defaults of [false, true])
+ for (const css of ['per-file', 'single']) {
+ const receipts = []
+ for (const [index, engine] of [
+ 'native',
+ 'native',
+ 'wasm',
+ 'wasm',
+ ].entries()) {
+ const base = join(
+ root,
+ `cuts-${defaults ? 'defaults' : 'hmr'}-${css}-${index}`,
+ )
+ const child = spawnSync(
+ process.execPath,
+ [filename, engine, css, String(defaults), `${base}.json`, port],
+ {
+ cwd: process.cwd(),
+ env: process.env,
+ timeout: 120000,
+ windowsHide: true,
+ },
+ )
+ writeFileSync(`${base}.stdout`, child.stdout ?? '')
+ writeFileSync(`${base}.stderr`, child.stderr ?? '')
+ const ok = !child.error && child.status === 0 && child.signal === null
+ summary.push({
+ base,
+ ok,
+ error: child.error && errorEvidence(child.error),
+ status: child.status,
+ signal: child.signal,
+ })
+ try {
+ receipts.push(
+ ok ? JSON.parse(readFileSync(`${base}.json`, 'utf8')) : undefined,
+ )
+ } catch (error) {
+ receipts.push(undefined)
+ summary.push({ base, ok: false, error: errorEvidence(error) })
+ }
+ }
+ for (const [left, right] of [
+ [0, 1],
+ [2, 3],
+ [0, 2],
+ ]) {
+ try {
+ assert(
+ receipts[left] && receipts[right],
+ 'Missing successful scheduled endpoint',
+ )
+ assert.deepEqual(receipts[left], receipts[right])
+ summary.push({ defaults, css, left, right, ok: true })
+ } catch (error) {
+ summary.push({
+ defaults,
+ css,
+ left,
+ right,
+ ok: false,
+ error: errorEvidence(error),
+ })
+ }
+ }
+ save(join(root, 'dev-cuts-summary.json'), summary)
+ }
+ assert(
+ summary.every((row) => row.ok),
+ 'Inspect dev-cuts-summary.json and raw streams',
+ )
+} else {
+ assert(['native', 'wasm'].includes(command))
+ const [css, defaultsText, destination, portText] = args
+ const defaults = defaultsText === 'true',
+ singleCss = css === 'single',
+ port = Number(portText)
+ const directory = viteRealpath(process.cwd()),
+ generated = join(directory, 'df')
+ const require = createRequire(join(directory, 'package.json'))
+ const pluginPath = require.resolve('@devup-ui/vite-plugin')
+ const { DevupUI } = require(pluginPath)
+ const utils = createRequire(pluginPath)('@devup-ui/plugin-utils')
+ const compiler = utils.loadCompiler(command),
+ originalClock = Date.now
+ const context = new AsyncLocalStorage(),
+ ready = Promise.withResolvers(),
+ poison = Promise.withResolvers()
+ const trace = [],
+ operations = [],
+ turns = [],
+ fences = new Map(),
+ restores = [],
+ cuts = []
+ let clock = 2000000000000,
+ phase = 'C0',
+ finding,
+ server,
+ creation,
+ owner,
+ originalCss,
+ originalProof
+ Date.now = () => context.getStore()?.fence?.clock ?? clock
+ const fail = (error) => {
+ finding ??= error instanceof Error ? error : new Error(String(error))
+ trace.push({ failure: errorEvidence(finding) })
+ poison.resolve(finding)
+ }
+ const check = () => {
+ if (finding) throw finding
+ }
+ const bounded = async (work, clean = false) => {
+ let timer
+ try {
+ return await Promise.race([
+ work,
+ new Promise((_, reject) => {
+ timer = setTimeout(
+ () => reject(new Error('Vite cut operation exceeded 10 seconds')),
+ 10000,
+ )
+ }),
+ ...(clean
+ ? []
+ : [
+ poison.promise.then((error) => {
+ throw error
+ }),
+ ]),
+ ])
+ } finally {
+ clearTimeout(timer)
+ }
+ }
+ const observed = (work) => {
+ const task = Promise.resolve(work).then(
+ (value) => ({ value }),
+ (error) => {
+ fail(error)
+ return { error: errorEvidence(error) }
+ },
+ )
+ operations.push(task)
+ return task
+ }
+ const state = () => ({
+ css: compiler.getCss(null, false),
+ sheet: compiler.exportSheet(),
+ classes: compiler.exportClassMap(),
+ files: compiler.exportFileMap(),
+ canonical: compiler.exportCanonicalMap(),
+ residual: compiler.getUnreservedClassSlotCount(),
+ prefix: compiler.getPrefix(),
+ debug: compiler.isDebug(),
+ theme: compiler.getDefaultTheme(),
+ })
+ const disk = () =>
+ Object.fromEntries(
+ readdirSync(generated, { recursive: true, withFileTypes: true })
+ .filter((entry) => entry.isFile())
+ .map((entry) => {
+ const path = join(entry.parentPath, entry.name)
+ return [path, readFileSync(path).toString('base64')]
+ })
+ .sort(([a], [b]) => a.localeCompare(b)),
+ )
+ const put = (path, content) => {
+ mkdirSync(dirname(join(directory, path)), { recursive: true })
+ writeFileSync(join(directory, path), content)
+ }
+ const urls = (code) =>
+ [...code.matchAll(/["']([^"'\n]*devup-ui(?:-\d+)?\.css[^"'\n]*)["']/g)].map(
+ (match) => match[1],
+ )
+ const http = async (url, accept) => {
+ const receipt = await bounded(
+ new Promise((resolveResponse, reject) => {
+ const req = request(
+ {
+ host: '127.0.0.1',
+ port,
+ path: url,
+ headers: { Accept: accept },
+ agent: false,
+ },
+ (res) => {
+ const chunks = []
+ res.on('data', (chunk) => chunks.push(chunk))
+ res.on('error', reject)
+ res.on('aborted', () =>
+ reject(new Error(`Aborted response: ${url}`)),
+ )
+ res.on('end', () =>
+ resolveResponse({
+ url,
+ accept,
+ queryBytes: Buffer.from(
+ url.includes('?') ? url.slice(url.indexOf('?')) : '',
+ ).toString('base64'),
+ requestRaw: req._header,
+ status: res.statusCode,
+ statusMessage: res.statusMessage,
+ httpVersion: res.httpVersion,
+ rawHeaders: res.rawHeaders,
+ headers: res.headers,
+ allheaders: res.headersDistinct,
+ rawTrailers: res.rawTrailers,
+ body: Buffer.concat(chunks).toString('base64'),
+ }),
+ )
+ },
+ )
+ req.on('error', reject)
+ req.setTimeout(10000, () =>
+ req.destroy(new Error(`HTTP deadline: ${url}`)),
+ )
+ req.end()
+ }),
+ )
+ trace.push({ http: receipt })
+ assert.equal(receipt.status, 200)
+ return receipt
+ }
+ const witness = (receipt) => {
+ const name = receipt.url.split('?')[0].split('/').at(-1),
+ path = join(generated, 'devup-ui', name)
+ const bytes = readFileSync(path),
+ live = compiler.getCss(utils.getFileNumByFilename(name), false)
+ const result = {
+ url: receipt.url,
+ path,
+ live,
+ disk: bytes.toString('base64'),
+ response: receipt.body,
+ }
+ assert.equal(result.response, result.disk)
+ assert.equal(bytes.toString(), live)
+ return result
+ }
+ const seal = async () => {
+ const count = operations.length
+ await bounded(Promise.all(operations))
+ check()
+ assert.equal(
+ operations.length,
+ count,
+ 'Work was added during the finite cut',
+ )
+ for (const turn of turns) turn.sealed = true
+ }
+ const publish = async (name, revision) => {
+ const transforms = {},
+ responses = [],
+ coherence = [],
+ discovered = new Set()
+ for (const url of [
+ '/src/A.js',
+ '/src/B.js',
+ '/src/entry.js',
+ '/src/tokens.js',
+ ]) {
+ transforms[`admit:${url}`] = await bounded(
+ server.environments.client.transformRequest(url),
+ )
+ assert.equal(typeof transforms[`admit:${url}`]?.code, 'string')
+ }
+ for (const url of ['/', '/src/A.js', '/src/B.js']) {
+ const receipt = await http(url, '*/*')
+ responses.push(receipt)
+ for (const sheet of urls(Buffer.from(receipt.body, 'base64').toString()))
+ discovered.add(sheet)
+ }
+ for (const url of ['/src/A.js', '/src/B.js']) {
+ transforms[url] = await bounded(
+ server.environments.client.transformRequest(url),
+ )
+ for (const sheet of urls(transforms[url].code)) discovered.add(sheet)
+ }
+ assert(discovered.size > 0)
+ for (const url of discovered) {
+ assert.match(url, /[?&]devup-revision=\d+/)
+ const raw = await http(url, 'text/css')
+ coherence.push(witness(raw))
+ responses.push(raw)
+ if (/\/devup-ui\.css(?:\?|$)/.test(url) && !originalCss) originalCss = raw
+ const js = await http(url, '*/*'),
+ direct = await http(`${url}&direct`, '*/*')
+ coherence.push(witness(direct))
+ responses.push(js, direct)
+ const embedded = /const __vite__css = ("(?:[^"\\]|\\.)*")/.exec(
+ Buffer.from(js.body, 'base64').toString(),
+ )
+ assert(embedded)
+ assert.equal(JSON.parse(embedded[1]), coherence.at(-1).live)
+ assert.match(raw.headers['content-type'], /text\/css/)
+ assert.match(direct.headers['content-type'], /text\/css/)
+ assert.match(js.headers['content-type'], /javascript/)
+ }
+ for (const name of ['A', 'B']) {
+ const mapped = urls(transforms[`/src/${name}.js`].code),
+ numbered = mapped.filter((url) => /devup-ui-\d+\.css/.test(url))
+ const buckets = numbered.length ? numbered : mapped
+ assert(buckets.length > 0)
+ for (const url of buckets)
+ assert.match(
+ Buffer.from(
+ responses.find(
+ (item) => item.url === url && item.accept === 'text/css',
+ ).body,
+ 'base64',
+ ).toString(),
+ revision ? /teal/ : /maroon/,
+ )
+ }
+ await seal()
+ const cut = {
+ name,
+ transforms,
+ http: responses,
+ coherence,
+ original: originalProof,
+ disk: disk(),
+ state: state(),
+ }
+ assert.equal(cut.state.residual, 0)
+ assert.match(cut.state.css, revision === 2 ? /29px/ : /17px/)
+ for (const [path, body] of Object.entries(cut.disk))
+ if (path.endsWith('.css'))
+ assert.equal(
+ Buffer.from(body, 'base64').toString(),
+ compiler.getCss(utils.getFileNumByFilename(path), false),
+ )
+ cuts.push(cut)
+ save(destination, cuts)
+ return cut
+ }
+ try {
+ for (const name of ['src', 'df', '.vite-cuts'])
+ rmSync(join(directory, name), { recursive: true, force: true })
+ put('devup.json', '{"extends":["./theme.json"]}')
+ put('theme.json', '{"theme":{"length":{"default":{"size":"17px"}}}}')
+ put('index.html', '')
+ put('src/tokens.js', 'export const color="maroon";')
+ for (const name of ['A', 'B'])
+ put(
+ `src/${name}.js`,
+ `import {css} from '@devup-ui/react'; import {color} from './tokens.js'; export const cls=css({color,w:'$size',p:${name === 'A' ? 1 : 2}});`,
+ )
+ put(
+ 'src/entry.js',
+ 'import {cls as a} from "./A.js"; import {cls as b} from "./B.js"; let latestA=a,latestB=b; const render=()=>{document.body.className=latestA+" "+latestB}; render(); if(import.meta.hot) import.meta.hot.accept(["./A.js","./B.js"],([nextA,nextB])=>{if(nextA)latestA=nextA.cls;if(nextB)latestB=nextB.cls;render()});',
+ )
+ compiler.resetStateForTesting()
+ const extract = compiler.codeExtract
+ let extractionCount = 0
+ compiler.codeExtract = (...values) => {
+ extractionCount++
+ return extract(...values)
+ }
+ restores.push(() => {
+ compiler.codeExtract = extract
+ })
+ const plugins = DevupUI({
+ engine: command,
+ singleCss,
+ ...(defaults ? {} : { atomHoist: 0 }),
+ })
+ owner = plugins[0]
+ for (const plugin of plugins)
+ for (const name of [
+ 'configResolved',
+ 'buildStart',
+ 'watchChange',
+ 'hotUpdate',
+ 'transform',
+ 'load',
+ 'closeBundle',
+ ]) {
+ if (typeof plugin[name] === 'function')
+ plugin[name] = diagnosticHook(plugin[name], (_, error) => fail(error))
+ }
+ const { createServer, createLogger } = await import('vite')
+ const logger = createLogger('silent'),
+ logError = logger.error,
+ consoleError = console.error
+ logger.error = (...values) => {
+ trace.push({ logger: values.map(errorEvidence) })
+ fail(values[0])
+ return logError(...values)
+ }
+ console.error = (...values) => {
+ trace.push({ console: values.map(errorEvidence) })
+ fail(values[0])
+ return consoleError(...values)
+ }
+ const fatal = (error, origin) => {
+ trace.push({ origin })
+ fail(error)
+ }
+ process.on('uncaughtExceptionMonitor', fatal)
+ restores.push(() => {
+ console.error = consoleError
+ process.off('uncaughtExceptionMonitor', fatal)
+ })
+ const observer = {
+ name: 'devup-cut-inputs',
+ enforce: 'post',
+ configureServer(current) {
+ const watcher = current.watcher,
+ emit = watcher.emit
+ watcher.emit = function (event, ...values) {
+ if (!['add', 'change', 'unlink'].includes(event))
+ return Reflect.apply(emit, this, [event, ...values])
+ const file = resolve(values[0]),
+ fence = fences.get(file)
+ const turn = {
+ id: turns.length,
+ file,
+ fence,
+ event,
+ phase,
+ hooks: new Map(),
+ sealed: false,
+ }
+ turns.push(turn)
+ trace.push({ native: { id: turn.id, file, event, phase } })
+ if (fence) {
+ if (fence.turn) fail(new Error(`Duplicate armed input: ${file}`))
+ else fence.turn = turn
+ }
+ return context.run(turn, () =>
+ Reflect.apply(emit, this, [event, ...values]),
+ )
+ }
+ watcher.once('ready', ready.resolve)
+ watcher.on('error', fail)
+ restores.push(() => {
+ watcher.emit = emit
+ watcher.off('error', fail)
+ watcher.off('ready', ready.resolve)
+ })
+ for (const environment of Object.values(current.environments)) {
+ const hot = environment.hot,
+ send = hot.send
+ hot.send = function (...values) {
+ trace.push({ hot: environment.name, values })
+ if (values[0]?.type === 'error') fail(new Error(json(values[0])))
+ return Reflect.apply(send, this, values)
+ }
+ restores.push(() => {
+ hot.send = send
+ })
+ }
+ },
+ hotUpdate: {
+ order: 'post',
+ handler({ file, type, timestamp }) {
+ const turn = context.getStore()
+ assert(turn && !turn.sealed)
+ assert.equal(resolve(file), turn.file)
+ assert.equal(
+ type,
+ { add: 'create', change: 'update', unlink: 'delete' }[turn.event],
+ )
+ assert(!turn.hooks.has(this.environment.name))
+ turn.hooks.set(this.environment.name, timestamp)
+ trace.push({
+ post: this.environment.name,
+ turn: turn.id,
+ file,
+ type,
+ timestamp,
+ })
+ },
+ },
+ }
+ observer.hotUpdate.handler = diagnosticHook(
+ observer.hotUpdate.handler,
+ (_, error) => fail(error),
+ )
+ const options = {
+ root: directory,
+ configFile: false,
+ customLogger: logger,
+ cacheDir: join(directory, '.vite-cuts'),
+ plugins: [...plugins, observer],
+ optimizeDeps: { noDiscovery: true, include: [] },
+ server: {
+ host: '127.0.0.1',
+ port,
+ strictPort: true,
+ headers: { Date: new Date(clock).toUTCString() },
+ ...(defaults ? {} : { preTransformRequests: false }),
+ hotUpdateEnvironments(current, hmr) {
+ const turn = context.getStore()
+ const work = (async () => {
+ assert(turn && !turn.sealed)
+ const environments = Object.values(current.environments)
+ assert.deepEqual(
+ [...turn.hooks.keys()],
+ environments.map((environment) => environment.name),
+ )
+ assert.equal(new Set(turn.hooks.values()).size, 1)
+ const outcomes = await Promise.all(
+ environments.map((environment) => observed(hmr(environment))),
+ )
+ check()
+ assert(outcomes.every((result) => !result.error))
+ trace.push({ completed: turn.id, outcomes })
+ const fence = fences.get(turn.file)
+ if (fence?.turn === turn) fence.resolve()
+ })()
+ observed(work)
+ return work
+ },
+ },
+ }
+ creation = createServer(options).then(async (value) => {
+ server = value
+ if (finding) {
+ await bounded(value.close(), true)
+ throw finding
+ }
+ return value
+ })
+ observed(creation)
+ await bounded(creation)
+ assert.equal(server.config.server.preTransformRequests, defaults)
+ assert.equal(
+ server.environments.client.config.dev.preTransformRequests,
+ defaults,
+ )
+ await bounded(server.listen())
+ assert.equal(server.httpServer.address().port, port)
+ await bounded(ready.promise)
+ await bounded(server.environments.client.pluginContainer.buildStart())
+ await publish(defaults ? 'final' : 'C0', 0)
+ assert(
+ extractionCount > 0,
+ 'Installed plugin must use the observed compiler namespace',
+ )
+ if (!defaults)
+ for (const [index, file, content] of [
+ [1, 'src/tokens.js', 'export const color="teal";'],
+ [2, 'theme.json', '{"theme":{"length":{"default":{"size":"29px"}}}}'],
+ ]) {
+ phase = `C${index}`
+ const fence = Promise.withResolvers()
+ fence.clock = (clock += 10000) + (index === 2 ? 1000 : 0)
+ fences.set(resolve(directory, file), fence)
+ put(file, content)
+ await bounded(fence.promise)
+ assert(fence.turn)
+ if (index === 2) {
+ assert(originalCss)
+ assert.match(
+ Buffer.from(originalCss.body, 'base64').toString(),
+ /17px/,
+ )
+ const updated = await http(originalCss.url, originalCss.accept),
+ coherent = witness(updated)
+ assert.match(coherent.live, /29px/)
+ assert(originalCss.headers.etag && updated.headers.etag)
+ assert.notEqual(updated.headers.etag, originalCss.headers.etag)
+ originalProof = {
+ before: originalCss,
+ after: updated,
+ coherence: coherent,
+ }
+ }
+ await publish(phase, index)
+ }
+ if (!defaults) await publish('final', 2)
+ } catch (error) {
+ fail(error)
+ process.exitCode = 1
+ } finally {
+ for (const release of [
+ async () => {
+ if (creation) {
+ const current = await bounded(creation, true)
+ await bounded(current.close(), true)
+ }
+ },
+ async () => {
+ if (owner) await bounded(owner.closeBundle(), true)
+ },
+ ]) {
+ try {
+ await release()
+ } catch (error) {
+ fail(error)
+ process.exitCode = 1
+ }
+ }
+ try {
+ await bounded(Promise.all(operations), true)
+ const final = cuts.at(-1)
+ await seal()
+ assert.deepEqual(disk(), final.disk)
+ assert.deepEqual(state(), final.state)
+ assert.equal(utils.getBuildOwnership().running, 0)
+ check()
+ save(destination, cuts)
+ } catch (error) {
+ fail(error)
+ process.exitCode = 1
+ }
+ save(`${destination}.trace.json`, trace)
+ for (const restore of restores.reverse()) restore()
+ context.disable()
+ Date.now = originalClock
+ compiler.resetStateForTesting()
+ }
+}
diff --git a/e2e/plugins/engine-fixtures.mjs b/e2e/plugins/engine-fixtures.mjs
new file mode 100644
index 000000000..17d844076
--- /dev/null
+++ b/e2e/plugins/engine-fixtures.mjs
@@ -0,0 +1,476 @@
+import { execFileSync } from 'node:child_process'
+import { createHash } from 'node:crypto'
+import {
+ existsSync,
+ mkdirSync,
+ mkdtempSync,
+ readdirSync,
+ readFileSync,
+ realpathSync,
+ rmSync,
+ statSync,
+} from 'node:fs'
+import { createRequire } from 'node:module'
+import { tmpdir } from 'node:os'
+import {
+ basename,
+ dirname,
+ isAbsolute,
+ join,
+ relative,
+ resolve,
+ sep,
+} from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+import { selectHostTarget } from '../binding/package/build-host.mjs'
+import { targets } from '../binding/package/matrix.mjs'
+
+export function hostPackRealpath(
+ filename,
+ realpath = { native: realpathSync.native, regular: realpathSync },
+) {
+ try {
+ return realpath.native(filename)
+ } catch (error) {
+ if (
+ typeof error === 'object' &&
+ error !== null &&
+ 'code' in error &&
+ error.code === 'EISDIR'
+ )
+ return realpath.regular(filename)
+ throw error
+ }
+}
+
+export class HostPackError extends Error {
+ constructor(category, packageName, cause) {
+ super(
+ `${packageName}: ${category}${typeof cause === 'string' ? `: ${cause}` : ''}`,
+ { cause },
+ )
+ this.name = 'HostPackError'
+ this.category = category
+ this.packageName = packageName
+ }
+}
+
+export function hostPackSelection() {
+ const row = selectHostTarget()
+ if (!row)
+ throw new HostPackError(
+ 'missing-host',
+ '@devup-ui/binding',
+ 'unsupported host',
+ )
+ return {
+ name: `@devup-ui/binding-${row.suffix}`,
+ omitted: targets
+ .filter((item) => item !== row)
+ .map((item) => `@devup-ui/binding-${item.suffix}`),
+ installArgs: [
+ `--os=${row.os}`,
+ `--cpu=${row.cpu}`,
+ ...(row.libc ? [`--libc=${row.libc}`] : []),
+ ],
+ }
+}
+
+export async function packHostEngines(context, run) {
+ const host = hostPackSelection()
+ const tarballs = {}
+ for (const name of ['@devup-ui/binding', host.name]) {
+ const short = name.slice('@devup-ui/'.length)
+ const source = join(context.repository, 'bindings', `devup-ui-${short}`)
+ const manifest = JSON.parse(
+ readFileSync(join(source, 'package.json'), 'utf8'),
+ )
+ const entry = statSync(join(source, manifest.main))
+ if (!entry.isFile() || entry.size === 0) {
+ throw new HostPackError(
+ 'byte-mismatch',
+ name,
+ 'missing existing checkout build entry',
+ )
+ }
+ const destination = join(context.root, 'packs', short)
+ mkdirSync(destination, { recursive: true })
+ await run(
+ context.bun,
+ [
+ 'pm',
+ 'pack',
+ '--ignore-scripts',
+ '--quiet',
+ '--destination',
+ destination,
+ ],
+ {
+ cwd: source,
+ },
+ )
+ const files = readdirSync(destination).filter((file) =>
+ file.endsWith('.tgz'),
+ )
+ if (files.length !== 1)
+ throw new HostPackError(
+ 'byte-mismatch',
+ name,
+ 'expected one host tarball',
+ )
+ tarballs[name] =
+ `file:${hostPackRealpath(join(destination, files[0])).split(sep).join('/')}`
+ }
+ return { host, tarballs }
+}
+
+function archivePath(reference, base) {
+ if (typeof reference !== 'string' || !reference.startsWith('file:')) {
+ throw new TypeError('Expected a local file tarball reference')
+ }
+ return hostPackRealpath(
+ reference.startsWith('file://')
+ ? fileURLToPath(reference)
+ : resolve(base, reference.slice('file:'.length)),
+ )
+}
+
+function payload(directory, name, integrity) {
+ const files = new Map()
+ const pending = [{ directory, prefix: '' }]
+ while (pending.length) {
+ const current = pending.pop()
+ for (const entry of readdirSync(current.directory, {
+ withFileTypes: true,
+ })) {
+ if (entry.name === 'node_modules') continue
+ const path = join(current.directory, entry.name)
+ const key = current.prefix + entry.name
+ if (entry.name === '.bun-tag') {
+ if (key !== '.bun-tag') {
+ throw new HostPackError('marker-nested', name, key)
+ }
+ if (!entry.isFile()) {
+ throw new HostPackError('marker-kind', name, key)
+ }
+ if (!readFileSync(path).equals(Buffer.from(integrity))) {
+ throw new HostPackError('marker-bytes', name, key)
+ }
+ }
+ if (entry.isDirectory())
+ pending.push({ directory: path, prefix: `${key}/` })
+ else if (entry.isFile())
+ files.set(
+ key,
+ createHash('sha256').update(readFileSync(path)).digest('hex'),
+ )
+ else
+ throw new HostPackError(
+ 'byte-mismatch',
+ name,
+ `nonregular payload ${key}`,
+ )
+ }
+ }
+ return files
+}
+
+function packedPayload(name, reference) {
+ const scratch = mkdtempSync(join(tmpdir(), 'devup-host-pack-'))
+ try {
+ const file = archivePath(reference, process.cwd())
+ const options = { cwd: dirname(file), encoding: 'utf8', timeout: 30000 }
+ const members = execFileSync('tar', ['-tf', basename(file)], options)
+ .split(/\r?\n/)
+ .filter(Boolean)
+ const seen = new Set()
+ for (const member of members) {
+ const parts = member.replace(/\/$/, '').split('/')
+ if (parts.includes('.bun-tag')) {
+ throw new HostPackError('archive-marker', name, member)
+ }
+ if (
+ parts[0] !== 'package' ||
+ member.includes('\\') ||
+ parts.some(
+ (part) =>
+ !part ||
+ part === '.' ||
+ part === '..' ||
+ part.includes(':') ||
+ part === 'node_modules',
+ ) ||
+ seen.has(member)
+ ) {
+ throw new HostPackError(
+ 'byte-mismatch',
+ name,
+ `unsafe archive member ${member}`,
+ )
+ }
+ seen.add(member)
+ }
+ const listing = execFileSync('tar', ['-tvf', basename(file)], options)
+ .split(/\r?\n/)
+ .filter(Boolean)
+ if (listing.some((line) => line[0] !== '-' && line[0] !== 'd')) {
+ throw new HostPackError(
+ 'byte-mismatch',
+ name,
+ 'archive links or special files',
+ )
+ }
+ execFileSync('tar', ['-xf', basename(file), '-C', scratch], options)
+ const directory = join(scratch, 'package')
+ if (
+ JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')).name !==
+ name
+ ) {
+ throw new HostPackError('byte-mismatch', name, 'archive package name')
+ }
+ const compressed = readFileSync(file)
+ return {
+ files: payload(directory, name),
+ sha256: createHash('sha256').update(compressed).digest('hex'),
+ integrity: `sha512-${createHash('sha512').update(compressed).digest('base64')}`,
+ }
+ } catch (cause) {
+ if (cause instanceof HostPackError) throw cause
+ throw new HostPackError('byte-mismatch', name, cause)
+ } finally {
+ rmSync(scratch, { recursive: true, force: true })
+ }
+}
+
+function installedNodes(directory) {
+ const pending = [join(directory, 'node_modules')]
+ const visited = new Set()
+ const packages = new Map()
+ while (pending.length) {
+ const path = pending.pop()
+ const physical = hostPackRealpath(path)
+ if (!statSync(physical).isDirectory()) continue
+ const parent = dirname(path)
+ const slot =
+ basename(parent) === 'node_modules'
+ ? basename(path)
+ : basename(dirname(parent)) === 'node_modules' &&
+ basename(parent).startsWith('@')
+ ? `${basename(parent)}/${basename(path)}`
+ : undefined
+ const manifestFile = join(physical, 'package.json')
+ if (slot && existsSync(manifestFile)) {
+ const manifest = JSON.parse(readFileSync(manifestFile, 'utf8'))
+ if (slot.startsWith('@devup-ui/') && manifest.name !== slot) {
+ throw new HostPackError(
+ 'unexpected-package',
+ slot,
+ 'installed manifest name',
+ )
+ }
+ if (
+ typeof manifest.name === 'string' &&
+ manifest.name.startsWith('@devup-ui/')
+ ) {
+ packages.set(physical, { name: manifest.name, path: physical })
+ }
+ } else if (slot?.startsWith('@devup-ui/')) {
+ throw new HostPackError(
+ 'unexpected-package',
+ slot,
+ 'missing installed manifest',
+ )
+ }
+ if (visited.has(physical)) continue
+ visited.add(physical)
+ for (const entry of readdirSync(physical, { withFileTypes: true })) {
+ // Metadata cannot contain packages; payload() still inspects its kind/bytes.
+ if (entry.name === '.bun-tag') continue
+ if (entry.isDirectory() || entry.isSymbolicLink())
+ pending.push(join(physical, entry.name))
+ }
+ }
+ return [...packages.values()]
+}
+
+function verifySource(name, reference, context) {
+ const records = Object.entries(context.lock.packages).filter(
+ ([key, record]) =>
+ key === name ||
+ key.endsWith(`/${name}`) ||
+ (Array.isArray(record) &&
+ typeof record[0] === 'string' &&
+ record[0].startsWith(`${name}@`)),
+ )
+ try {
+ if (records.length === 0)
+ throw new TypeError('missing Bun package source record')
+ const expected = archivePath(reference, context.directory)
+ for (const [, record] of records) {
+ if (
+ !Array.isArray(record) ||
+ typeof record[0] !== 'string' ||
+ !record[0].startsWith(`${name}@`)
+ ) {
+ throw new TypeError('non-file Bun package source')
+ }
+ const source = record[0].slice(name.length + 1)
+ if (!isAbsolute(source) || hostPackRealpath(source) !== expected) {
+ throw new TypeError(
+ "Bun package source is not this run's canonical tarball",
+ )
+ }
+ }
+ } catch (cause) {
+ throw new HostPackError('source-mismatch', name, cause)
+ }
+ return records.map(([, record]) => record)
+}
+
+export function verifyHostInstall(options) {
+ const host = hostPackSelection()
+ const directory = hostPackRealpath(options.directory)
+ const tarballs = new Map(
+ Object.entries(options.tarballs).map(([name, reference]) => [
+ name.startsWith('@devup-ui/') ? name : `@devup-ui/${name}`,
+ reference,
+ ]),
+ )
+ const installed = installedNodes(directory)
+ if (!installed.some((item) => item.name === host.name)) {
+ throw new HostPackError(
+ 'missing-host',
+ host.name,
+ 'host sibling not installed',
+ )
+ }
+ if (!installed.some((item) => item.name === '@devup-ui/binding')) {
+ throw new HostPackError(
+ 'source-mismatch',
+ '@devup-ui/binding',
+ 'binding root not installed',
+ )
+ }
+ for (const binding of installed.filter(
+ (item) => item.name === '@devup-ui/binding',
+ )) {
+ try {
+ const require = createRequire(join(binding.path, 'package.json'))
+ const sibling = hostPackRealpath(
+ dirname(require.resolve(`${host.name}/package.json`)),
+ )
+ if (
+ !installed.some(
+ (item) => item.name === host.name && item.path === sibling,
+ )
+ ) {
+ throw new TypeError(
+ 'host sibling resolves outside the installed inventory',
+ )
+ }
+ } catch (cause) {
+ throw new HostPackError('missing-host', host.name, cause)
+ }
+ }
+ let lock
+ try {
+ lock = JSON.parse(
+ execFileSync(
+ options.bun ?? 'bun',
+ [
+ '--eval',
+ "process.stdout.write(JSON.stringify(Bun.JSONC.parse(require('node:fs').readFileSync(process.env.DEVUP_ENGINE_LOCKFILE, 'utf8'))))",
+ ],
+ {
+ cwd: directory,
+ encoding: 'utf8',
+ timeout: 30000,
+ env: {
+ ...process.env,
+ DEVUP_ENGINE_LOCKFILE: join(directory, 'bun.lock'),
+ },
+ },
+ ),
+ )
+ if (
+ !lock ||
+ typeof lock.packages !== 'object' ||
+ lock.packages === null ||
+ Array.isArray(lock.packages)
+ ) {
+ throw new TypeError('missing Bun packages table')
+ }
+ } catch (cause) {
+ throw new HostPackError('source-mismatch', '@devup-ui/binding', cause)
+ }
+ const cache = new Map()
+ for (const item of installed) {
+ if (!tarballs.has(item.name) || host.omitted.includes(item.name)) {
+ throw new HostPackError(
+ 'unexpected-package',
+ item.name,
+ 'not in this host pack inventory',
+ )
+ }
+ const location = relative(directory, item.path)
+ if (
+ location === '..' ||
+ location.startsWith(`..${sep}`) ||
+ isAbsolute(location)
+ ) {
+ throw new HostPackError(
+ 'source-mismatch',
+ item.name,
+ 'installed link escapes consumer',
+ )
+ }
+ const reference = tarballs.get(item.name)
+ const records = verifySource(item.name, reference, { directory, lock })
+ if (!cache.has(item.name))
+ cache.set(item.name, packedPayload(item.name, reference))
+ const packed = cache.get(item.name)
+ for (const record of records) {
+ if (typeof record[2] !== 'string' || record[2].length === 0) {
+ throw new HostPackError('lock-integrity-missing', item.name)
+ }
+ if (record[2] !== packed.integrity) {
+ throw new HostPackError('lock-integrity-mismatch', item.name)
+ }
+ }
+ const expected = new Map(packed.files)
+ const actual = payload(item.path, item.name, packed.integrity)
+ if (actual.has('.bun-tag')) {
+ expected.set(
+ '.bun-tag',
+ createHash('sha256')
+ .update(Buffer.from(packed.integrity))
+ .digest('hex'),
+ )
+ }
+ if (
+ actual.size !== expected.size ||
+ [...actual].some(([file, hash]) => expected.get(file) !== hash)
+ ) {
+ throw new HostPackError(
+ 'byte-mismatch',
+ item.name,
+ 'installed payload differs from its tarball',
+ )
+ }
+ }
+ const receipt = {
+ event: 'devup.host-pack-proof',
+ mode: 'host-only',
+ host: host.name,
+ packages: installed,
+ omitted: host.omitted,
+ tarballs: [...cache].map(([name, packed]) => ({
+ name,
+ sha256: packed.sha256,
+ })),
+ reason: `Explicit Bun ${host.installArgs.join(' ')}; non-host Devup siblings verified absent`,
+ }
+ console.info(JSON.stringify(receipt))
+ return receipt
+}
diff --git a/e2e/plugins/engine-parity.mjs b/e2e/plugins/engine-parity.mjs
new file mode 100644
index 000000000..188db91a8
--- /dev/null
+++ b/e2e/plugins/engine-parity.mjs
@@ -0,0 +1,1980 @@
+import assert from 'node:assert/strict'
+import { spawn, spawnSync } from 'node:child_process'
+import { createHash } from 'node:crypto'
+import {
+ cpSync,
+ existsSync,
+ lstatSync,
+ mkdirSync,
+ mkdtempSync,
+ readdirSync,
+ readFileSync,
+ readlinkSync,
+ realpathSync,
+ renameSync,
+ statSync,
+ symlinkSync,
+ writeFileSync,
+} from 'node:fs'
+import { createRequire } from 'node:module'
+import { createServer as createPortLease } from 'node:net'
+import { tmpdir } from 'node:os'
+import { isAbsolute, join, relative, resolve, sep } from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+import { deserialize, serialize } from 'node:v8'
+
+import {
+ assertBunDist,
+ assertRawParity,
+ assertRsbuildDist,
+ assertRsbuildParity,
+ assertViteParity,
+ assertWebpackDist,
+ collectViteCases,
+ completedReceipt,
+ errorEvidence,
+ prepareBunFixture,
+ viteRealpath,
+} from './engine-consumer.mjs'
+import { packHostEngines, verifyHostInstall } from './engine-fixtures.mjs'
+
+let artifactRoot
+let invocation = 0
+
+function run(binary, args, options) {
+ console.info(JSON.stringify({ binary, args, cwd: options.cwd }))
+ const { evidenceBase, ...execution } = options
+ const base = evidenceBase ?? join(artifactRoot, `command-${invocation++}`)
+ const result = spawnSync(binary, args, {
+ ...execution,
+ timeout: 600000,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ windowsHide: true,
+ })
+ writeFileSync(`${base}.stdout`, result.stdout ?? Buffer.alloc(0))
+ writeFileSync(`${base}.stderr`, result.stderr ?? Buffer.alloc(0))
+ if (result.error) throw result.error
+ assert.equal(result.status, 0, `Command failed; raw streams: ${base}`)
+ return result.stdout.toString('utf8')
+}
+
+async function viteCells(context) {
+ const { root, relocated, link, bun } = context
+ const lease = createPortLease()
+ const port = await new Promise((resolve, reject) => {
+ lease.once('error', reject)
+ lease.listen(0, '127.0.0.1', () => {
+ const address = lease.address()
+ assert(address && typeof address === 'object')
+ lease.close((error) => (error ? reject(error) : resolve(address.port)))
+ })
+ })
+ const summary = []
+ for (const runtime of [process.execPath, bun]) {
+ const name = runtime === bun ? 'bun' : 'node'
+ const cells = []
+ for (const css of ['per-file', 'single']) {
+ for (const format of ['require', 'import']) {
+ for (const mode of ['default', 'native', 'wasm'])
+ cells.push(['lifecycle', mode, format, css])
+ cells.push(['errors', 'native', format, css])
+ }
+ for (const mode of ['native', 'wasm']) {
+ for (const scenario of ['repeat', 'opposite'])
+ cells.push([scenario, mode, 'import', css])
+ }
+ }
+ for (const format of ['require', 'import']) {
+ cells.push(['guards', 'wasm', format, 'per-file'])
+ for (const mode of ['default', 'native', 'wasm'])
+ cells.push(['late', mode, format, 'per-file'])
+ }
+ cells.push(['guards', 'wasm', 'require', 'alias'])
+ const receipts = new Map()
+ const completed = new Set()
+ const results = await collectViteCases(
+ cells.map((cell) => cell.join('-')),
+ async (id) => {
+ const cell = cells.find((cell) => cell.join('-') === id)
+ const base = join(root, `${name}-${id}`)
+ const destination = `${base}.json`
+ try {
+ const args = [
+ join(link, 'engine-consumer.mjs'),
+ ...cell.slice(0, 3),
+ cell[3] === 'alias' ? 'per-file' : cell[3],
+ destination,
+ 'vite',
+ ]
+ run(runtime, args, {
+ cwd: relocated,
+ evidenceBase: base,
+ env: { ...process.env, DEVUP_ENGINE_VITE_PORT: String(port) },
+ })
+ } finally {
+ if (existsSync(destination))
+ receipts.set(id, JSON.parse(readFileSync(destination, 'utf8')))
+ }
+ assert(
+ receipts.has(id),
+ 'Every alias/cell CLI must execute and save its receipt',
+ )
+ if (cell[0] === 'guards') assert.equal(receipts.get(id).guards, true)
+ completed.add(id)
+ return destination
+ },
+ )
+ const comparisons = cells
+ .filter(([scenario]) =>
+ ['lifecycle', 'repeat', 'opposite'].includes(scenario),
+ )
+ .map((cell) => cell.join('-'))
+ .filter((id) => !id.startsWith('lifecycle-default-require-'))
+ const compared = await collectViteCases(comparisons, async (id) => {
+ const [scenario, mode, , css] = cells.find(
+ (cell) => cell.join('-') === id,
+ )
+ const expected = completedReceipt(
+ receipts,
+ completed,
+ scenario === 'lifecycle'
+ ? `lifecycle-default-require-${css}`
+ : `lifecycle-${mode}-import-${css}`,
+ )
+ assertViteParity(
+ expected,
+ completedReceipt(receipts, completed, id),
+ scenario === 'opposite' && css === 'single',
+ )
+ })
+ const index = (rows, group) =>
+ rows.map((result) => {
+ const diagnostic = join(
+ root,
+ `${name}-${group}-${result.name}.diagnostic.json`,
+ )
+ if (!result.ok)
+ writeFileSync(diagnostic, JSON.stringify(result.error, null, 2))
+ return {
+ name: result.name,
+ ok: result.ok,
+ receipt: result.value,
+ partialReceipt:
+ !result.ok && receipts.has(result.name)
+ ? join(root, `${name}-${result.name}.json`)
+ : undefined,
+ diagnostic: result.ok ? undefined : diagnostic,
+ }
+ })
+ summary.push({
+ runtime: name,
+ passed: [...results, ...compared].filter((cell) => cell.ok).length,
+ failed: [...results, ...compared].filter((cell) => !cell.ok).length,
+ results: index(results, 'execute'),
+ comparisons: index(compared, 'compare'),
+ })
+ writeFileSync(
+ join(root, 'vite-summary.json'),
+ JSON.stringify(summary, null, 2),
+ )
+ }
+ const cuts = join(root, 'dev-cuts')
+ try {
+ run(
+ process.execPath,
+ [join(link, 'engine-dev-cuts.mjs'), 'matrix', root, String(port)],
+ {
+ cwd: relocated,
+ evidenceBase: cuts,
+ },
+ )
+ summary.push({ runtime: 'node', cuts: true })
+ } catch (error) {
+ summary.push({ runtime: 'node', cuts: false, error: errorEvidence(error) })
+ }
+ writeFileSync(
+ join(root, 'vite-summary.json'),
+ JSON.stringify(summary, null, 2),
+ )
+ assert(
+ summary.every(
+ (row) =>
+ row.cuts ??
+ [...row.results, ...row.comparisons].every((cell) => cell.ok),
+ ),
+ 'Vite collect-all diagnostics failed; inspect vite-summary.json and raw cell evidence before one coordinator batch decision',
+ )
+}
+
+async function bunCells({ root, relocated, link, bun }) {
+ const receipts = new Map()
+ const completed = new Set()
+ const cells = []
+ for (const scenario of [
+ 'build',
+ 'repeat',
+ 'opposite',
+ 'runtime',
+ 'preload',
+ 'late',
+ 'errors',
+ ])
+ for (const format of ['require', 'import'])
+ for (const mode of ['default', 'native', 'wasm'])
+ cells.push([scenario, mode, format])
+ for (const format of ['require', 'import'])
+ cells.push(['guards', 'wasm', format])
+ cells.push(['preload-root', 'default', 'import'])
+ const results = await collectViteCases(
+ cells.map((cell) => cell.join('-')),
+ async (id) => {
+ const [scenario, mode, format] = cells.find(
+ (cell) => cell.join('-') === id,
+ )
+ const base = join(root, id)
+ const destination = `${base}.json`
+ const rootPreload = scenario === 'preload-root'
+ prepareBunFixture(relocated, !rootPreload)
+ const args = []
+ if (scenario === 'preload' || rootPreload) {
+ const preload = join(relocated, 'engine-preload.mjs')
+ const select = mode === 'default' ? {} : { engine: mode }
+ const load =
+ format === 'require'
+ ? 'createRequire(import.meta.url)("@devup-ui/bun-plugin/register")'
+ : 'await import("@devup-ui/bun-plugin/register")'
+ writeFileSync(
+ preload,
+ rootPreload
+ ? 'await import("@devup-ui/bun-plugin");'
+ : `import {createRequire} from 'node:module'; const {register}=${load}; await register(${JSON.stringify({ ...select, shorthands: { insetX: ['left', 'right'] } })});`,
+ )
+ args.push('--preload', preload)
+ }
+ args.push(
+ join(link, 'engine-consumer.mjs'),
+ rootPreload ? 'preload' : scenario,
+ mode,
+ format,
+ 'single',
+ destination,
+ 'bun',
+ )
+ try {
+ run(bun, args, { cwd: relocated, evidenceBase: base })
+ } finally {
+ if (existsSync(destination))
+ receipts.set(id, JSON.parse(readFileSync(destination, 'utf8')))
+ }
+ assert(receipts.has(id))
+ if (scenario === 'guards') assert.equal(receipts.get(id).guards, true)
+ completed.add(id)
+ return destination
+ },
+ )
+ const comparisons = await collectViteCases(
+ cells
+ .filter(
+ ([scenario, mode, format]) =>
+ !['guards', 'preload-root'].includes(scenario) &&
+ !(mode === 'default' && format === 'require'),
+ )
+ .map((cell) => cell.join('-')),
+ async (id) => {
+ const [scenario] = cells.find((cell) => cell.join('-') === id)
+ assertRawParity(
+ completedReceipt(receipts, completed, `${scenario}-default-require`),
+ completedReceipt(receipts, completed, id),
+ )
+ },
+ )
+ comparisons.push(
+ ...(await collectViteCases(
+ cells
+ .filter(([scenario]) => scenario === 'opposite')
+ .map((cell) => `admission-${cell.join('-')}`),
+ async (name) => {
+ const id = name.slice('admission-'.length)
+ const [, mode, format] = cells.find((cell) => cell.join('-') === id)
+ const forward = completedReceipt(
+ receipts,
+ completed,
+ `build-${mode}-${format}`,
+ )
+ const reverse = completedReceipt(receipts, completed, id)
+ try {
+ assertRawParity(forward.phases[0], reverse.phases[0])
+ } catch (error) {
+ writeFileSync(
+ join(root, `${name}-raw-difference.json`),
+ JSON.stringify(errorEvidence(error), null, 2),
+ )
+ }
+ // Output::new snapshots the current sheet; Bun publishes via getCss.
+ const publication = (phase) => {
+ const copy = structuredClone(phase)
+ for (const output of Object.values(copy.outputs)) {
+ assert(Object.hasOwn(output, 'css'))
+ delete output.css
+ }
+ return copy
+ }
+ assertRawParity(
+ publication(forward.phases[0]),
+ publication(reverse.phases[0]),
+ )
+ },
+ )),
+ )
+ writeFileSync(
+ join(root, 'bun-summary.json'),
+ JSON.stringify({ results, comparisons }, null, 2),
+ )
+ assert(
+ [...results, ...comparisons].every((row) => row.ok),
+ 'Bun collect-all diagnostics failed; inspect bun-summary.json and raw evidence',
+ )
+}
+
+const nextPreviewKeys = [
+ 'previewModeId',
+ 'previewModeSigningKey',
+ 'previewModeEncryptionKey',
+]
+const nextMetadataFiles = [
+ '.next/cache/.previewinfo',
+ '.next/cache/.rscinfo',
+ '.next/prerender-manifest.json',
+ '.next/server/preview-props.json',
+]
+const nextRouteMetadata =
+ /^\.next\/server\/route-cache\/APP_PAGE\/[a-f0-9]{64}\/\$\/index\.meta$/
+const nextSelectorManifest = '.next/required-server-files.json'
+const nextSelectorScript = '.next/required-server-files.js'
+const nextSelectorPrefix = 'self.__SERVER_FILES_MANIFEST='
+const nextTurboStorage = '.next/cache/turbopack/v16.4.4-b175f52b/'
+const nextStrictWebpackCache =
+ '.next/cache/webpack/edge-server-production/0.pack'
+
+function nextStoredContent(file) {
+ return (
+ nextMetadataFiles.includes(file) ||
+ nextRouteMetadata.test(file) ||
+ [nextSelectorManifest, 'df/setup.bin'].includes(file)
+ )
+}
+
+function nextFileContent(record) {
+ assert.equal(typeof record.content, 'string')
+ const bytes = Buffer.from(record.content, 'base64')
+ assert.equal(bytes.toString('base64'), record.content)
+ assert.equal(record.bytes, bytes.length)
+ assert.equal(record.sha256, createHash('sha256').update(bytes).digest('hex'))
+ return bytes
+}
+
+function nextSelectors(value, engine, kind) {
+ const paths = []
+ const visit = (object, path = []) => {
+ if (!object || typeof object !== 'object') return
+ for (const [key, child] of Object.entries(object)) {
+ const at = [...path, key]
+ if (key === 'engine') {
+ assert.equal(
+ child,
+ engine,
+ `Wrong requested Next selector ${at.join('/')}`,
+ )
+ paths.push(at.join('/'))
+ object[key] = ''
+ } else visit(child, at)
+ }
+ }
+ visit(value)
+ const prefix = kind === 'manifest' ? 'config/turbopack/rules/' : ''
+ assert.deepEqual(
+ paths.sort(),
+ kind === 'key'
+ ? ['engine']
+ : [
+ `${prefix}./df/devup-ui/*.css/0/options/engine`,
+ `${prefix}*.{tsx,ts,jsx,js,mjs}/loaders/0/options/engine`,
+ ].sort(),
+ )
+ return value
+}
+
+function nextSelectorText(text, engine, kind) {
+ nextSelectors(JSON.parse(text), engine, kind)
+ let count = 0
+ const replacement = text.replace(
+ /("engine"\s*:\s*")(native|wasm)(")/g,
+ (_, before, value, after) => {
+ assert.equal(value, engine)
+ count++
+ return `${before}${after}`
+ },
+ )
+ assert.equal(count, kind === 'key' ? 1 : 2)
+ assert.equal(
+ Buffer.byteLength(replacement),
+ Buffer.byteLength(text) +
+ count * (''.length - engine.length),
+ )
+ return replacement
+}
+
+function nextPublicationView(publication) {
+ const { receipt, turbo, mode, wallStarted, wallFinished, servedAt } =
+ publication
+ assert(['default', 'native', 'wasm'].includes(mode))
+ assert(wallStarted <= wallFinished && wallFinished <= servedAt)
+ const engine = mode === 'wasm' ? 'wasm' : 'native'
+ const view = structuredClone(receipt)
+ const { files, compiler } = view
+ for (const [file, record] of Object.entries(files)) {
+ assert.deepEqual(
+ Object.keys(record).sort(),
+ nextStoredContent(file)
+ ? ['bytes', 'content', 'sha256']
+ : ['bytes', 'sha256'],
+ )
+ }
+ assert(Number.isSafeInteger(compiler.pid) && compiler.pid > 0)
+ const pid = compiler.pid
+ delete compiler.pid
+ if (!turbo) {
+ const groups = new Map()
+ for (const output of compiler.outputs) {
+ const input = JSON.stringify(output.input)
+ if (!groups.has(input)) groups.set(input, [])
+ groups.get(input).push(output)
+ }
+ compiler.outputs = [...groups].sort(([a], [b]) => a.localeCompare(b))
+ }
+ const administrative = turbo
+ ? [
+ ...Array.from(
+ { length: 14 },
+ (_, index) =>
+ `${nextTurboStorage}${String(index + 1).padStart(8, '0')}.sst`,
+ ),
+ ...Array.from(
+ { length: 4 },
+ (_, index) =>
+ `${nextTurboStorage}${String(index + 15).padStart(8, '0')}.meta`,
+ ),
+ `${nextTurboStorage}CURRENT`,
+ `${nextTurboStorage}LOG`,
+ ]
+ : [
+ 'client-production/0.pack',
+ 'client-production/index.pack',
+ 'edge-server-production/index.pack',
+ 'server-production/0.pack',
+ 'server-production/index.pack',
+ ].map((file) => `.next/cache/webpack/${file}`)
+ assert.deepEqual(
+ Object.keys(files)
+ .filter((file) =>
+ file.startsWith(
+ turbo ? '.next/cache/turbopack/' : '.next/cache/webpack/',
+ ),
+ )
+ .sort(),
+ (turbo
+ ? [...administrative]
+ : [...administrative, nextStrictWebpackCache]
+ ).sort(),
+ )
+ for (const file of [...administrative, '.next/trace', '.next/trace-build']) {
+ assert(
+ Object.hasOwn(files, file),
+ `Missing named Next administrative file ${file}`,
+ )
+ assert.deepEqual(Object.keys(files[file]).sort(), ['bytes', 'sha256'])
+ assert(Number.isSafeInteger(files[file].bytes) && files[file].bytes > 0)
+ assert.match(files[file].sha256, /^[a-f0-9]{64}$/)
+ // Raw binary storage remains retained and explicitly unqualified.
+ files[file] = { administrative: true }
+ }
+ const metadata = [
+ ...nextMetadataFiles,
+ ...Object.keys(files).filter((file) => nextRouteMetadata.test(file)),
+ ]
+ assert.equal(metadata.length, 5)
+ const parsed = Object.fromEntries(
+ metadata.map((file) => [
+ file,
+ JSON.parse(nextFileContent(files[file]).toString('utf8')),
+ ]),
+ )
+ const preview = parsed[nextMetadataFiles[0]]
+ for (const [index, key] of nextPreviewKeys.entries()) {
+ assert.match(
+ preview[key],
+ new RegExp(`^[a-f0-9]{${index === 0 ? 32 : 64}}$`),
+ )
+ for (const copy of [
+ parsed['.next/prerender-manifest.json'].preview,
+ parsed['.next/server/preview-props.json'],
+ ]) {
+ assert.equal(copy[key], preview[key])
+ delete copy[key]
+ }
+ delete preview[key]
+ }
+ const expiry = (object, key) => {
+ assert(Number.isSafeInteger(object[key]) && object[key] > wallFinished)
+ delete object[key]
+ }
+ expiry(preview, 'expireAt')
+ expiry(parsed['.next/cache/.rscinfo'], 'encryption.expire_at')
+ assert.equal(
+ parsed['.next/cache/.rscinfo']['encryption.key'],
+ Buffer.alloc(32, 7).toString('base64'),
+ )
+ const route = parsed[metadata[4]]
+ assert(
+ Number.isSafeInteger(route.routeCacheLastModified) &&
+ route.routeCacheLastModified >= wallStarted &&
+ route.routeCacheLastModified <= wallFinished,
+ )
+ delete route.routeCacheLastModified
+ for (const file of metadata) {
+ delete files[file].sha256
+ delete files[file].content
+ files[file].parsed = parsed[file]
+ }
+ if (turbo) {
+ const setupFile = files['df/setup.bin']
+ const setup = deserialize(nextFileContent(setupFile))
+ const numeric = '(\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?)'
+ const owner = new RegExp(`^${numeric}-${numeric}$`).exec(setup.owner)
+ const token = new RegExp(`^([1-9]\\d*)-${numeric}-${numeric}$`).exec(
+ setup.token,
+ )
+ assert(owner && token, 'Missing or invalid actual Next setup ownership')
+ for (const text of [...owner.slice(1), ...token.slice(1)]) {
+ assert(Number.isFinite(Number(text)) && String(Number(text)) === text)
+ }
+ assert.equal(Number(token[1]), pid)
+ const origin = Number(owner[1])
+ assert(origin >= wallStarted && origin <= wallFinished)
+ assert(Number(token[2]) >= 0 && origin + Number(token[2]) <= wallFinished)
+ for (const random of [Number(owner[2]), Number(token[3])])
+ assert(random >= 0 && random < 1)
+ delete setup.owner
+ delete setup.token
+ setup.key = nextSelectorText(setup.key, engine, 'key')
+ nextSelectors(setup.rules, engine, 'rules')
+ delete setupFile.sha256
+ delete setupFile.bytes
+ setupFile.content = serialize(setup).toString('base64')
+ const manifest = files[nextSelectorManifest]
+ const rawManifest = nextFileContent(manifest).toString('utf8')
+ const script = files[nextSelectorScript]
+ const rawScript = Buffer.from(nextSelectorPrefix + rawManifest)
+ assert.deepEqual(script, {
+ bytes: rawScript.length,
+ sha256: createHash('sha256').update(rawScript).digest('hex'),
+ })
+ const text = nextSelectorText(rawManifest, engine, 'manifest')
+ manifest.content = text
+ manifest.bytes = Buffer.byteLength(text)
+ manifest.sha256 = createHash('sha256').update(text).digest('hex')
+ const projectedScript = Buffer.from(nextSelectorPrefix + text)
+ script.bytes = projectedScript.length
+ script.sha256 = createHash('sha256').update(projectedScript).digest('hex')
+ const coordinator = view.coordinator
+ assert.equal(coordinator.pid, pid)
+ assert.match(coordinator.portFile, /^[1-9]\d*\n[1-9]\d*$/)
+ const [port, ownerPid] = coordinator.portFile.split('\n').map(Number)
+ assert(port < 65536 && Number.isSafeInteger(port))
+ assert.equal(ownerPid, pid)
+ // The owning compiler removes its port file on exit; the hook saved it live.
+ assert(!Object.hasOwn(files, 'df/coordinator.port'))
+ delete coordinator.pid
+ delete coordinator.portFile
+ assert.equal(coordinator.requests.length, 2)
+ for (const request of coordinator.requests) {
+ const origin = `http://127.0.0.1:${port}`
+ assert(request.url.startsWith(`${origin}/css?`))
+ request.url = request.url.slice(origin.length)
+ assert.equal(request.status, 200)
+ assert.equal(
+ request.body,
+ Buffer.from(request.liveCss).toString('base64'),
+ )
+ assert.equal(
+ request.liveCss,
+ request.fileNum === null
+ ? compiler.state.css
+ : compiler.fileCss[request.fileNum],
+ )
+ }
+ }
+ const headerView = (response, staticFile = false) => {
+ const headers = response.headers
+ assert.equal(typeof headers.date, 'string')
+ const date = Date.parse(headers.date)
+ assert.equal(new Date(date).toUTCString(), headers.date)
+ assert(date >= wallStarted - 1000 && date <= servedAt + 1000)
+ delete headers.date
+ if (staticFile) {
+ const { size, mtimeMs, mtimeTime } = response.fileStat
+ assert(Number.isSafeInteger(size) && size > 0 && Number.isFinite(mtimeMs))
+ assert(Number.isSafeInteger(mtimeTime))
+ // Node fs.Stats constructs its Date by rounding fractional milliseconds.
+ assert.equal(mtimeTime, Math.round(mtimeMs))
+ const mtime = new Date(mtimeTime)
+ assert.equal(
+ headers.etag,
+ `W/"${size.toString(16)}-${mtime.getTime().toString(16)}"`,
+ )
+ assert.equal(headers['last-modified'], mtime.toUTCString())
+ delete headers.etag
+ delete headers['last-modified']
+ assert.deepEqual(Object.keys(response.fileStat).sort(), [
+ 'mtimeMs',
+ 'mtimeTime',
+ 'size',
+ ])
+ delete response.fileStat
+ }
+ }
+ if (turbo)
+ for (const request of view.coordinator.requests) headerView(request)
+ for (const [url, response] of Object.entries(view.browser.network))
+ headerView(
+ response,
+ /\/_next\/static\/.*\.(?:js|css)$/.test(new URL(url).pathname),
+ )
+ return view
+}
+
+export function assertNextPublication(expected, actual) {
+ const before = [expected, actual].map((item) => JSON.stringify(item))
+ try {
+ assert.deepEqual(nextPublicationView(actual), nextPublicationView(expected))
+ } finally {
+ assert.deepEqual(
+ [expected, actual].map((item) => JSON.stringify(item)),
+ before,
+ )
+ }
+}
+
+export function nextComparisonControls(publication) {
+ const original = JSON.stringify(publication)
+ assertNextPublication(publication, publication)
+ const replaceFile = (copy, file, transform, binary = false) => {
+ const record = copy.receipt.files[file]
+ const bytes = nextFileContent(record)
+ const value = binary ? deserialize(bytes) : JSON.parse(bytes.toString())
+ transform(value)
+ const changed = binary
+ ? serialize(value)
+ : Buffer.from(JSON.stringify(value))
+ record.content = changed.toString('base64')
+ record.bytes = changed.length
+ record.sha256 = createHash('sha256').update(changed).digest('hex')
+ }
+ const rejected = []
+ const reject = (name, mutate) => {
+ const copy = structuredClone(publication)
+ mutate(copy)
+ assert.throws(
+ () => assertNextPublication(publication, copy),
+ undefined,
+ name,
+ )
+ rejected.push(name)
+ assert.equal(JSON.stringify(publication), original)
+ }
+ for (const extension of ['.css', '.js', '.map'])
+ reject(`product ${extension}`, (copy) => {
+ const file = Object.keys(copy.receipt.files).find(
+ (file) => file.startsWith('.next/') && file.endsWith(extension),
+ )
+ assert(file)
+ copy.receipt.files[file].sha256 = '0'.repeat(64)
+ })
+ reject('df product bytes', (copy) => {
+ const file = Object.keys(copy.receipt.files).find(
+ (file) =>
+ file.startsWith('df/') &&
+ !['df/setup.bin', 'df/coordinator.port'].includes(file),
+ )
+ assert(file)
+ copy.receipt.files[file].sha256 = '0'.repeat(64)
+ })
+ for (const field of [
+ 'code',
+ 'css',
+ 'cssFile',
+ 'map',
+ 'dependencies',
+ 'updatedBaseStyle',
+ ])
+ reject(`whole getter ${field}`, (copy) => {
+ copy.receipt.compiler.outputs[0].output[field] = { changed: true }
+ })
+ for (const field of Object.keys(publication.receipt.compiler.state))
+ reject(`state ${field}`, (copy) => {
+ copy.receipt.compiler.state[field] = { changed: true }
+ })
+ reject('calls', (copy) => {
+ copy.receipt.compiler.calls.codeExtract++
+ })
+ reject('live file CSS', (copy) => {
+ const file = Object.keys(copy.receipt.compiler.fileCss)[0]
+ copy.receipt.compiler.fileCss[file] += '/* changed */'
+ })
+ reject('unknown compiler field', (copy) => {
+ copy.receipt.compiler.unknown = true
+ })
+ reject('unknown JSON metadata field', (copy) =>
+ replaceFile(copy, nextMetadataFiles[0], (value) => {
+ value.unknown = true
+ }),
+ )
+ reject('missing approved metadata leaf', (copy) =>
+ replaceFile(copy, nextMetadataFiles[0], (value) => {
+ delete value.expireAt
+ }),
+ )
+ reject('preview copy mismatch', (copy) =>
+ replaceFile(copy, '.next/server/preview-props.json', (value) => {
+ value.previewModeId = '0'.repeat(32)
+ }),
+ )
+ reject('missing named storage file', (copy) => {
+ const file = Object.keys(copy.receipt.files).find((file) =>
+ /\.(?:sst|pack)$/.test(file),
+ )
+ assert(file)
+ delete copy.receipt.files[file]
+ })
+ reject('artifact path', (copy) => {
+ const [file] = Object.keys(copy.receipt.files)
+ copy.receipt.files[`${file}.changed`] = copy.receipt.files[file]
+ delete copy.receipt.files[file]
+ })
+ const networkUrl = Object.keys(publication.receipt.browser.network)[0]
+ reject('unknown HTTP header', (copy) => {
+ copy.receipt.browser.network[networkUrl].headers['x-unknown'] = 'changed'
+ })
+ reject('missing HTTP date', (copy) => {
+ delete copy.receipt.browser.network[networkUrl].headers.date
+ })
+ reject('HTTP body', (copy) => {
+ copy.receipt.browser.network[networkUrl].body += 'YQ=='
+ })
+ reject('computed browser style', (copy) => {
+ copy.receipt.browser.computed.A.width = '99px'
+ })
+ reject('published CSS/sentinel', (copy) => {
+ copy.receipt.publishedCss['devup-ui.css'] += '\n/* 1 */'
+ })
+ const staticUrl = Object.keys(publication.receipt.browser.network).find(
+ (url) => publication.receipt.browser.network[url].fileStat,
+ )
+ assert(staticUrl)
+ reject('unbound static ETag', (copy) => {
+ copy.receipt.browser.network[staticUrl].headers.etag = 'W/"1-1"'
+ })
+ reject('corrupted actual stat Date', (copy) => {
+ copy.receipt.browser.network[staticUrl].fileStat.mtimeTime++
+ })
+ if (!publication.turbo)
+ reject('strict unchanged sixth Webpack pack', (copy) => {
+ copy.receipt.files[nextStrictWebpackCache].sha256 = '0'.repeat(64)
+ })
+ const administrative = Object.keys(publication.receipt.files).find((file) =>
+ /\.(?:sst|pack)$/.test(file),
+ )
+ const allowed = structuredClone(publication)
+ allowed.receipt.files[administrative].sha256 = '0'.repeat(64)
+ assertNextPublication(publication, allowed)
+ const allowedPreview = structuredClone(publication)
+ for (const file of [
+ nextMetadataFiles[0],
+ '.next/prerender-manifest.json',
+ '.next/server/preview-props.json',
+ ]) {
+ const record = allowedPreview.receipt.files[file]
+ let text = nextFileContent(record).toString()
+ const value = JSON.parse(text)
+ const keys =
+ file === '.next/prerender-manifest.json' ? value.preview : value
+ for (const [index, key] of nextPreviewKeys.entries())
+ text = text.replace(
+ JSON.stringify(keys[key]),
+ JSON.stringify('1'.repeat(index === 0 ? 32 : 64)),
+ )
+ const bytes = Buffer.from(text)
+ record.content = bytes.toString('base64')
+ record.bytes = bytes.length
+ record.sha256 = createHash('sha256').update(bytes).digest('hex')
+ }
+ assertNextPublication(publication, allowedPreview)
+ const unknownStorage = structuredClone(publication)
+ unknownStorage.receipt.files[`${administrative}.unexpected`] = {
+ bytes: 1,
+ sha256: '0'.repeat(64),
+ }
+ assert.throws(() => assertNextPublication(unknownStorage, unknownStorage))
+ if (publication.turbo) {
+ for (const mutation of [
+ 'missing',
+ 'size',
+ 'mirror',
+ 'wrapper',
+ 'suffix',
+ 'unknown',
+ 'third-selector',
+ ])
+ reject(`JS selector copy ${mutation}`, (copy) => {
+ if (mutation === 'missing') {
+ delete copy.receipt.files[nextSelectorScript]
+ return
+ }
+ const record = copy.receipt.files[nextSelectorManifest]
+ let text = nextFileContent(record).toString('utf8')
+ if (mutation === 'unknown' || mutation === 'third-selector') {
+ text = text.replace(
+ '{',
+ mutation === 'unknown'
+ ? '{"unknown":true,'
+ : '{"extra":{"engine":"native"},',
+ )
+ const bytes = Buffer.from(text)
+ record.content = bytes.toString('base64')
+ record.bytes = bytes.length
+ record.sha256 = createHash('sha256').update(bytes).digest('hex')
+ }
+ const bytes = Buffer.from(
+ (mutation === 'wrapper'
+ ? 'window.__SERVER_FILES_MANIFEST='
+ : nextSelectorPrefix) +
+ (mutation === 'mirror' ? ' ' : '') +
+ text +
+ (mutation === 'suffix' ? ';unexpected' : ''),
+ )
+ copy.receipt.files[nextSelectorScript] = {
+ bytes: bytes.length + (mutation === 'size' ? 1 : 0),
+ sha256: createHash('sha256').update(bytes).digest('hex'),
+ }
+ })
+ for (const kind of ['key', 'rules', 'manifest'])
+ for (const mutation of ['wrong', 'missing', 'extra'])
+ reject(`${kind} selector ${mutation}`, (copy) => {
+ const file =
+ kind === 'manifest' ? nextSelectorManifest : 'df/setup.bin'
+ replaceFile(
+ copy,
+ file,
+ (value) => {
+ const object =
+ kind === 'key'
+ ? JSON.parse(value.key)
+ : kind === 'rules'
+ ? value.rules
+ : value.config.turbopack.rules
+ const selected =
+ kind === 'key'
+ ? object
+ : object['./df/devup-ui/*.css'][0].options
+ if (mutation === 'wrong') selected.engine = 'wasm'
+ if (mutation === 'missing') delete selected.engine
+ if (mutation === 'extra') object.extra = { engine: 'native' }
+ if (kind === 'key') value.key = JSON.stringify(object)
+ },
+ kind !== 'manifest',
+ )
+ })
+ reject('setup default theme', (copy) =>
+ replaceFile(
+ copy,
+ 'df/setup.bin',
+ (value) => {
+ value.defaultTheme = 'changed'
+ },
+ true,
+ ),
+ )
+ reject('setup unknown rule', (copy) =>
+ replaceFile(
+ copy,
+ 'df/setup.bin',
+ (value) => {
+ value.rules.unknown = {}
+ },
+ true,
+ ),
+ )
+ reject('setup ownership PID', (copy) =>
+ replaceFile(
+ copy,
+ 'df/setup.bin',
+ (value) => {
+ value.token = `1-${value.token.split('-').slice(1).join('-')}`
+ },
+ true,
+ ),
+ )
+ reject('coordinator request target', (copy) => {
+ copy.receipt.coordinator.requests[0].url += '&unknown=1'
+ })
+ reject('coordinator live CSS', (copy) => {
+ copy.receipt.coordinator.requests[0].liveCss += 'x'
+ })
+ reject('coordinator PID binding', (copy) => {
+ copy.receipt.coordinator.pid++
+ })
+ reject('Turbo global extraction order', (copy) => {
+ copy.receipt.compiler.outputs.reverse()
+ })
+ }
+ assert.equal(JSON.stringify(publication), original)
+ return { rejected, rawUnchanged: true, administrativeOnlyPositive: true }
+}
+
+async function nextPublicationCells({ root, relocated, link }) {
+ assert(
+ [undefined, 'webpack', 'turbopack'].includes(
+ process.env.DEVUP_NEXT_BUNDLER,
+ ),
+ )
+ const bundlers = process.env.DEVUP_NEXT_BUNDLER
+ ? [process.env.DEVUP_NEXT_BUNDLER]
+ : ['webpack', 'turbopack']
+ const cells = []
+ for (const bundler of bundlers)
+ for (const singleCss of [false, true])
+ for (const format of ['require', 'import'])
+ for (const mode of ['default', 'native', 'wasm']) {
+ cells.push({ bundler, singleCss, format, mode, repeat: false })
+ if (format === 'import' && mode !== 'default')
+ cells.push({ bundler, singleCss, format, mode, repeat: true })
+ }
+ const require = createRequire(join(relocated, 'package.json'))
+ const { chromium } = createRequire(
+ join(fileURLToPath(new URL('../../', import.meta.url)), 'package.json'),
+ )('@playwright/test')
+ const portLease = createPortLease()
+ const port = await new Promise((resolve, reject) => {
+ portLease.once('error', reject)
+ portLease.listen(0, '127.0.0.1', () => {
+ const address = portLease.address()
+ assert(address && typeof address === 'object')
+ portLease.close((error) =>
+ error ? reject(error) : resolve(address.port),
+ )
+ })
+ })
+ const put = (file, text) => {
+ mkdirSync(join(relocated, file, '..'), { recursive: true })
+ writeFileSync(join(relocated, file), text)
+ }
+ put('src/tokens.js', "export const tone = 'teal';")
+ put(
+ 'src/A.jsx',
+ "import {Box} from '@devup-ui/react'; import {tone} from './tokens'; export default function A(){return ;}",
+ )
+ put(
+ 'src/B.jsx',
+ "import {Box} from '@devup-ui/react'; export default function B(){return ;}",
+ )
+ put(
+ 'src/Unused.jsx',
+ "import {Box} from '@devup-ui/react'; export const unused=;",
+ )
+ put(
+ 'src/app/page.jsx',
+ "import A from '../A'; import B from '../B'; export default function Page(){return <>>;}",
+ )
+ put(
+ 'src/app/layout.jsx',
+ 'export default function Layout({children}){return {children};}',
+ )
+ put(
+ 'devup.json',
+ JSON.stringify({ theme: { length: { default: { size: ['17px'] } } } }),
+ )
+ const next = require.resolve('next/dist/bin/next')
+ const receipts = [],
+ costs = []
+ const env = {
+ ...process.env,
+ NODE_ENV: 'production',
+ NEXT_TELEMETRY_DISABLED: '1',
+ NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: Buffer.alloc(32, 7).toString('base64'),
+ }
+ delete env.TURBOPACK
+ const rawFiles = () =>
+ Object.fromEntries(
+ ['df', '.next']
+ .flatMap((folder) =>
+ readdirSync(join(relocated, folder), {
+ recursive: true,
+ withFileTypes: true,
+ })
+ .filter((entry) => entry.isFile())
+ .map((entry) => {
+ const file = join(entry.parentPath, entry.name),
+ bytes = readFileSync(file),
+ name = `${folder}/${relative(join(relocated, folder), file).split(sep).join('/')}`
+ return [
+ name,
+ {
+ bytes: bytes.length,
+ sha256: createHash('sha256').update(bytes).digest('hex'),
+ ...(nextStoredContent(name)
+ ? { content: bytes.toString('base64') }
+ : {}),
+ },
+ ]
+ }),
+ )
+ .sort(([a], [b]) => a.localeCompare(b)),
+ )
+ for (const cell of cells) {
+ const { bundler, singleCss, format, mode, repeat } = cell
+ const turbo = bundler === 'turbopack'
+ const phase = `${bundler}-${singleCss ? 'single' : 'per-file'}-${format}-${mode}-${repeat ? 'repeat' : 'first'}`
+ const capture = join(root, `next-${phase}-compiler.json`)
+ const rootImport =
+ format === 'require'
+ ? "import {createRequire} from 'node:module'; const {DevupUI}=createRequire(import.meta.url)('@devup-ui/next-plugin');"
+ : "import {DevupUI} from '@devup-ui/next-plugin';"
+ const options = {
+ ...(mode === 'default' ? {} : { engine: mode }),
+ singleCss,
+ debug: false,
+ sourceMap: true,
+ shorthands: { insetX: ['left', 'right'] },
+ }
+ put(
+ 'next.config.mjs',
+ `${rootImport} ${turbo ? `import {captureNextCoordinator} from ${JSON.stringify(pathToFileURL(join(link, 'engine-consumer.mjs')).href)};` : ''} export default DevupUI({productionBrowserSourceMaps:true,generateBuildId:async()=> 'devup-next-repeat',experimental:{webpackBuildWorker:false}${turbo ? `,compiler:{runAfterProductionCompile:async()=>{await captureNextCoordinator(${JSON.stringify({ directory: relocated, mode, singleCss, destination: `${capture}.coordinator.json` })})}}` : ''}}, ${JSON.stringify(options)});`,
+ )
+ put(
+ 'next-capture.mjs',
+ `import {captureNextCompiler} from ${JSON.stringify(pathToFileURL(join(link, 'engine-consumer.mjs')).href)}; await captureNextCompiler(${JSON.stringify({ directory: relocated, mode, destination: capture })});`,
+ )
+ const started = performance.now(),
+ wallStarted = Date.now()
+ run(
+ process.execPath,
+ [
+ '--import',
+ pathToFileURL(join(relocated, 'next-capture.mjs')).href,
+ next,
+ 'build',
+ turbo ? '--turbopack' : '--webpack',
+ ],
+ { cwd: relocated, env, evidenceBase: join(root, `next-${phase}-build`) },
+ )
+ const wallFinished = Date.now()
+ costs.push({
+ phase,
+ buildMs: performance.now() - started,
+ wallStarted,
+ wallFinished,
+ })
+ const compiler = JSON.parse(readFileSync(capture, 'utf8'))
+ assert.equal(compiler.exitCode, 0)
+ assert(
+ compiler.outputs.length > 0,
+ 'Actual CLI compiler process was not captured',
+ )
+ assert(
+ compiler.calls.codeExtract + compiler.calls.codeExtractWithoutSourceMap >
+ 0,
+ )
+ assert.deepEqual(compiler.unselectedAfter, compiler.unselectedBefore)
+ assert(
+ compiler.calls.registerShorthands > 0 && compiler.calls.registerTheme > 0,
+ )
+ assert.notDeepEqual(compiler.state, compiler.unselectedBefore)
+ assert.equal(compiler.state.residual, 0)
+ const unusedOutputs = compiler.outputs.filter(
+ ({ input }) => input[0] === 'src/Unused.jsx',
+ )
+ assert(unusedOutputs.length > 0)
+ const unusedScope = singleCss ? '' : 'src/Unused.jsx'
+ const alphabet = 'abcdefghijklmnopqrstuvwxyz_'
+ const unusedFile = JSON.parse(compiler.state.files)['src/Unused.jsx']
+ if (!singleCss)
+ assert(
+ Number.isInteger(unusedFile) &&
+ unusedFile >= 0 &&
+ unusedFile < alphabet.length,
+ )
+ const unusedIdentity = singleCss
+ ? 'color-0-magenta--255'
+ : turbo
+ ? `color-0-magenta--255-${alphabet[unusedFile]}`
+ : `a1-l-${Buffer.from(unusedScope).toString('hex')}-636f6c6f72-0-s-6d6167656e7461-6e2d6e-255`
+ const unusedSlot = JSON.parse(compiler.state.classes)[unusedScope][
+ unusedIdentity
+ ]
+ assert(
+ Number.isInteger(unusedSlot) &&
+ unusedSlot >= 0 &&
+ unusedSlot < alphabet.length,
+ )
+ const unusedClass = `${singleCss ? '' : `${alphabet[unusedFile]}-`}${alphabet[unusedSlot]}`
+ const unusedRule = `.${unusedClass}{color:magenta}`
+ for (const { output } of unusedOutputs) {
+ assert.equal(/\bclassName="([^"]+)"/.exec(output.code)?.[1], unusedClass)
+ assert(output.css.includes(unusedRule))
+ }
+ const assertUnusedCss = (css, expectedRule = unusedRule) => {
+ if (singleCss) {
+ assert(
+ css.includes(expectedRule),
+ 'Single CSS must retain the prewarmed unused atom',
+ )
+ assert.doesNotMatch(css.replaceAll(expectedRule, ''), /magenta|#f0f/)
+ } else assert.doesNotMatch(css, /magenta|#f0f/)
+ }
+ assertUnusedCss(compiler.state.css)
+ assert.throws(() =>
+ assertUnusedCss(
+ singleCss
+ ? compiler.state.css.replaceAll(unusedRule, '')
+ : compiler.state.css + unusedRule,
+ ),
+ )
+ assert.throws(() =>
+ assertUnusedCss(compiler.state.css + '.unexpected{color:magenta}'),
+ )
+ assert.throws(() =>
+ assertUnusedCss(compiler.state.css + '.unexpected{color:#f0f}'),
+ )
+ const coordinator = turbo
+ ? JSON.parse(readFileSync(`${capture}.coordinator.json`, 'utf8'))
+ : undefined
+ if (coordinator) {
+ assert.equal(coordinator.requests.length, 2)
+ for (const request of coordinator.requests)
+ assert.equal(
+ request.liveCss,
+ request.fileNum === null
+ ? compiler.state.css
+ : compiler.fileCss[request.fileNum],
+ )
+ }
+ const cacheDirectory = join(
+ relocated,
+ turbo ? 'df/devup-ui' : '.next/cache/devup-ui',
+ )
+ const publishedCss = Object.fromEntries(
+ readdirSync(cacheDirectory)
+ .filter((name) => name.endsWith('.css'))
+ .map((name) => [
+ name,
+ readFileSync(join(cacheDirectory, name), 'utf8'),
+ ]),
+ )
+ const diskMain = publishedCss['devup-ui.css'],
+ liveMain = compiler.state.css
+ let mainDiskWitness
+ if (turbo && diskMain !== liveMain) {
+ assert(diskMain.startsWith(liveMain))
+ const match = /^\n\/\* ([1-9]\d*) \*\/$/.exec(
+ diskMain.slice(liveMain.length),
+ )
+ assert(match, 'Unknown Turbo main CSS suffix')
+ const timestamp = Number(match[1])
+ assert(
+ Number.isSafeInteger(timestamp) &&
+ timestamp >= wallStarted &&
+ timestamp <= wallFinished,
+ )
+ mainDiskWitness = { variant: 'timestamp-sentinel', timestamp }
+ } else {
+ assert.equal(diskMain, liveMain)
+ mainDiskWitness = { variant: 'bare' }
+ }
+ for (const [name, css] of Object.entries(publishedCss)) {
+ if (name === 'devup-ui.css') continue
+ const match = /^devup-ui-(\d+)\.css$/.exec(name)
+ assert(match, `Unexpected published CSS file: ${name}`)
+ assert.equal(css, compiler.fileCss[match[1]])
+ }
+ assert.match(
+ (coordinator
+ ? coordinator.requests.map((request) => request.liveCss)
+ : Object.values(publishedCss)
+ ).join('\n'),
+ /color:teal/,
+ )
+ assertUnusedCss(Object.values(publishedCss).join('\n'))
+ if (coordinator)
+ assertUnusedCss(
+ coordinator.requests.map((request) => request.liveCss).join('\n'),
+ )
+ const server = spawn(
+ process.execPath,
+ [next, 'start', '--hostname', '127.0.0.1', '--port', String(port)],
+ {
+ cwd: relocated,
+ env,
+ windowsHide: true,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ },
+ )
+ const stdout = [],
+ stderr = []
+ server.stdout.on('data', (data) => stdout.push(data))
+ server.stderr.on('data', (data) => stderr.push(data))
+ let serverFailure
+ const serverExit = new Promise((resolve) => {
+ server.once('exit', (code, signal) => resolve({ code, signal }))
+ server.once('error', (error) => {
+ serverFailure = error
+ stderr.push(Buffer.from(String(error)))
+ resolve({ error: errorEvidence(error) })
+ })
+ })
+ let browser, browserReceipt
+ const readinessErrors = []
+ try {
+ const deadline = performance.now() + 60000
+ let ready = false
+ while (
+ performance.now() < deadline &&
+ server.exitCode === null &&
+ !server.killed &&
+ !serverFailure
+ ) {
+ try {
+ const response = await fetch(`http://127.0.0.1:${port}`, {
+ signal: AbortSignal.timeout(1000),
+ })
+ if (response.status === 200) {
+ await response.arrayBuffer()
+ ready = true
+ break
+ }
+ } catch (error) {
+ readinessErrors.push(errorEvidence(error))
+ }
+ await new Promise((resolve) => setTimeout(resolve, 50))
+ }
+ assert(ready, 'Actual Next start never served HTTP200')
+ browser = await chromium.launch({ headless: true })
+ const page = await browser.newPage(),
+ pending = [],
+ network = {},
+ networkErrors = []
+ page.on('response', (response) =>
+ pending.push(
+ (async () => {
+ const url = response.url()
+ if (!url.startsWith(`http://127.0.0.1:${port}/`)) return
+ network[url] = {
+ status: response.status(),
+ headers: await response.allHeaders(),
+ body: (await response.body()).toString('base64'),
+ }
+ })().catch((error) => {
+ networkErrors.push(error)
+ }),
+ ),
+ )
+ await page.goto(`http://127.0.0.1:${port}`, {
+ waitUntil: 'load',
+ timeout: 60000,
+ })
+ const computed = await page.evaluate(() =>
+ Object.fromEntries(
+ ['A', 'B'].map((name) => {
+ const element = document.querySelector(`[data-testid="${name}"]`),
+ style = element && getComputedStyle(element)
+ if (!element || !style) throw new Error(`Missing ${name}`)
+ const rules = []
+ const visit = (items) => {
+ for (const rule of items) {
+ if (
+ 'selectorText' in rule &&
+ element.matches(rule.selectorText)
+ )
+ rules.push({
+ selector: rule.selectorText,
+ left: rule.style.getPropertyValue('left'),
+ right: rule.style.getPropertyValue('right'),
+ })
+ if ('cssRules' in rule) visit(rule.cssRules)
+ }
+ }
+ for (const sheet of document.styleSheets) visit(sheet.cssRules)
+ return [
+ name,
+ {
+ className: element.className,
+ color: style.color,
+ background: style.backgroundColor,
+ width: style.width,
+ height: style.height,
+ padding: style.paddingTop,
+ rules,
+ },
+ ]
+ }),
+ ),
+ )
+ for (const name of ['A', 'B']) {
+ assert(computed[name].className)
+ assert(computed[name].rules.some((rule) => rule.left === '19px'))
+ assert(computed[name].rules.some((rule) => rule.right === '19px'))
+ }
+ assert.equal(computed.A.color, 'rgb(0, 128, 128)')
+ assert.equal(computed.B.background, 'rgb(0, 0, 128)')
+ assert.equal(computed.A.width, '17px')
+ assert.equal(computed.B.height, '17px')
+ assert.equal(computed.A.padding, '1px')
+ assert.equal(computed.B.padding, '2px')
+ const unusedElements = await page.locator(`.${unusedClass}`).count()
+ assert.equal(unusedElements, 0)
+ await Promise.all(pending)
+ if (networkErrors.length) throw networkErrors[0]
+ const servedCss = []
+ for (const [url, response] of Object.entries(network)) {
+ const asset = new URL(url).pathname
+ if (asset.startsWith('/_next/static/') && /\.(?:js|css)$/.test(asset)) {
+ const file = join(relocated, '.next', asset.slice('/_next/'.length))
+ const { size, mtimeMs, mtime } = statSync(file)
+ response.fileStat = { size, mtimeMs, mtimeTime: mtime.getTime() }
+ assert.deepEqual(
+ Buffer.from(response.body, 'base64'),
+ readFileSync(file),
+ )
+ if (asset.endsWith('.css'))
+ servedCss.push(
+ Buffer.from(response.body, 'base64').toString('utf8'),
+ )
+ }
+ }
+ assert(servedCss.length > 0)
+ const served = servedCss.join('\n')
+ const servedUnusedRule = turbo
+ ? `.${unusedClass}{color:#f0f}`
+ : unusedRule
+ assertUnusedCss(served, servedUnusedRule)
+ assert.throws(() =>
+ assertUnusedCss(
+ singleCss
+ ? served.replaceAll(servedUnusedRule, '')
+ : served + servedUnusedRule,
+ servedUnusedRule,
+ ),
+ )
+ for (const color of ['magenta', '#f0f'])
+ assert.throws(() =>
+ assertUnusedCss(
+ served + `.unexpected{color:${color}}`,
+ servedUnusedRule,
+ ),
+ )
+ browserReceipt = {
+ computed,
+ unused: { className: unusedClass, elements: unusedElements },
+ network,
+ }
+ } finally {
+ try {
+ await browser?.close()
+ } finally {
+ server.kill()
+ let timer
+ try {
+ const exited = await Promise.race([
+ serverExit,
+ new Promise((_, reject) => {
+ timer = setTimeout(
+ () => reject(new Error('Owned Next server did not exit')),
+ 10000,
+ )
+ }),
+ ])
+ writeFileSync(
+ join(root, `next-${phase}-server-exit.json`),
+ JSON.stringify(exited),
+ )
+ } finally {
+ clearTimeout(timer)
+ writeFileSync(
+ join(root, `next-${phase}-readiness.json`),
+ JSON.stringify(readinessErrors, null, 2),
+ )
+ writeFileSync(
+ join(root, `next-${phase}-server.stdout`),
+ Buffer.concat(stdout),
+ )
+ writeFileSync(
+ join(root, `next-${phase}-server.stderr`),
+ Buffer.concat(stderr),
+ )
+ }
+ }
+ }
+ const files = rawFiles()
+ assert(Object.keys(files).some((file) => file.endsWith('.map')))
+ assert(
+ Object.keys(files).some(
+ (file) =>
+ file.startsWith(turbo ? 'df/devup-ui/' : '.next/cache/devup-ui/') &&
+ file.endsWith('.css'),
+ ),
+ )
+ const receipt = {
+ files,
+ compiler,
+ publishedCss,
+ coordinator,
+ mainDiskWitness,
+ browser: browserReceipt,
+ }
+ const publication = {
+ id: phase,
+ receipt,
+ turbo,
+ mode,
+ wallStarted,
+ wallFinished,
+ servedAt: Date.now(),
+ cell,
+ }
+ receipts.push(publication)
+ writeFileSync(
+ join(root, `next-${phase}-publication.json`),
+ JSON.stringify(publication, null, 2),
+ )
+ const raw = join(root, `raw-${phase}`)
+ mkdirSync(raw)
+ for (const folder of ['df', '.next']) {
+ const source = resolve(relocated, folder),
+ target = resolve(raw, folder)
+ assert(
+ source.startsWith(resolve(root) + sep) &&
+ target.startsWith(resolve(root) + sep),
+ )
+ assert(
+ realpathSync(source).startsWith(realpathSync(root) + sep) &&
+ realpathSync(raw).startsWith(realpathSync(root) + sep),
+ )
+ renameSync(source, target)
+ }
+ }
+ const byId = new Map(
+ receipts.map((publication) => [publication.id, publication]),
+ )
+ const comparisons = await collectViteCases(
+ receipts
+ .filter(
+ ({ cell }) =>
+ cell.repeat || cell.format !== 'require' || cell.mode !== 'default',
+ )
+ .map(({ id }) => id),
+ async (id) => {
+ const actual = byId.get(id)
+ const { cell } = actual
+ const expected = receipts.find(
+ ({ cell: row }) =>
+ !row.repeat &&
+ row.bundler === cell.bundler &&
+ row.singleCss === cell.singleCss &&
+ row.format === (cell.repeat ? cell.format : 'require') &&
+ row.mode === (cell.repeat ? cell.mode : 'default'),
+ )
+ assert(expected)
+ assertNextPublication(expected, actual)
+ },
+ )
+ const controls = await collectViteCases(
+ receipts
+ .filter(
+ ({ cell }) =>
+ !cell.repeat && cell.format === 'require' && cell.mode === 'default',
+ )
+ .map(({ id }) => id),
+ async (id) => nextComparisonControls(byId.get(id)),
+ )
+ assert.equal(receipts.length, bundlers.length * 16)
+ assert.equal(comparisons.length, bundlers.length * 14)
+ const result = {
+ cliBuilds: receipts.length,
+ browserPages: receipts.length,
+ comparisons,
+ controls,
+ costs,
+ binaryStorageParity: 'unqualified',
+ }
+ writeFileSync(
+ join(root, 'next-publication-summary.json'),
+ JSON.stringify(result, null, 2),
+ )
+ assert(
+ [...comparisons, ...controls].every((row) => row.ok),
+ 'Next publication comparison failed; preserve raw evidence and review exact differences',
+ )
+ console.info(
+ JSON.stringify({
+ cliBuilds: receipts.length,
+ browserPages: receipts.length,
+ comparisons: comparisons.length,
+ binaryStorageParity: 'unqualified',
+ root,
+ }),
+ )
+}
+
+async function nextLoaderCells({ root, relocated, link, bun }) {
+ const receipts = new Map(),
+ pairs = []
+ const children = []
+ const cell = (
+ runtime,
+ format,
+ singleCss,
+ mode,
+ repeat = false,
+ guard = false,
+ authoredErrors = false,
+ ) => {
+ const id = [
+ 'next-loader',
+ runtime === bun ? 'bun' : 'node',
+ format,
+ singleCss ? 'single' : 'per-file',
+ mode,
+ authoredErrors
+ ? 'authored-errors'
+ : repeat
+ ? 'repeat'
+ : guard
+ ? 'guard'
+ : 'first',
+ ].join('-')
+ const destination = join(root, `${id}.json`)
+ run(
+ runtime,
+ [
+ join(link, 'engine-consumer.mjs'),
+ guard ? 'guards' : 'build',
+ mode,
+ format,
+ singleCss ? 'single' : 'per-file',
+ destination,
+ 'next',
+ ...(authoredErrors ? ['authored-errors'] : []),
+ ],
+ { cwd: relocated, evidenceBase: join(root, id) },
+ )
+ const receipt = JSON.parse(readFileSync(destination, 'utf8'))
+ receipts.set(id, receipt)
+ children.push(id)
+ if (guard) assert.equal(receipt.guards, true)
+ return id
+ }
+ const baselines = new Map()
+ for (const runtime of [process.execPath, bun])
+ for (const format of ['require', 'import']) {
+ cell(runtime, format, false, 'default', false, true)
+ for (const singleCss of [false, true]) {
+ const first = cell(runtime, format, singleCss, 'default')
+ baselines.set(`${runtime}-${format}-${singleCss}`, first)
+ for (const mode of ['native', 'wasm']) {
+ const actual = cell(runtime, format, singleCss, mode)
+ pairs.push([first, actual])
+ pairs.push([actual, cell(runtime, format, singleCss, mode, true)])
+ }
+ }
+ }
+ for (const singleCss of [false, true]) {
+ const first = baselines.get(`${process.execPath}-require-${singleCss}`)
+ pairs.push([
+ first,
+ baselines.get(`${process.execPath}-import-${singleCss}`),
+ ])
+ pairs.push([
+ baselines.get(`${bun}-require-${singleCss}`),
+ baselines.get(`${bun}-import-${singleCss}`),
+ ])
+ const authored = []
+ for (const runtime of [process.execPath, bun])
+ for (const format of ['require', 'import'])
+ authored.push(
+ cell(runtime, format, singleCss, 'default', false, false, true),
+ )
+ for (const actual of authored.slice(1)) pairs.push([authored[0], actual])
+ }
+ const comparisons = await collectViteCases(
+ pairs,
+ async ([expected, actual]) =>
+ assertRawParity(receipts.get(expected), receipts.get(actual)),
+ )
+ const controls = await collectViteCases(
+ [...receipts].filter(([, receipt]) => !receipt.guards).map(([id]) => id),
+ async (id) => {
+ const receipt = receipts.get(id)
+ for (const [name, mutate] of [
+ [
+ 'original identity',
+ (copy) => {
+ copy.local[0].thrownIdentity = false
+ },
+ ],
+ [
+ 'cause identity',
+ (copy) => {
+ copy.local[0].causeIdentity = false
+ },
+ ],
+ [
+ 'unknown local field',
+ (copy) => {
+ copy.local[0].unknown = true
+ },
+ ],
+ [
+ 'selected state',
+ (copy) => {
+ copy.state.css += 'changed'
+ },
+ ],
+ [
+ 'source map',
+ (copy) => {
+ copy.sourceResult.map += 'changed'
+ },
+ ],
+ ]) {
+ const changed = structuredClone(receipt)
+ mutate(changed)
+ assert.throws(() => assertRawParity(receipt, changed), undefined, name)
+ }
+ return { rejected: 5 }
+ },
+ )
+ writeFileSync(
+ join(root, 'next-loader-summary.json'),
+ JSON.stringify({ children, comparisons, controls }, null, 2),
+ )
+ assert.equal(children.length, 52)
+ assert.equal(comparisons.length, 42)
+ assert.equal(controls.length, 48)
+ assert(
+ controls.every((row) => row.ok),
+ 'Next loader negative controls failed',
+ )
+ assert(
+ comparisons.every((row) => row.ok),
+ 'Next installed loader parity failed',
+ )
+}
+
+export async function webpackParity(adapter = 'webpack') {
+ const repository = fileURLToPath(new URL('../../', import.meta.url))
+ const parent = process.env.DEVUP_ENGINE_ARTIFACT_PARENT ?? tmpdir()
+ const root = mkdtempSync(join(parent, `devup-${adapter}-engine-`))
+ artifactRoot = root
+ console.info(`${adapter} engine evidence: ${root}`)
+ const bun = process.env.BUN_BINARY ?? 'bun'
+ const context = { repository, root, bun }
+ const { host, tarballs } = await packHostEngines(context, run)
+ for (const name of [
+ 'wasm',
+ 'plugin-utils',
+ 'react',
+ ...(adapter === 'next' ? ['webpack-plugin'] : []),
+ `${adapter}-plugin`,
+ ]) {
+ const source = join(
+ repository,
+ name === 'wasm' ? 'bindings/devup-ui-wasm' : `packages/${name}`,
+ )
+ const destination = join(root, 'packs', name)
+ mkdirSync(destination, { recursive: true })
+ run(
+ bun,
+ [
+ 'pm',
+ 'pack',
+ '--ignore-scripts',
+ '--quiet',
+ '--destination',
+ destination,
+ ],
+ { cwd: source },
+ )
+ const packs = readdirSync(destination).filter((file) =>
+ file.endsWith('.tgz'),
+ )
+ assert.equal(packs.length, 1)
+ tarballs[`@devup-ui/${name}`] =
+ `file:${(adapter === 'vite' || adapter === 'bun' ? viteRealpath : realpathSync)(join(destination, packs[0])).split(sep).join('/')}`
+ }
+ const assertDist =
+ adapter === 'bun'
+ ? assertBunDist
+ : adapter === 'rsbuild' || adapter === 'vite'
+ ? assertRsbuildDist
+ : assertWebpackDist
+ assertDist(join(repository, `packages/${adapter}-plugin/dist`))
+ const bundler =
+ adapter === 'next'
+ ? 'next'
+ : adapter === 'bun'
+ ? '@devup-ui/bun-plugin'
+ : adapter === 'vite'
+ ? 'vite'
+ : adapter === 'rsbuild'
+ ? '@rsbuild/core'
+ : 'webpack'
+ const versions = {}
+ for (const name of [
+ ...(adapter === 'bun' ? [] : [bundler]),
+ ...(adapter === 'next' ? ['webpack', 'react-dom'] : []),
+ 'react',
+ 'csstype-extra',
+ ]) {
+ const response = await fetch(`https://registry.npmjs.org/${name}/latest`, {
+ signal: AbortSignal.timeout(30000),
+ })
+ assert.equal(response.status, 200)
+ const manifest = await response.json()
+ assert.equal(manifest.name, name)
+ versions[name] = manifest.version
+ }
+ const directory = join(root, 'consumer')
+ mkdirSync(directory)
+ writeFileSync(
+ join(directory, 'package.json'),
+ JSON.stringify(
+ {
+ name: `${adapter}-engine-consumer`,
+ private: true,
+ type: 'module',
+ dependencies: {
+ ...versions,
+ [`@devup-ui/${adapter}-plugin`]:
+ tarballs[`@devup-ui/${adapter}-plugin`],
+ '@devup-ui/react': tarballs['@devup-ui/react'],
+ },
+ overrides: tarballs,
+ },
+ null,
+ 2,
+ ),
+ )
+ run(
+ bun,
+ ['install', '--ignore-scripts', '--linker=isolated', ...host.installArgs],
+ { cwd: directory },
+ )
+ const beforeRelocation = verifyHostInstall({ directory, tarballs, host, bun })
+ const relocated = join(root, 'relocated consumer')
+ const entryLink = join(directory, 'node_modules', bundler)
+ assert(lstatSync(entryLink).isSymbolicLink())
+ const target = readlinkSync(entryLink)
+ const reinstall = isAbsolute(target)
+ const relocation = {
+ intent:
+ 'Keep a moved installed consumer usable: relative installer links move unchanged; absolute installer links require a frozen reinstall, not a claim of portable absolute junctions.',
+ entryLink,
+ target,
+ branch: reinstall
+ ? 'absolute-link-frozen-reinstall'
+ : 'relative-link-plain-move',
+ beforeRelocation,
+ }
+ writeFileSync(
+ join(root, 'relocation-before.json'),
+ JSON.stringify(relocation, null, 2),
+ )
+ renameSync(directory, relocated)
+ const lock = readFileSync(join(relocated, 'bun.lock'))
+ writeFileSync(join(root, 'relocation-before.lock'), lock)
+ if (reinstall) {
+ run(
+ bun,
+ [
+ 'install',
+ '--frozen-lockfile',
+ '--ignore-scripts',
+ '--linker=isolated',
+ ...host.installArgs,
+ ],
+ { cwd: relocated },
+ )
+ }
+ const relocatedLock = readFileSync(join(relocated, 'bun.lock'))
+ writeFileSync(join(root, 'relocation-after.lock'), relocatedLock)
+ assert.deepEqual(relocatedLock, lock)
+ const afterRelocation = verifyHostInstall({
+ directory: relocated,
+ tarballs,
+ host,
+ bun,
+ })
+ writeFileSync(
+ join(root, 'relocation-after.json'),
+ JSON.stringify({ ...relocation, afterRelocation }, null, 2),
+ )
+ cpSync(
+ new URL('./engine-consumer.mjs', import.meta.url),
+ join(relocated, 'engine-consumer.mjs'),
+ )
+ const link = join(root, 'consumer link')
+ symlinkSync(
+ relocated,
+ link,
+ process.platform === 'win32' ? 'junction' : 'dir',
+ )
+ writeFileSync(
+ join(root, 'provenance.json'),
+ JSON.stringify(
+ {
+ repository,
+ versions,
+ host,
+ tarballs,
+ ...(adapter === 'next'
+ ? {
+ tarballSHA256: Object.fromEntries(
+ Object.entries(tarballs).map(([name, reference]) => [
+ name,
+ createHash('sha256')
+ .update(readFileSync(reference.slice('file:'.length)))
+ .digest('hex'),
+ ]),
+ ),
+ }
+ : {}),
+ bun: run(bun, ['--version'], { cwd: root }).trim(),
+ node: process.version,
+ },
+ null,
+ 2,
+ ),
+ )
+ if (adapter === 'next') {
+ assert(
+ [undefined, 'publication', 'loaders'].includes(
+ process.env.DEVUP_NEXT_SUITE,
+ ),
+ )
+ if (process.env.DEVUP_NEXT_SUITE !== 'loaders')
+ await nextPublicationCells({ root, relocated, link })
+ if (process.env.DEVUP_NEXT_SUITE !== 'publication')
+ await nextLoaderCells({ root, relocated, link, bun })
+ return root
+ }
+ if (adapter === 'bun') {
+ await bunCells({ root, relocated, link, bun })
+ return root
+ }
+ if (adapter === 'vite') {
+ cpSync(
+ new URL('./engine-dev-cuts.mjs', import.meta.url),
+ join(relocated, 'engine-dev-cuts.mjs'),
+ )
+ await viteCells({ root, relocated, link, bun })
+ return root
+ }
+ for (const runtime of [process.execPath, bun]) {
+ const runtimeName = runtime === bun ? 'bun' : 'node'
+ const aliasControl = join(root, `${runtimeName}-alias-entry-control`)
+ const aliasReceipt = `${aliasControl}.json`
+ run(
+ runtime,
+ [
+ join(link, 'engine-consumer.mjs'),
+ 'guards',
+ 'wasm',
+ 'require',
+ 'per-file',
+ aliasReceipt,
+ adapter,
+ ],
+ { cwd: relocated, evidenceBase: aliasControl },
+ )
+ assert(
+ existsSync(aliasReceipt),
+ 'The alias CLI must execute and produce a receipt',
+ )
+ assert.equal(JSON.parse(readFileSync(aliasReceipt, 'utf8')).guards, true)
+ const cell = (scenario, mode, format, css) => {
+ const base = join(
+ root,
+ `${runtimeName}-${scenario}-${mode}-${format}-${css}`,
+ )
+ const destination = `${base}.json`
+ run(
+ runtime,
+ [
+ join(link, 'engine-consumer.mjs'),
+ scenario,
+ mode,
+ format,
+ css,
+ destination,
+ adapter,
+ ],
+ { cwd: relocated, evidenceBase: base },
+ )
+ return JSON.parse(readFileSync(destination, 'utf8'))
+ }
+ for (const scenario of [
+ 'build',
+ 'additional',
+ 'watch',
+ ...(adapter === 'rsbuild' ? ['dev'] : []),
+ ]) {
+ for (const css of ['per-file', 'single']) {
+ let expected
+ for (const format of ['require', 'import']) {
+ for (const mode of ['default', 'native', 'wasm']) {
+ const actual = cell(scenario, mode, format, css)
+ if (expected === undefined) expected = actual
+ else if (adapter === 'rsbuild')
+ assertRsbuildParity(expected, actual)
+ else assertRawParity(expected, actual)
+ }
+ }
+ }
+ }
+ for (const format of ['require', 'import']) {
+ for (const mode of ['default', 'native', 'wasm']) {
+ cell('late', mode, format, 'per-file')
+ }
+ cell('guards', 'wasm', format, 'per-file')
+ if (adapter === 'webpack')
+ cell('wrong-loader', 'default', format, 'per-file')
+ for (const css of ['per-file', 'single']) {
+ cell('errors', 'native', format, css)
+ }
+ }
+ }
+ console.info(`${adapter} engine evidence: ${root}`)
+ return root
+}
+
+export const rsbuildParity = () => webpackParity('rsbuild')
+export const viteParity = () => webpackParity('vite')
+export const bunParity = () => webpackParity('bun')
+
+if (
+ process.argv[1] &&
+ resolve(process.argv[1]) === fileURLToPath(import.meta.url)
+) {
+ const adapter = process.argv[2] ?? 'webpack'
+ assert(['webpack', 'rsbuild', 'vite', 'bun', 'next'].includes(adapter))
+ await webpackParity(adapter)
+}
diff --git a/e2e/plugins/engine.test.mjs b/e2e/plugins/engine.test.mjs
new file mode 100644
index 000000000..0e4e17d21
--- /dev/null
+++ b/e2e/plugins/engine.test.mjs
@@ -0,0 +1,1043 @@
+import assert from 'node:assert/strict'
+import { execFileSync } from 'node:child_process'
+import { createHash } from 'node:crypto'
+import {
+ cpSync,
+ mkdirSync,
+ mkdtempSync,
+ readFileSync,
+ realpathSync,
+ rmSync,
+ symlinkSync,
+ writeFileSync,
+} from 'node:fs'
+import { createRequire } from 'node:module'
+import { tmpdir } from 'node:os'
+import { basename, dirname, join, sep } from 'node:path'
+import { test } from 'node:test'
+import { fileURLToPath } from 'node:url'
+import { runInNewContext } from 'node:vm'
+
+import {
+ assertErrorParity,
+ assertRawParity,
+ assertRsbuildDist,
+ assertRsbuildParity,
+ assertViteBuckets,
+ assertViteParity,
+ assertWebpackDist,
+ collectViteCases,
+ completedReceipt,
+ diagnosticHook,
+ errorEvidence,
+ initialLayerGate,
+ observeRejection,
+ webpackDistFiles,
+} from './engine-consumer.mjs'
+import {
+ HostPackError,
+ hostPackRealpath,
+ hostPackSelection,
+ verifyHostInstall,
+} from './engine-fixtures.mjs'
+
+for (const single of [false, true]) {
+ for (const corrupt of ['none', 'disk', 'served']) {
+ test(`Vite mapped bucket proof: single=${single}, corrupt=${corrupt}`, () => {
+ // Given: an unrelated teal bucket cannot rescue either affected bucket.
+ const files = single
+ ? ['devup-ui.css']
+ : ['devup-ui-3.css', 'devup-ui-4.css']
+ const urls = files.map((file) => `/df/devup-ui/${file}?devup-revision=1`)
+ const phase = {
+ disk: { '/df/devup-ui/devup-ui-5.css': '.unrelated{color:teal}' },
+ requested: { transforms: {}, http: {} },
+ }
+ for (const [index, name] of ['A', 'B'].entries()) {
+ const url = urls[single ? 0 : index]
+ phase.requested.transforms[`/src/${name}.js`] = {
+ code: `import "${url}";`,
+ }
+ phase.requested.http[`${url}&direct`] = { body: '.a{color:teal}' }
+ phase.disk[url.split('?')[0]] = '.a{color:teal}'
+ }
+ if (!single) {
+ phase.disk['/df/devup-ui/devup-ui.css'] = ':root{--size:17px}'
+ assert.throws(() =>
+ assert.match(phase.disk['/df/devup-ui/devup-ui.css'], /teal/),
+ )
+ }
+ if (corrupt === 'disk') phase.disk[urls[0].split('?')[0]] = ''
+ if (corrupt === 'served')
+ phase.requested.http[`${urls[0]}&direct`].body = ''
+ // When / Then
+ const check = () => assertViteBuckets(phase, /teal/)
+ if (corrupt === 'none') assert.doesNotThrow(check)
+ else assert.throws(check, assert.AssertionError)
+ })
+ }
+}
+
+test('Vite diagnostic collection preserves a failure and still visits later cells', async () => {
+ // Given
+ const visited = []
+ const failure = new Error('diagnostic control')
+ // When
+ const cases = [
+ 'located',
+ 'resolver',
+ 'registration',
+ 'reservation',
+ 'later-execution',
+ 'later-comparison',
+ ]
+ const results = await collectViteCases(cases, async (name) => {
+ visited.push(name)
+ if (name === 'located') throw failure
+ return 'second receipt'
+ })
+ // Then
+ assert.deepEqual(visited, cases)
+ assert.equal(results[0].ok, false)
+ assertRawParity(results[0].error, errorEvidence(failure))
+ assert.deepEqual(results[1], {
+ name: 'resolver',
+ ok: true,
+ value: 'second receipt',
+ })
+})
+
+test('a partial saved receipt cannot become a successful boundary comparison', () => {
+ // Given
+ const receipts = new Map([['partial', { phases: [] }]])
+ // When / Then
+ assert.throws(
+ () => completedReceipt(receipts, new Set(), 'partial'),
+ assert.AssertionError,
+ )
+ assert.equal(
+ completedReceipt(receipts, new Set(['partial']), 'partial'),
+ receipts.get('partial'),
+ )
+})
+
+for (const field of ['http', 'disk', 'map', 'observations', 'residual']) {
+ test(`Vite raw parity rejects a changed ${field} without an intermediate exclusion`, () => {
+ // Given
+ const expected = { phases: [{ [field]: 'before' }] }
+ const actual = { phases: [{ [field]: 'after' }] }
+ // When / Then
+ assert.throws(() => assertRawParity(expected, actual))
+ })
+}
+
+for (const field of [
+ 'css',
+ 'code',
+ 'map',
+ 'assets',
+ 'disk',
+ 'state',
+ 'calls',
+]) {
+ test(`Vite opposite single CSS exemption preserves ${field} custody`, () => {
+ const expected = { phases: [{ outputs: { file: { css: 'early' } } }] }
+ const actual = structuredClone(expected)
+ const target = ['css', 'code', 'map'].includes(field)
+ ? actual.phases[0].outputs.file
+ : actual.phases[0]
+ target[field] = 'different'
+ if (field === 'css') {
+ assertViteParity(expected, actual, true)
+ assert.throws(() => assertViteParity(expected, actual))
+ } else assert.throws(() => assertViteParity(expected, actual, true))
+ assert.equal(
+ actual.phases[0].outputs.file.css,
+ field === 'css' ? 'different' : 'early',
+ )
+ delete actual.phases[0].outputs.file.css
+ assert.throws(() => assertViteParity(expected, actual, true))
+ })
+}
+
+test('Vite raw parity distinguishes a present no-map value from an absent field', () => {
+ // Given
+ const expected = { output: { map: undefined } }
+ const actual = { output: {} }
+ // When / Then
+ assert.throws(() => assertRawParity(expected, actual))
+})
+
+test('host path policy equates short, expanded and junction spellings exactly', (t) => {
+ const root = mkdtempSync(join(tmpdir(), 'devup host realpath '))
+ t.after(() => rmSync(root, { recursive: true, force: true }))
+ const target = join(root, 'target')
+ const alias = join(root, 'alias')
+ mkdirSync(target)
+ symlinkSync(target, alias, process.platform === 'win32' ? 'junction' : 'dir')
+ const expanded = realpathSync.native(target)
+ assert.equal(hostPackRealpath(target), expanded)
+ assert.equal(hostPackRealpath(expanded), expanded)
+ assert.equal(hostPackRealpath(alias), expanded)
+})
+
+test('host path policy retries only EISDIR with the exact same input', () => {
+ const calls = []
+ const result = hostPackRealpath('same-input', {
+ native(filename) {
+ calls.push(['native', filename])
+ throw Object.assign(new Error('native EISDIR'), { code: 'EISDIR' })
+ },
+ regular(filename) {
+ calls.push(['regular', filename])
+ return 'physical-path'
+ },
+ })
+ assert.equal(result, 'physical-path')
+ assert.deepEqual(calls, [
+ ['native', 'same-input'],
+ ['regular', 'same-input'],
+ ])
+})
+
+for (const code of ['ENOENT', 'EACCES']) {
+ test(`host path policy preserves ${code} without fallback`, () => {
+ const failure = Object.assign(new Error('native failure'), { code })
+ assert.throws(
+ () =>
+ hostPackRealpath('same-input', {
+ native() {
+ throw failure
+ },
+ regular() {
+ assert.fail('Only EISDIR may retry')
+ },
+ }),
+ (error) => error === failure,
+ )
+ })
+}
+
+for (const format of ['require', 'import']) {
+ test(`built ${format} shorthand error uses its public constructor, not the other format`, async () => {
+ // Given
+ const cjs = createRequire(import.meta.url)(
+ '../../packages/plugin-utils/dist/index.cjs',
+ )
+ const esm = await import('../../packages/plugin-utils/dist/index.mjs')
+ const selected = format === 'require' ? cjs : esm
+ const other = format === 'require' ? esm : cjs
+ const invalid = () =>
+ selected.normalizeShorthands({ bad: ['backgroundColour'] })
+ // When / Then
+ assert.throws(invalid, selected.ShorthandConfigError)
+ assert.throws(
+ () => assert.throws(invalid, other.ShorthandConfigError),
+ assert.AssertionError,
+ )
+ })
+}
+
+for (const order of ['AB', 'BA']) {
+ for (const releaseAt of [0, 1, 2]) {
+ test(`initial gate releases ${order} admissions in filename order with CSS signal at ${releaseAt}`, () => {
+ // Given
+ const module = { exports: {} }
+ runInNewContext(initialLayerGate, { module }, { timeout: 1000 })
+ const loader = module.exports
+ const released = []
+ // When
+ for (let index = 0; index <= order.length; index++) {
+ if (index === releaseAt) loader.release()
+ if (index < order.length) {
+ const name = order[index]
+ loader.call(
+ {
+ resourcePath: `C:/fixture/src/${name}.css.ts`,
+ async: () => (error, source) => {
+ assert.equal(error, null)
+ released.push(source)
+ },
+ },
+ name,
+ )
+ }
+ // Then
+ assert.deepEqual(
+ released,
+ index >= releaseAt && index >= 1 ? ['A', 'B'] : [],
+ )
+ }
+ assert.equal(
+ loader.call({ resourcePath: 'C:/fixture/src/B.css.ts' }, 'edited'),
+ 'edited',
+ )
+ })
+ }
+}
+
+test('built Webpack entry and both loader formats use only the shared selector', () => {
+ // Given
+ const dist = fileURLToPath(
+ new URL('../../packages/webpack-plugin/dist/', import.meta.url),
+ )
+ // When / Then
+ assertWebpackDist(dist)
+})
+
+for (const name of webpackDistFiles) {
+ test(`rejects a direct backend import in ${name}`, (t) => {
+ // Given
+ const root = mkdtempSync(join(tmpdir(), 'devup-dist-guard-'))
+ t.after(() => rmSync(root, { recursive: true, force: true }))
+ for (const file of webpackDistFiles) {
+ writeFileSync(join(root, file), 'import "@devup-ui/plugin-utils";')
+ }
+ writeFileSync(
+ join(root, name),
+ 'import "@devup-ui/plugin-utils"; require("@devup-ui/wasm");',
+ )
+ // When / Then
+ assert.throws(() => assertWebpackDist(root), assert.AssertionError)
+ })
+}
+
+for (const name of ['index.cjs', 'index.mjs']) {
+ for (const backend of ['wasm', 'binding']) {
+ test(`rejects a direct ${backend} import in Rsbuild ${name}`, (t) => {
+ const root = mkdtempSync(join(tmpdir(), 'devup-rsbuild-dist-guard-'))
+ t.after(() => rmSync(root, { recursive: true, force: true }))
+ for (const file of ['index.cjs', 'index.mjs']) {
+ writeFileSync(join(root, file), 'import "@devup-ui/plugin-utils";')
+ }
+ assertRsbuildDist(root)
+ writeFileSync(
+ join(root, name),
+ `import "@devup-ui/plugin-utils"; require("@devup-ui/${backend}");`,
+ )
+ assert.throws(() => assertRsbuildDist(root), assert.AssertionError)
+ })
+ }
+}
+
+for (const field of ['code', 'css', 'map', 'dependencies', 'sheet', 'error']) {
+ test(`raw parity rejects a changed ${field} without normalizing it`, () => {
+ // Given
+ const expected = {
+ code: 'x\r\n',
+ css: '/* retained */.a{}',
+ map: undefined,
+ dependencies: ['a', 'b'],
+ sheet: '{"x":1}',
+ error: { name: 'Error', message: 'file.tsx:1:2' },
+ }
+ const actual = {
+ ...expected,
+ [field]: field === 'dependencies' ? ['b', 'a'] : 'different',
+ }
+ // When / Then
+ assert.throws(
+ () => assertRawParity(expected, actual),
+ assert.AssertionError,
+ )
+ })
+}
+
+test('error parity rejects a difference only in a non-enumerable value', () => {
+ // Given
+ const expected = new Error('same diagnostic')
+ const actual = new Error('same diagnostic')
+ Object.defineProperty(expected, 'detail', { value: 'before' })
+ Object.defineProperty(actual, 'detail', { value: 'after' })
+ // When / Then
+ assert.throws(
+ () => assertErrorParity(expected, actual),
+ assert.AssertionError,
+ )
+})
+
+test('only Bun permits optional original creation-field presence', () => {
+ const expected = new Error('same diagnostic')
+ const actual = new Error('same diagnostic')
+ for (const key of ['originalLine', 'originalColumn']) {
+ Object.defineProperty(expected, key, {
+ value: 1,
+ configurable: true,
+ enumerable: false,
+ writable: true,
+ })
+ Reflect.deleteProperty(actual, key)
+ }
+ for (const compare of [
+ () => assertErrorParity(expected, actual),
+ () => assertRawParity(errorEvidence(expected), errorEvidence(actual)),
+ ]) {
+ if (process.versions.bun) assert.doesNotThrow(compare)
+ else assert.throws(compare, assert.AssertionError)
+ }
+})
+
+for (const side of ['expected', 'actual']) {
+ test(`error parity rejects an unrelated key present only on ${side}`, () => {
+ const expected = new Error('same diagnostic')
+ const actual = new Error('same diagnostic')
+ Object.defineProperty(side === 'expected' ? expected : actual, 'other', {
+ value: 1,
+ })
+ assert.throws(
+ () => assertErrorParity(expected, actual),
+ assert.AssertionError,
+ )
+ assert.throws(
+ () => assertRawParity(errorEvidence(expected), errorEvidence(actual)),
+ assert.AssertionError,
+ )
+ })
+}
+
+test('optional creation fields retain their descriptor contract', () => {
+ const expected = new Error('same diagnostic')
+ const actual = new Error('same diagnostic')
+ for (const [error, writable] of [
+ [expected, true],
+ [actual, false],
+ ]) {
+ Reflect.ownKeys(error)
+ Reflect.get(error, 'stack')
+ Object.defineProperty(error, 'originalLine', {
+ value: 1,
+ configurable: true,
+ enumerable: false,
+ writable,
+ })
+ }
+ assert.equal(
+ Object.getOwnPropertyDescriptor(actual, 'originalLine').writable,
+ false,
+ )
+ assert.throws(
+ () => assertErrorParity(expected, actual),
+ assert.AssertionError,
+ )
+ assert.throws(
+ () => assertRawParity(errorEvidence(expected), errorEvidence(actual)),
+ assert.AssertionError,
+ )
+})
+
+test('error parity retains the order of every other own key', () => {
+ const expected = new Error('same diagnostic')
+ const actual = new Error('same diagnostic')
+ for (const key of ['a', 'b'])
+ Object.defineProperty(expected, key, { value: 1 })
+ for (const key of ['b', 'a']) Object.defineProperty(actual, key, { value: 1 })
+ assert.throws(
+ () => assertErrorParity(expected, actual),
+ assert.AssertionError,
+ )
+ assert.throws(
+ () => assertRawParity(errorEvidence(expected), errorEvidence(actual)),
+ assert.AssertionError,
+ )
+})
+
+test('error parity rejects a difference only in a descriptor flag', () => {
+ // Given
+ const expected = new Error('same diagnostic')
+ const actual = new Error('same diagnostic')
+ Object.defineProperty(expected, 'detail', { value: 1, writable: false })
+ Object.defineProperty(actual, 'detail', { value: 1, writable: true })
+ // When / Then
+ assert.throws(
+ () => assertErrorParity(expected, actual),
+ assert.AssertionError,
+ )
+})
+
+for (const difference of ['value', 'writable']) {
+ test(`packed error receipts reject a non-enumerable ${difference} difference`, () => {
+ // Given
+ const expected = new Error('same diagnostic')
+ const actual = new Error('same diagnostic')
+ Object.defineProperty(expected, 'detail', { value: 1, writable: false })
+ Object.defineProperty(actual, 'detail', {
+ value: difference === 'value' ? 2 : 1,
+ writable: difference === 'writable',
+ })
+ const before = errorEvidence(expected)
+ const after = errorEvidence(actual)
+ // When / Then
+ assert.throws(() => assertRawParity(before, after), assert.AssertionError)
+ })
+}
+
+test('error parity retains native-only public fields rather than excluding them', () => {
+ // Given
+ const expected = new Error('same diagnostic')
+ const actual = new Error('same diagnostic')
+ Object.defineProperty(actual, 'code', { value: 'GenericFailure' })
+ // When / Then
+ assert.throws(
+ () => assertErrorParity(expected, actual),
+ assert.AssertionError,
+ )
+})
+
+test('error parity compares cause diagnostics recursively', () => {
+ // Given
+ const expected = new Error('outer', { cause: new Error('before') })
+ const actual = new Error('outer', { cause: new Error('after') })
+ // When / Then
+ assert.throws(
+ () => assertErrorParity(expected, actual),
+ assert.AssertionError,
+ )
+})
+
+for (const kind of ['symbol', 'function', 'accessor']) {
+ test(`error parity rejects different live ${kind} identities with equal text`, () => {
+ // Given
+ const expected = new Error('same diagnostic')
+ const actual = new Error('same diagnostic')
+ const descriptor = () => {
+ switch (kind) {
+ case 'symbol':
+ return { value: Symbol('same') }
+ case 'function':
+ return { value: function same() {} }
+ case 'accessor':
+ return {
+ get: function same() {
+ return 1
+ },
+ }
+ default:
+ assert.fail(`Unknown identity control ${kind}`)
+ }
+ }
+ Object.defineProperty(expected, 'detail', descriptor())
+ Object.defineProperty(actual, 'detail', descriptor())
+ // When / Then
+ assert.throws(
+ () => assertErrorParity(expected, actual),
+ assert.AssertionError,
+ )
+ })
+}
+
+test('error parity accepts shared live symbol keys and accessor identities', () => {
+ // Given
+ const key = Symbol('detail')
+ const get = () => 1
+ const expected = new Error('same diagnostic')
+ const actual = new Error('same diagnostic')
+ Object.defineProperty(expected, key, { get })
+ Object.defineProperty(actual, key, { get })
+ // When / Then
+ assertErrorParity(expected, actual)
+})
+
+test('saved error evidence retains complete raw stack values', () => {
+ // Given
+ const error = new Error('raw stack')
+ const stack = error.stack
+ // When
+ const evidence = errorEvidence(error)
+ // Then
+ const property = evidence.raw.properties.find(
+ ({ key }) => key.type === 'string' && key.value === 'stack',
+ )
+ assert.equal(property.observed.value, stack)
+})
+
+test('hook diagnostics retain the complete catch-turn graph before bundler mutation', async () => {
+ // Given
+ const key = Symbol('hidden')
+ const cause = new Error('cause')
+ const error = new Error('boundary', { cause })
+ const nested = { value: 'before' }
+ Object.defineProperty(error, key, { value: nested })
+ Object.defineProperty(error, 'detail', { get: () => nested })
+ const expected = errorEvidence(error)
+ let captured
+ const hook = diagnosticHook(
+ () => {
+ throw error
+ },
+ (value) => {
+ captured = value
+ },
+ )
+ // When: Vite keeps the original reference, adds metadata and changes nested data.
+ await assert.rejects(hook(), (value) => value === error)
+ Object.assign(error, {
+ plugin: 'devup-ui',
+ id: '/src/error.js',
+ loc: { line: 357, column: 12 },
+ frame: 'later',
+ })
+ nested.value = 'after'
+ // Then: the old deferred receipt fails while the immediate graph is unchanged.
+ assert.throws(() => assertErrorParity(expected, errorEvidence(error)))
+ assertErrorParity(expected, captured)
+ for (const field of ['loc', 'frame', 'message', 'other']) {
+ const left = errorEvidence(new Error('same'))
+ const right = errorEvidence(
+ Object.assign(new Error('same'), { [field]: 'different' }),
+ )
+ assert.throws(() => assertErrorParity(left, right))
+ }
+})
+
+test('a rejection stays observed behind a held gate and rethrows its exact cause on consumption', async () => {
+ // Given
+ const gate = Promise.withResolvers()
+ const cause = new Error('held rejection')
+ const consume = observeRejection(Promise.reject(cause))
+ // When: cross a complete event-loop turn before releasing the unrelated work.
+ await new Promise(setImmediate)
+ gate.resolve()
+ await gate.promise
+ // Then
+ await assert.rejects(consume(), (error) => error === cause)
+})
+
+function fixture(t, archiveMarker) {
+ const root = mkdtempSync(join(tmpdir(), 'devup host guard '))
+ t.after(() => rmSync(root, { recursive: true, force: true }))
+ const directory = join(root, 'consumer')
+ const host = hostPackSelection()
+ const tarballs = {}
+ const locations = new Map()
+ const lock = { lockfileVersion: 1, packages: {} }
+ const names = [
+ '@devup-ui/binding',
+ host.name,
+ '@devup-ui/wasm',
+ '@devup-ui/plugin-utils',
+ '@devup-ui/react',
+ '@devup-ui/webpack-plugin',
+ '@devup-ui/next-plugin',
+ ]
+ mkdirSync(join(directory, 'node_modules/@devup-ui'), { recursive: true })
+ mkdirSync(join(root, 'packs'))
+ for (const [index, name] of names.entries()) {
+ const source = join(root, `source-${index}`)
+ const payload = join(source, 'package')
+ mkdirSync(payload, { recursive: true })
+ const manifest = { name, version: '0.1.0', main: 'index.js' }
+ if (name === '@devup-ui/binding') {
+ manifest.optionalDependencies = Object.fromEntries(
+ [host.name, ...host.omitted].map((item) => [item, '0.1.0']),
+ )
+ }
+ writeFileSync(
+ join(payload, 'package.json'),
+ JSON.stringify(manifest, null, 2),
+ )
+ writeFileSync(
+ join(payload, 'index.js'),
+ `/* retained ${name} */\r\nexport const value=1\r\n`,
+ )
+ writeFileSync(
+ join(payload, 'index.js.map'),
+ JSON.stringify({ sources: [name], sourcesContent: ['original\r\n'] }),
+ )
+ if (archiveMarker && name === '@devup-ui/react') {
+ const marker = join(payload, archiveMarker)
+ mkdirSync(dirname(marker), { recursive: true })
+ writeFileSync(marker, 'archive-controlled marker')
+ }
+ const archive = join(root, 'packs', `package ${index}.tgz`)
+ execFileSync('tar', ['-czf', basename(archive), '-C', source, 'package'], {
+ cwd: dirname(archive),
+ timeout: 30000,
+ })
+ const installed = join(
+ directory,
+ 'node_modules/.bun',
+ `slot-${index}`,
+ 'node_modules',
+ name,
+ )
+ cpSync(payload, installed, { recursive: true })
+ symlinkSync(
+ installed,
+ join(directory, 'node_modules', name),
+ process.platform === 'win32' ? 'junction' : 'dir',
+ )
+ locations.set(name, installed)
+ tarballs[name] = `file:${archive.split(sep).join('/')}`
+ lock.packages[name] = [
+ `${name}@${archive.split(sep).join('/')}`,
+ {},
+ `sha512-${createHash('sha512').update(readFileSync(archive)).digest('base64')}`,
+ ]
+ }
+ for (const name of host.omitted) lock.packages[name] = [`${name}@0.1.0`, {}]
+ const saveLock = () =>
+ writeFileSync(join(directory, 'bun.lock'), JSON.stringify(lock, null, 2))
+ saveLock()
+ return {
+ root,
+ directory,
+ locations,
+ lock,
+ saveLock,
+ options: {
+ directory,
+ tarballs,
+ host,
+ bun: process.env.BUN_BINARY ?? 'bun',
+ },
+ }
+}
+
+function assertRejected(options, expected) {
+ assert.throws(
+ () => verifyHostInstall(options),
+ (error) => {
+ assert.ok(error instanceof HostPackError)
+ assert.equal(error.category, expected.category)
+ assert.equal(error.packageName, expected.packageName)
+ return true
+ },
+ )
+}
+
+test('accepts all own tarballs with physical deduplication of isolated links', (t) => {
+ // Given
+ const fs = fixture(t)
+ // When
+ const receipt = verifyHostInstall(fs.options)
+ // Then
+ assert.equal(receipt.mode, 'host-only')
+ assert.deepEqual(
+ new Set(receipt.packages.map((item) => item.name)),
+ new Set(Object.keys(fs.options.tarballs)),
+ )
+ assert.equal(receipt.packages.length, Object.keys(fs.options.tarballs).length)
+ assert.equal(receipt.tarballs.length, Object.keys(fs.options.tarballs).length)
+ assert.deepEqual(receipt.omitted, fs.options.host.omitted)
+})
+
+test('rejects a nested payload byte mismatch even when the root copy matches', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = '@devup-ui/react'
+ const nested = join(
+ fs.directory,
+ 'node_modules/tool/node_modules',
+ packageName,
+ )
+ cpSync(fs.locations.get(packageName), nested, { recursive: true })
+ writeFileSync(join(nested, 'index.js.map'), 'different map bytes')
+ // When / Then
+ assertRejected(fs.options, { category: 'byte-mismatch', packageName })
+})
+
+test('rejects byte-identical registry provenance beside a valid file record', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = '@devup-ui/react'
+ cpSync(
+ fs.locations.get(packageName),
+ join(fs.directory, 'node_modules/tool/node_modules', packageName),
+ { recursive: true },
+ )
+ fs.lock.packages[`tool/${packageName}`] = [
+ `${packageName}@0.1.0`,
+ {},
+ 'https://registry.npmjs.org/byte-identical.tgz',
+ ]
+ fs.saveLock()
+ // When / Then
+ assertRejected(fs.options, { category: 'source-mismatch', packageName })
+})
+
+test('rejects a byte-identical local archive from a different canonical source', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = '@devup-ui/plugin-utils'
+ const other = join(fs.root, 'other source.tgz')
+ const original = fs.options.tarballs[packageName].slice('file:'.length)
+ cpSync(original, other)
+ assert.deepEqual(readFileSync(other), readFileSync(original))
+ fs.lock.packages[packageName] = [
+ `${packageName}@${other.split(sep).join('/')}`,
+ {},
+ fs.lock.packages[packageName][2],
+ ]
+ fs.saveLock()
+ // When / Then
+ assertRejected(fs.options, { category: 'source-mismatch', packageName })
+})
+
+test('rejects a missing host sibling rather than treating optional omission as success', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = fs.options.host.name
+ rmSync(join(fs.directory, 'node_modules', packageName), {
+ recursive: true,
+ force: true,
+ })
+ rmSync(fs.locations.get(packageName), { recursive: true, force: true })
+ // When / Then
+ assertRejected(fs.options, { category: 'missing-host', packageName })
+})
+
+test('rejects an unexpected nested Devup package outside the pack inventory', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = '@devup-ui/unexpected'
+ const extra = join(
+ fs.directory,
+ 'node_modules/tool/node_modules',
+ packageName,
+ )
+ mkdirSync(extra, { recursive: true })
+ writeFileSync(
+ join(extra, 'package.json'),
+ JSON.stringify({ name: packageName }),
+ )
+ // When / Then
+ assertRejected(fs.options, { category: 'unexpected-package', packageName })
+})
+
+test('rejects an orphan host cache entry that the binding cannot resolve', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = fs.options.host.name
+ rmSync(join(fs.directory, 'node_modules', packageName), {
+ recursive: true,
+ force: true,
+ })
+ // When / Then
+ assertRejected(fs.options, { category: 'missing-host', packageName })
+})
+
+test('rejects an installed nonhost sibling despite optional registry metadata being allowed', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = fs.options.host.omitted[0]
+ const extra = join(fs.directory, 'node_modules', packageName)
+ mkdirSync(extra, { recursive: true })
+ writeFileSync(
+ join(extra, 'package.json'),
+ JSON.stringify({ name: packageName }),
+ )
+ fs.lock.packages[packageName] = [`${packageName}@0.1.0`, {}]
+ fs.saveLock()
+ // When / Then
+ assertRejected(fs.options, { category: 'unexpected-package', packageName })
+})
+
+test('accepts exact archive SRI markers on every installed Devup package', (t) => {
+ // Given
+ const fs = fixture(t)
+ for (const [name, directory] of fs.locations) {
+ writeFileSync(
+ join(directory, '.bun-tag'),
+ Buffer.from(fs.lock.packages[name][2]),
+ )
+ }
+ // When
+ const receipt = verifyHostInstall(fs.options)
+ // Then
+ assert.deepEqual(
+ new Set(receipt.packages.map((item) => item.name)),
+ new Set(fs.locations.keys()),
+ )
+})
+
+test('rejects marker bytes with an appended newline', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = '@devup-ui/wasm'
+ writeFileSync(
+ join(fs.locations.get(packageName), '.bun-tag'),
+ `${fs.lock.packages[packageName][2]}\n`,
+ )
+ // When / Then
+ assertRejected(fs.options, { category: 'marker-bytes', packageName })
+})
+
+test('rejects an unknown extra file beside an attested marker', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = '@devup-ui/plugin-utils'
+ const directory = fs.locations.get(packageName)
+ writeFileSync(join(directory, '.bun-tag'), fs.lock.packages[packageName][2])
+ writeFileSync(join(directory, 'unknown-extra'), 'not in the archive')
+ // When / Then
+ assertRejected(fs.options, { category: 'byte-mismatch', packageName })
+})
+
+test('rejects a marker symlink including a Windows directory junction', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = '@devup-ui/react'
+ const target = join(fs.root, 'marker-target')
+ mkdirSync(target)
+ symlinkSync(
+ target,
+ join(fs.locations.get(packageName), '.bun-tag'),
+ process.platform === 'win32' ? 'junction' : 'dir',
+ )
+ // When / Then
+ assertRejected(fs.options, { category: 'marker-kind', packageName })
+})
+
+test('rejects a marker directory', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = '@devup-ui/react'
+ mkdirSync(join(fs.locations.get(packageName), '.bun-tag'))
+ // When / Then
+ assertRejected(fs.options, { category: 'marker-kind', packageName })
+})
+
+test('rejects a nested marker even with correct archive SRI bytes', (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = '@devup-ui/wasm'
+ const nested = join(fs.locations.get(packageName), 'nested')
+ mkdirSync(nested)
+ writeFileSync(join(nested, '.bun-tag'), fs.lock.packages[packageName][2])
+ // When / Then
+ assertRejected(fs.options, { category: 'marker-nested', packageName })
+})
+
+for (const marker of ['.bun-tag', 'nested/.bun-tag']) {
+ test(`rejects an archive containing ${marker}`, (t) => {
+ // Given
+ const fs = fixture(t, marker)
+ const packageName = '@devup-ui/react'
+ // When / Then
+ assertRejected(fs.options, { category: 'archive-marker', packageName })
+ })
+}
+
+for (const withMarker of [false, true]) {
+ test(`rejects mismatched lock integrity with marker ${withMarker}`, (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = '@devup-ui/wasm'
+ if (withMarker) {
+ writeFileSync(
+ join(fs.locations.get(packageName), '.bun-tag'),
+ fs.lock.packages[packageName][2],
+ )
+ }
+ fs.lock.packages[packageName][2] =
+ `sha512-${createHash('sha512').update('different archive').digest('base64')}`
+ fs.saveLock()
+ // When / Then
+ assertRejected(fs.options, {
+ category: 'lock-integrity-mismatch',
+ packageName,
+ })
+ })
+
+ test(`rejects missing lock integrity with marker ${withMarker}`, (t) => {
+ // Given
+ const fs = fixture(t)
+ const packageName = '@devup-ui/wasm'
+ if (withMarker) {
+ writeFileSync(
+ join(fs.locations.get(packageName), '.bun-tag'),
+ fs.lock.packages[packageName][2],
+ )
+ }
+ fs.lock.packages[packageName].pop()
+ fs.saveLock()
+ // When / Then
+ assertRejected(fs.options, {
+ category: 'lock-integrity-missing',
+ packageName,
+ })
+ })
+}
+
+function rsbuildReceipt() {
+ return {
+ compilationProofs: [],
+ generations: [{ outputs: { file: { css: 'early' } } }],
+ }
+}
+
+for (const [scope, fields] of [
+ ['receipt', ['local', 'additionalPasses', 'deleted', 'errors', 'future']],
+ [
+ 'generation',
+ [
+ 'assets',
+ 'emitted',
+ 'http',
+ 'css',
+ 'served',
+ 'sheet',
+ 'classes',
+ 'files',
+ 'canonical',
+ 'disk',
+ 'residual',
+ 'source',
+ 'errors',
+ 'future',
+ ],
+ ],
+ [
+ 'output',
+ ['code', 'map', 'cssFile', 'dependencies', 'updatedBaseStyle', 'future'],
+ ],
+]) {
+ for (const field of fields) {
+ test(`Rsbuild rejects changed ${scope}.${field} outside its named exclusions`, () => {
+ // Given
+ const expected = rsbuildReceipt()
+ const actual = rsbuildReceipt()
+ for (const [receipt, value] of [
+ [expected, 'before'],
+ [actual, 'after'],
+ ]) {
+ const targets = {
+ receipt,
+ generation: receipt.generations[0],
+ output: receipt.generations[0].outputs.file,
+ }
+ targets[scope][field] = value
+ }
+ // When / Then
+ assert.throws(
+ () => assertRsbuildParity(expected, actual),
+ assert.AssertionError,
+ )
+ })
+ }
+}
+
+for (const [scope, field] of [
+ ['receipt', 'compilationProofs'],
+ ['output', 'css'],
+]) {
+ for (const missing of [false, true]) {
+ test(`Rsbuild ${scope}.${field} variation has missing raw field ${missing}`, () => {
+ // Given
+ const expected = rsbuildReceipt()
+ const actual = rsbuildReceipt()
+ const parent = {
+ receipt: actual,
+ output: actual.generations[0].outputs.file,
+ }[scope]
+ if (missing) delete parent[field]
+ else parent[field] = 'different intermediate observation'
+ const raw = structuredClone(actual)
+ // When / Then
+ const compare = () => assertRsbuildParity(expected, actual)
+ if (missing) assert.throws(compare, assert.AssertionError)
+ else assert.doesNotThrow(compare)
+ assert.deepEqual(actual, raw)
+ })
+ }
+}
diff --git a/e2e/release/run.mjs b/e2e/release/run.mjs
index 90b6610f9..be8a3fd39 100644
--- a/e2e/release/run.mjs
+++ b/e2e/release/run.mjs
@@ -12,6 +12,10 @@ import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
+import {
+ packHostEngines,
+ verifyHostInstall,
+} from '../plugins/engine-fixtures.mjs'
import { prepareConsumer } from './consumer.mjs'
import { CommandExitError, runGuarded } from './guarded-process.mjs'
@@ -155,6 +159,16 @@ try {
assert.equal(files.length, 1)
tarballs[name] = `file:${join(destination, files[0]).replaceAll('\\', '/')}`
}
+ const native = await packHostEngines({ repository, root, bun }, run)
+ for (const [name, reference] of Object.entries(native.tarballs)) {
+ tarballs[name.slice('@devup-ui/'.length)] = reference
+ }
+ const installForHost = (command, args, options) =>
+ run(
+ command,
+ args[0] === 'install' ? [...args, ...native.host.installArgs] : args,
+ options,
+ )
const targets = (
process.env.RELEASE_TARGETS ??
'vite,rsbuild,webpack,next-webpack,next-turbopack'
@@ -162,7 +176,12 @@ try {
for (const name of targets) {
for (const singleCss of [false, true]) {
try {
- const directory = await prepareConsumer(name, { root, tarballs }, run)
+ const directory = await prepareConsumer(
+ name,
+ { root, tarballs },
+ installForHost,
+ )
+ verifyHostInstall({ directory, tarballs, host: native.host, bun })
for (const generated of ['df', 'dist', '.next', 'out'])
await rm(join(directory, generated), { recursive: true, force: true })
await buildAndCheck(name, directory, singleCss)
diff --git a/package.json b/package.json
index 29d815322..410defde0 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"lint": "cargo fmt --all -- --check && cargo clippy --all-targets --all-features -- -D warnings && node --require ./eslint-typescript6.cjs ./node_modules/eslint/bin/eslint.js",
"lint:fix": "node --require ./eslint-typescript6.cjs ./node_modules/eslint/bin/eslint.js --fix && cargo fmt",
"test": "node e2e/binding/native-coverage/run.mjs && bun run test:bun",
- "test:bun": "node -e \"require('node:fs').rmSync('coverage/root', { recursive: true, force: true })\" && bun test --coverage && bun run test:vite",
+ "test:bun": "node -e \"require('node:fs').rmSync('coverage/root', { recursive: true, force: true })\" && bun test --coverage && bun run test:vite && node --test e2e/plugins/engine.test.mjs",
"test:vite": "node -e \"require('node:fs').rmSync('coverage/vite', { recursive: true, force: true })\" && bun --config=./bunfig.vite.toml test --coverage",
"test:e2e": "bunx playwright test",
"test:e2e:next": "LANDING_BUILD_MODE=next bunx playwright test",
@@ -16,11 +16,11 @@
"test:release": "node e2e/release/run.mjs",
"build:native:artifact": "node e2e/binding/package/provenance.mjs",
"pack:native": "node e2e/binding/package/collect.mjs",
- "test:native:package": "node --test e2e/binding/package/package.test.mjs e2e/binding/package/publish.test.mjs e2e/binding/package/elf.test.mjs",
+ "test:native:package": "node --test e2e/binding/package/package.test.mjs e2e/binding/package/publish.test.mjs e2e/binding/package/elf.test.mjs e2e/binding/package/build-host.test.mjs",
"test:native:consumer": "node e2e/binding/package/consumer.mjs",
"test:e2e:ui": "bunx playwright test --ui",
"test:e2e:update": "bunx playwright test --update-snapshots",
- "build": "bun run --filter @devup-ui/wasm --filter @devup-ui/plugin-utils build && bun run --filter @devup-ui/react --filter @devup-ui/webpack-plugin build && bun run --filter @devup-ui/eslint-plugin --filter @devup-ui/vite-plugin --filter @devup-ui/next-plugin --filter @devup-ui/rsbuild-plugin --filter @devup-ui/bun-plugin --filter @devup-ui/components --filter @devup-ui/reset-css build",
+ "build": "node e2e/binding/package/build-host.mjs && bun run --filter @devup-ui/wasm --filter @devup-ui/plugin-utils build && bun run --filter @devup-ui/react --filter @devup-ui/webpack-plugin build && bun run --filter @devup-ui/eslint-plugin --filter @devup-ui/vite-plugin --filter @devup-ui/next-plugin --filter @devup-ui/rsbuild-plugin --filter @devup-ui/bun-plugin --filter @devup-ui/components --filter @devup-ui/reset-css build",
"dev": "bun run --filter '*' dev",
"verify:dist": "bun verify-dist.ts",
"benchmark": "bun benchmark.js",
diff --git a/packages/bun-plugin/README.md b/packages/bun-plugin/README.md
index 289863c3e..03b5d0223 100644
--- a/packages/bun-plugin/README.md
+++ b/packages/bun-plugin/README.md
@@ -60,6 +60,21 @@ module in the bundle. Imports of the packages Devup UI takes the place of
Class names are short in `Bun.build` and readable under the runtime; pass
`debug` to choose.
+## Compiler Engine
+
+The native compiler is selected by default, including the zero-config preload.
+To select WASM, use the configurable registration entry and await setup:
+
+```ts
+import { register } from '@devup-ui/bun-plugin/register'
+await register({ engine: 'wasm' })
+```
+
+`DevupUI({ engine: 'wasm' })` selects WASM for `Bun.build` too. Native load
+errors retain their cause and identify the missing platform package; they never
+silently select WASM. Each factory retains its selected compiler for later loads
+and theme reloads. Independent concurrent builds in one process are unsupported.
+
## Custom Shorthands
To configure custom shorthands, preload a local module instead of the
diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json
index afe2f3fc4..9c3a13136 100644
--- a/packages/bun-plugin/package.json
+++ b/packages/bun-plugin/package.json
@@ -52,8 +52,5 @@
"devDependencies": {
"@types/bun": "^1.4.4",
"typescript": "^7.0.2"
- },
- "peerDependencies": {
- "@devup-ui/wasm": "*"
}
}
diff --git a/packages/bun-plugin/src/__tests__/captured-hooks.test.ts b/packages/bun-plugin/src/__tests__/captured-hooks.test.ts
index e9c149f63..f9f879f4d 100644
--- a/packages/bun-plugin/src/__tests__/captured-hooks.test.ts
+++ b/packages/bun-plugin/src/__tests__/captured-hooks.test.ts
@@ -24,7 +24,7 @@ it('releases a rejected setup and restores spies before subsequent registration'
// Given: real plugin setup rejects after registering its real session end.
const failure = new Error('initialization write rejected')
hooks.write.mockRejectedValueOnce(failure)
- await expect(register()).rejects.toBe(failure)
+ await expect(register({ engine: 'wasm' })).rejects.toBe(failure)
hooks.expectedErrors.add(failure)
let ended = 0
const terminals = hooks.ends.splice(0)
@@ -42,7 +42,7 @@ it('releases a rejected setup and restores spies before subsequent registration'
expect(Bun.plugin).toBe(realPlugin)
expect(bunRegistration.clearAll).toBe(realPlugin.clearAll)
hooks = captureBunHooks()
- await register({ debug: false })
+ await register({ engine: 'wasm', debug: false })
expect(hooks.loads.length).toBe(2)
expect(wasm.isDebug()).toBe(false)
})
@@ -50,7 +50,7 @@ it('releases a rejected setup and restores spies before subsequent registration'
it('keeps unacknowledged setup failures as teardown errors after releasing sessions', async () => {
const failure = new Error('unexpected setup failure')
hooks.write.mockRejectedValueOnce(failure)
- await expect(register()).rejects.toBe(failure)
+ await expect(register({ engine: 'wasm' })).rejects.toBe(failure)
let ended = false
hooks.ends.push(() => {
ended = true
diff --git a/packages/bun-plugin/src/__tests__/order-independent-build.helper.ts b/packages/bun-plugin/src/__tests__/order-independent-build.helper.ts
index 11b5bc2e9..5c81cc5bd 100644
--- a/packages/bun-plugin/src/__tests__/order-independent-build.helper.ts
+++ b/packages/bun-plugin/src/__tests__/order-independent-build.helper.ts
@@ -75,7 +75,7 @@ function decorate(build: PluginBuilder): PluginBuilder {
})
}
-const genuine = DevupUI()
+const genuine = DevupUI({ engine: 'wasm' })
const observed: BunPlugin = {
...genuine,
async setup(build) {
diff --git a/packages/bun-plugin/src/__tests__/plugin.test.ts b/packages/bun-plugin/src/__tests__/plugin.test.ts
index 485d6b7dd..6e2ea54a2 100644
--- a/packages/bun-plugin/src/__tests__/plugin.test.ts
+++ b/packages/bun-plugin/src/__tests__/plugin.test.ts
@@ -3,8 +3,8 @@ import { tmpdir } from 'node:os'
import * as nodePath from 'node:path'
import { join } from 'node:path'
+import * as pluginUtils from '@devup-ui/plugin-utils'
import { ShorthandConfigError } from '@devup-ui/plugin-utils'
-import * as wasm from '@devup-ui/wasm'
import {
afterAll,
afterEach,
@@ -19,13 +19,30 @@ import { bunRegistration } from '../../../../bun.setup'
import { DevupUI, register } from '../plugin'
import { captureBunHooks } from './captured-hooks.test-support'
+const selectCompiler = pluginUtils.loadCompiler
+const wasm = selectCompiler('wasm')
+
describe('shorthand creation boundary', () => {
+ it.each([undefined, 'native', 'wasm'] as const)(
+ 'selects engine=%s once per factory',
+ (engine) => {
+ const selection = spyOn(pluginUtils, 'loadCompiler').mockReturnValue(wasm)
+ try {
+ DevupUI(engine === undefined ? undefined : { engine })
+ expect(selection.mock.calls).toEqual([[engine]])
+ } finally {
+ selection.mockRestore()
+ selection.mockImplementation(selectCompiler)
+ }
+ },
+ )
it('registers canonical targets when the plugin is registered', async () => {
const registration = spyOn(wasm, 'registerShorthands').mockReturnValue(
undefined,
)
try {
await register({
+ engine: 'wasm',
shorthands: { bgx: ['backgroundColor', 'py', '--Gap'] },
})
expect(registration.mock.calls).toEqual([
@@ -41,6 +58,7 @@ describe('shorthand creation boundary', () => {
})
it('rejects a typo before registration when runtime options bypass types', () => {
+ const selection = spyOn(pluginUtils, 'loadCompiler')
const registration = spyOn(wasm, 'registerShorthands').mockReturnValue(
undefined,
)
@@ -51,8 +69,11 @@ describe('shorthand creation boundary', () => {
]),
).toThrow(ShorthandConfigError)
expect(registration).not.toHaveBeenCalled()
+ expect(selection).not.toHaveBeenCalled()
} finally {
registration.mockRestore()
+ selection.mockRestore()
+ selection.mockImplementation(selectCompiler)
}
})
})
@@ -97,6 +118,81 @@ async function load(path: string, namespace = 'file') {
}
describe('registered Bun hooks', () => {
+ it('compiles default runtime input into native state without mutating WASM', async () => {
+ const native = pluginUtils.loadCompiler()
+ native.resetStateForTesting()
+ const file = join(root, 'native-default.ts')
+ await Bun.write(
+ file,
+ "import {css} from '@devup-ui/react'; export const cls=css({color:'teal'});",
+ )
+ const before = {
+ native: native.getCss(null, false),
+ wasm: wasm.getCss(null, false),
+ }
+ try {
+ await register({ debug: false })
+ const result = await load(file)
+ const after = {
+ native: native.getCss(null, false),
+ wasm: wasm.getCss(null, false),
+ }
+ console.info(
+ JSON.stringify({ observed: 'native-default', before, after, result }),
+ )
+ expect(before.native).not.toContain('color:teal')
+ expect(result && 'contents' in result && result.contents).not.toContain(
+ 'css(',
+ )
+ expect(after.native).toContain('color:teal')
+ expect(after.wasm).toBe(before.wasm)
+ } finally {
+ native.resetStateForTesting()
+ }
+ })
+
+ it('keeps retained source callbacks on their factory compiler', async () => {
+ const native = pluginUtils.loadCompiler()
+ native.resetStateForTesting()
+ const file = join(root, 'retained-native.ts')
+ await Bun.write(
+ file,
+ "import {css} from '@devup-ui/react'; export const cls=css({color:'teal'});",
+ )
+ try {
+ await register({ engine: 'native', debug: false })
+ const retained = loads.splice(0)
+ await register({ engine: 'wasm', debug: false })
+ const before = {
+ native: native.getCss(null, false),
+ wasm: wasm.getCss(null, false),
+ }
+ const source = retained.find(({ options }) => !options.namespace)
+ if (!source) throw new Error('Missing retained native source callback')
+ const result = await source.callback({
+ path: file,
+ namespace: 'file',
+ loader: 'file',
+ defer: async () => {},
+ })
+ const after = {
+ native: native.getCss(null, false),
+ wasm: wasm.getCss(null, false),
+ }
+ console.info(
+ JSON.stringify({ observed: 'retained-native', before, after, result }),
+ )
+ expect(before.native).not.toContain('color:teal')
+ expect(result && 'contents' in result && result.contents).not.toContain(
+ 'css(',
+ )
+ expect(after.native).toContain('color:teal')
+ expect(after.wasm).toBe(before.wasm)
+ } finally {
+ native.resetStateForTesting()
+ }
+ })
+
it('seeds readable names from physical inputs before runtime source loads', async () => {
const file = join(root, 'numbered.tsx')
await Bun.write(
@@ -108,7 +204,7 @@ describe('registered Bun hooks', () => {
paths.length === 1 && paths[0] === 'src' ? root : realResolve(...paths),
)
try {
- await register()
+ await register({ engine: 'wasm' })
expect(Object.keys(JSON.parse(wasm.exportFileMap()))).toContain(file)
expect(wasm.isDebug()).toBe(true)
} finally {
@@ -122,7 +218,7 @@ describe('registered Bun hooks', () => {
})
it('reserves short runtime names during awaited setup', async () => {
- await register({ debug: false })
+ await register({ engine: 'wasm', debug: false })
expect(wasm.isDebug()).toBe(false)
expect(loads.length).toBe(2)
})
@@ -131,7 +227,7 @@ describe('registered Bun hooks', () => {
const path = join(root, 'malformed.ts')
const source = 'import {'
await Bun.write(path, source)
- await register()
+ await register({ engine: 'wasm' })
expect(await load(path)).toEqual({ contents: source, loader: 'ts' })
expect(writeSync).not.toHaveBeenCalled()
})
@@ -149,7 +245,7 @@ describe('registered Bun hooks', () => {
target: 'bun',
external: ['react/jsx-dev-runtime', 'react/jsx-runtime'],
outdir: join(root, 'out'),
- plugins: [DevupUI()],
+ plugins: [DevupUI({ engine: 'wasm' })],
})
expect(result.success).toBe(true)
expect(result.logs).toEqual([])
@@ -166,7 +262,7 @@ describe('registered Bun hooks', () => {
})
it('creates cold directories before writing configuration and CSS', async () => {
- await register()
+ await register({ engine: 'wasm' })
expect(mkdir).toHaveBeenCalledWith('df', { recursive: true })
expect(mkdir).toHaveBeenCalledWith(expect.stringContaining('devup-ui'), {
recursive: true,
@@ -184,7 +280,7 @@ describe('registered Bun hooks', () => {
readConfig.mockResolvedValue(
'{"theme":{"colors":{"default":{"primary":"#123456"}}}}',
)
- await register()
+ await register({ engine: 'wasm' })
expect(wasm.getCss(null, false)).toContain('#123456')
expect(mkdir).not.toHaveBeenCalled()
})
@@ -192,12 +288,12 @@ describe('registered Bun hooks', () => {
it('registers an empty theme when reading configuration fails', async () => {
exists.mockReturnValue(true)
readConfig.mockRejectedValue(new Error('unreadable fixture config'))
- await register()
+ await register({ engine: 'wasm' })
expect(wasm.getDefaultTheme()).toBeUndefined()
})
it('resolves generated stylesheets and loads their runtime placeholder', async () => {
- await register()
+ await register({ engine: 'wasm' })
const resolve = resolves[0]
if (!resolve) throw new Error('No CSS resolver registered')
const result = await resolve({
@@ -232,7 +328,7 @@ describe('registered Bun hooks', () => {
`source.${extension}`,
)
await Bun.write(path, source)
- await register()
+ await register({ engine: 'wasm' })
const result = await load(path)
if (!result || !('contents' in result))
throw new Error('Expected transformed source contents')
@@ -251,7 +347,7 @@ describe('registered Bun hooks', () => {
it('returns source without style imports unchanged', async () => {
const path = join(root, 'plain.ts')
await Bun.write(path, 'export const value = 42')
- await register()
+ await register({ engine: 'wasm' })
expect(await load(path)).toEqual({
contents: 'export const value = 42',
loader: 'ts',
@@ -260,7 +356,7 @@ describe('registered Bun hooks', () => {
})
it('propagates source read errors instead of returning partial output', async () => {
- await register()
+ await register({ engine: 'wasm' })
await expect(load(join(root, 'missing.ts'))).rejects.toThrow()
})
@@ -270,7 +366,7 @@ describe('registered Bun hooks', () => {
path,
"import { css } from '@devup-ui/react'; export const styles = css({ color: window.name })",
)
- await register()
+ await register({ engine: 'wasm' })
await expect(load(path)).rejects.toThrow(/invalid\.ts:\d+:\d+:/)
})
})
diff --git a/packages/bun-plugin/src/__tests__/reservation-lifecycle.test.ts b/packages/bun-plugin/src/__tests__/reservation-lifecycle.test.ts
index bb18ca851..5a83376c8 100644
--- a/packages/bun-plugin/src/__tests__/reservation-lifecycle.test.ts
+++ b/packages/bun-plugin/src/__tests__/reservation-lifecycle.test.ts
@@ -50,7 +50,7 @@ writeFileSync('src/Unused.tsx','import {Box} from "@devup-ui/react"; target.onStart(()=>{events.push('devup-onStart-enter');const result=callback();events.push('devup-onStart-exit');return result;});
@@ -85,7 +85,7 @@ writeFileSync('node_modules/@devup-ui/reservation-fixture/dist/index.js','import
writeFileSync('src/Entry.tsx','export {built} from "@devup-ui/reservation-fixture";');
const outputs=[];
for(let i=0;i<2;i++){
- const result=await Bun.build({entrypoints:['./src/Entry.tsx'],plugins:[DevupUI()]});
+ const result=await Bun.build({entrypoints:['./src/Entry.tsx'],plugins:[DevupUI({ engine: 'wasm' })]});
assert(result.success,result.logs.map(String).join('\n'));
assert.equal(engine.getUnreservedClassSlotCount(),0);
const css=(await Promise.all(result.outputs.filter(o=>o.path.endsWith('.css')).map(o=>o.text()))).join('\n');
@@ -108,7 +108,7 @@ const engine=await import(${JSON.stringify(wasmUrl)});
writeFileSync('src/A.ts','import {css} from "@devup-ui/react"; export const cls=css({color:"red"});');
writeFileSync('src/B.ts','import {css} from "@devup-ui/react"; export const cls=css({background:"blue"});');
writeFileSync('src/Unused.ts','import {css} from "@devup-ui/react"; export const cls=css({color:"magenta"});');
-await register(${mode === 'short' ? '{debug:false}' : '{}'});
+await register(${mode === 'short' ? '{engine:"wasm",debug:false}' : '{engine:"wasm"}'});
assert.equal(engine.isDebug(),${mode !== 'short'});
const b=await import(process.cwd()+'/src/B.ts');
const a=await import(process.cwd()+'/src/A.ts');
diff --git a/packages/bun-plugin/src/__tests__/reservation-roots.cjs b/packages/bun-plugin/src/__tests__/reservation-roots.cjs
index c94f4ade7..ce7d770f5 100644
--- a/packages/bun-plugin/src/__tests__/reservation-roots.cjs
+++ b/packages/bun-plugin/src/__tests__/reservation-roots.cjs
@@ -27,7 +27,7 @@ async function main() {
const result = await build({
entrypoints: ['./index.ts'],
outdir: './out',
- plugins: [DevupUI()],
+ plugins: [DevupUI({ engine: 'wasm' })],
})
assert(result.success, result.logs.map(String).join('\n'))
for (const output of result.outputs) {
@@ -36,7 +36,10 @@ async function main() {
writeFileSync(output.path, bytes)
}
} else {
- await register(mode === 'default' ? {} : { debug: false })
+ await register({
+ engine: 'wasm',
+ ...(mode === 'default' ? {} : { debug: false }),
+ })
const module = await import(
resolve(mode === 'outside' ? 'outside.ts' : 'src/A.ts')
)
diff --git a/packages/bun-plugin/src/plugin.ts b/packages/bun-plugin/src/plugin.ts
index b425d8469..284772623 100644
--- a/packages/bun-plugin/src/plugin.ts
+++ b/packages/bun-plugin/src/plugin.ts
@@ -10,7 +10,10 @@ import {
createSharedSlotReservation,
createThemeInterfaceArgs,
type CustomShorthands,
+ type DevupCompiler,
+ type DevupEngine,
isProjectJsx,
+ loadCompiler,
loadDevupConfig,
mergeImportAliases,
normalizeShorthands,
@@ -19,20 +22,6 @@ import {
tailwindCssFiles,
withTailwindCss,
} from '@devup-ui/plugin-utils'
-import {
- codeExtract,
- getCss,
- getThemeInterface,
- hasDevupUI,
- registerShorthands,
- registerTheme,
- reserveClassSlots,
- resetBuildState,
- seedFileMap,
- setDebug,
- setModuleResolver,
- setPrefix,
-} from '@devup-ui/wasm'
import { type BunPlugin, plugin, type PluginBuilder } from 'bun'
import { cssDirName, cssNamespace, resolveCssId } from './css-id'
@@ -53,6 +42,8 @@ const compiledPackages = [
]
export interface DevupUIBunPluginOptions {
+ /** Native by default. Select WASM explicitly without automatic fallback. */
+ engine?: DevupEngine
shorthands?: CustomShorthands
/**
* Readable class names. Defaults to `true` under the Bun runtime (tests) and
@@ -63,7 +54,7 @@ export interface DevupUIBunPluginOptions {
type SourceLoader = 'tsx' | 'ts' | 'jsx' | 'js'
-async function writeDataFiles(): Promise {
+async function writeDataFiles(compiler: DevupCompiler): Promise {
let theme = {}
let tailwindFiles: string[] = []
try {
@@ -73,28 +64,34 @@ async function writeDataFiles(): Promise {
} catch {
// Error reading devup.json, use empty theme
}
- registerTheme(theme)
+ compiler.registerTheme(theme)
// Generate theme interface after registration (always write, even if empty)
await writeFile(
join(distDir, 'theme.d.ts'),
- getThemeInterface(...createThemeInterfaceArgs(libPackage)),
+ compiler.getThemeInterface(...createThemeInterfaceArgs(libPackage)),
'utf-8',
)
if (!existsSync(cssDir)) {
await mkdir(cssDir, { recursive: true })
}
- await writeFile(join(cssDir, 'devup-ui.css'), getCss(null, false), 'utf-8')
+ await writeFile(
+ join(cssDir, 'devup-ui.css'),
+ compiler.getCss(null, false),
+ 'utf-8',
+ )
return tailwindFiles
}
-const watchTailwindCss = createTailwindWatcher(writeDataFiles)
-
-async function initialize(shorthands: Record) {
- registerShorthands(shorthands)
- setPrefix(null)
- setModuleResolver(createModuleResolver())
+async function initialize(
+ compiler: DevupCompiler,
+ shorthands: Record,
+ watchTailwindCss: ReturnType,
+) {
+ compiler.registerShorthands(shorthands)
+ compiler.setPrefix(null)
+ compiler.setModuleResolver(createModuleResolver())
if (!existsSync(distDir)) await mkdir(distDir, { recursive: true })
await writeFile(join(distDir, '.gitignore'), '*', 'utf-8')
await writeFile(
@@ -102,7 +99,7 @@ async function initialize(shorthands: Record) {
createCompatTypes(importAliases),
'utf-8',
)
- watchTailwindCss(await writeDataFiles())
+ watchTailwindCss(await writeDataFiles(compiler))
}
const scanners = new Map()
@@ -128,7 +125,11 @@ function importsCompiledPackage(contents: string, loader: SourceLoader) {
}
}
-async function loadSourceFile(filePath: string, bundling: boolean) {
+async function loadSourceFile(
+ compiler: DevupCompiler,
+ filePath: string,
+ bundling: boolean,
+) {
const loader: SourceLoader = filePath.endsWith('.tsx')
? 'tsx'
: filePath.endsWith('.ts')
@@ -141,9 +142,9 @@ async function loadSourceFile(filePath: string, bundling: boolean) {
if (
isProjectJsx(filePath, importAliases) ||
importsCompiledPackage(contents, loader) ||
- hasDevupUI(filePath, contents, libPackage, importAliases)
+ compiler.hasDevupUI(filePath, contents, libPackage, importAliases)
) {
- const code = codeExtract(
+ using code = compiler.codeExtract(
filePath,
contents,
libPackage,
@@ -157,7 +158,11 @@ async function loadSourceFile(filePath: string, bundling: boolean) {
// every extracted style in the base sheet; synchronous writes keep
// concurrent source loads from overwriting a newer sheet with an older one.
if (!bundling)
- writeFileSync(join(cssDir, 'devup-ui.css'), getCss(null, false), 'utf-8')
+ writeFileSync(
+ join(cssDir, 'devup-ui.css'),
+ compiler.getCss(null, false),
+ 'utf-8',
+ )
return { contents: code.code, loader }
}
return { contents, loader }
@@ -169,6 +174,10 @@ async function loadSourceFile(filePath: string, bundling: boolean) {
*/
function DevupUI(options: DevupUIBunPluginOptions = {}) {
const shorthands = normalizeShorthands(options.shorthands ?? {})
+ const compiler = loadCompiler(options.engine)
+ const watchTailwindCss = createTailwindWatcher(
+ writeDataFiles.bind(undefined, compiler),
+ )
return {
name: 'devup-ui',
@@ -177,12 +186,12 @@ function DevupUI(options: DevupUIBunPluginOptions = {}) {
const bundling = build.config !== undefined
// A build starts from its own options, not from what an earlier build in
// this process left in the engine
- const endBuild = beginBuild({ resetBuildState })
+ const endBuild = beginBuild(compiler)
build.onEnd?.(endBuild)
- await initialize(shorthands)
+ await initialize(compiler, shorthands, watchTailwindCss)
const debug = options.debug ?? !bundling
- setDebug(debug)
- const slots = createSharedSlotReservation({ reserveClassSlots })
+ compiler.setDebug(debug)
+ const slots = createSharedSlotReservation(compiler)
let reservationPending = bundling
const reserveFiles = () => {
slots.invalidate()
@@ -217,7 +226,7 @@ function DevupUI(options: DevupUIBunPluginOptions = {}) {
else {
try {
seedFileNumbers(
- { seedFileMap },
+ compiler,
collectNumberedFiles({
roots: [resolve('src')],
needles: compiledPackages,
@@ -247,7 +256,7 @@ function DevupUI(options: DevupUIBunPluginOptions = {}) {
async ({ defer }) => {
if (!bundling) return { contents: '', loader: 'js' }
await defer()
- return { contents: getCss(null, false), loader: 'css' }
+ return { contents: compiler.getCss(null, false), loader: 'css' }
},
)
@@ -261,7 +270,7 @@ function DevupUI(options: DevupUIBunPluginOptions = {}) {
reserveFiles()
reservationPending = false
}
- return loadSourceFile(path, bundling)
+ return loadSourceFile(compiler, path, bundling)
},
)
},
diff --git a/packages/next-plugin/README.md b/packages/next-plugin/README.md
index facbc7247..631d06541 100644
--- a/packages/next-plugin/README.md
+++ b/packages/next-plugin/README.md
@@ -205,6 +205,20 @@ custom `distDir`) to `include`.
```
+## Extraction engine
+
+The build-time compiler defaults to `native` for Webpack and Turbopack. Select
+WASM explicitly with `DevupUI(nextConfig, { engine: 'wasm' })`. The chosen engine
+is retained by configuration, prewarm, extraction, CSS and delayed callbacks.
+Turbopack loader options carry only the engine string; the coordinator retains
+the selected physical compiler. Independent concurrent builds in one process
+are unsupported.
+
+Native load failures identify the required platform package and retain the
+original cause. There is no automatic fallback to WASM. Install optional native
+dependencies normally, or choose WASM explicitly for an unsupported platform.
+Both engines extract CSS during the build and add no client styling runtime.
+
## Turbopack build profiling
Set `DEVUP_UI_PROFILE=1` for an opt-in, structured timing log during a
@@ -221,6 +235,6 @@ $env:DEVUP_UI_PROFILE = '1'; bun run build
Each `[devup-ui:profile]` JSON entry reports one phase. `next.graph` measures
the static import-graph pre-pass, `next.prewarm` measures production extraction
before Turbopack starts loaders, and `coordinator.extract` separates request,
-WASM extraction, CSS/state serialization, and write time for each loaded
+compiler extraction, CSS/state serialization, and write time for each loaded
module. The setting is disabled by default and does not collect timings or
write logs when it is absent.
diff --git a/packages/next-plugin/package.json b/packages/next-plugin/package.json
index 27f4e4078..fc8bd81e1 100644
--- a/packages/next-plugin/package.json
+++ b/packages/next-plugin/package.json
@@ -61,7 +61,6 @@
},
"peerDependencies": {
"@devup-ui/webpack-plugin": "*",
- "next": "*",
- "@devup-ui/wasm": "*"
+ "next": "*"
}
}
diff --git a/packages/next-plugin/src/__tests__/native-webpack-observer.cjs b/packages/next-plugin/src/__tests__/native-webpack-observer.cjs
index 5e34da6ac..08884eb05 100644
--- a/packages/next-plugin/src/__tests__/native-webpack-observer.cjs
+++ b/packages/next-plugin/src/__tests__/native-webpack-observer.cjs
@@ -2,7 +2,7 @@ const assert = require('node:assert/strict')
const { EventEmitter } = require('node:events')
const { appendFileSync } = require('node:fs')
const { join } = require('node:path')
-const wasm = require('@devup-ui/wasm')
+const wasm = require('@devup-ui/plugin-utils').loadCompiler()
const { DevupUIWebpackPlugin } = require('@devup-ui/webpack-plugin')
const { parseClasses, parseSlots } = require('./native-webpack-receipt.ts')
const { assertReserved } = require('./native-webpack-checks.ts')
diff --git a/packages/next-plugin/src/__tests__/plugin.test.ts b/packages/next-plugin/src/__tests__/plugin.test.ts
index 6baa9b6b0..66270541c 100644
--- a/packages/next-plugin/src/__tests__/plugin.test.ts
+++ b/packages/next-plugin/src/__tests__/plugin.test.ts
@@ -1,3 +1,4 @@
+import { spawnSync } from 'node:child_process'
import * as fs from 'node:fs'
import { join, resolve } from 'node:path'
@@ -16,12 +17,17 @@ import {
} from 'bun:test'
import * as coordinatorModule from '../coordinator'
+import cssLoader, { resetInit as resetCssInit } from '../css-loader'
import {
DevupUI,
reloadTurboSetupModuleForTesting,
resetTurboSetupCacheForTesting,
} from '../plugin'
-import { setWasmForTesting, setWebpackPluginForTesting } from '../wasm'
+import {
+ loadWasm,
+ setWasmForTesting,
+ setWebpackPluginForTesting,
+} from '../wasm'
type CodeExtractResult = ReturnType
type NextWebpackConfig = Parameters<
@@ -247,7 +253,155 @@ describe('DevupUINextPlugin', () => {
expect(startCoordinatorSpy).toHaveBeenCalledTimes(1)
})
+ it('prewarms and serves actual default Turbo extraction through native state', () => {
+ const child = spawnSync(
+ process.execPath,
+ [
+ '-e',
+ `
+ import assert from 'node:assert/strict';
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
+ import { tmpdir } from 'node:os';
+ import { join } from 'node:path';
+ import { DevupUI } from ${JSON.stringify(new URL('../plugin.ts', import.meta.url).href)};
+ import loader from ${JSON.stringify(new URL('../loader.ts', import.meta.url).href)};
+ import { loadWasm, withModuleResolver } from ${JSON.stringify(new URL('../wasm.ts', import.meta.url).href)};
+ import { flushCoordinatorWrites, resetCoordinator } from ${JSON.stringify(new URL('../coordinator.ts', import.meta.url).href)};
+ import { loadCompiler } from ${JSON.stringify(new URL('../../../plugin-utils/src/index.ts', import.meta.url).href)};
+ const native = loadCompiler('native'), wasm = loadCompiler('wasm');
+ const primedWasm = loadWasm('wasm'); // Resolve the old loader dependency at its installed location.
+ const cwd = process.cwd(), root = mkdtempSync(join(tmpdir(), 'devup-next-native-'));
+ try {
+ mkdirSync(join(root, 'src/app'), { recursive: true });
+ writeFileSync(join(root, 'tsconfig.json'), JSON.stringify({ compilerOptions: { jsx: 'react-jsx' }, include: ['src'] }));
+ writeFileSync(join(root, 'src/tokens.ts'), "export const tone = 'teal';");
+ const source = "import {Box} from '@devup-ui/react'; import {tone} from '../tokens'; export default function Page(){return ;}";
+ const resourcePath = join(root, 'src/app/page.tsx');
+ writeFileSync(resourcePath, source);
+ process.chdir(root);
+ native.resetStateForTesting(); wasm.resetStateForTesting();
+ withModuleResolver(primedWasm); // Match normal fixed-cwd supplier resolution after fixture setup.
+ const before = { native: native.getCss(null, false), wasm: wasm.getCss(null, false) };
+ const config = DevupUI({}, { singleCss: true, debug: false });
+ const options = config.turbopack.rules['*.{tsx,ts,jsx,js,mjs}'].loaders[0].options;
+ const code = await new Promise((resolve, reject) => loader.call({
+ resourcePath, getOptions: () => options, addDependency() {},
+ async: () => (error, code) => error ? reject(error) : resolve(code),
+ }, Buffer.from(source)));
+ const after = { native: native.getCss(null, false), wasm: wasm.getCss(null, false) };
+ console.info(JSON.stringify({ observed: 'next-default-native', before, after, code }));
+ assert.equal(typeof code, 'string'); assert(!code.includes(' {
+ resetCssInit()
+ const failure = new Error('consumer callback failure', {
+ cause: new Error('consumer cause'),
+ })
+ let callbacks = 0
+ const context = {
+ resourcePath: 'df/devup-ui/devup-ui.css',
+ getOptions: () => ({
+ watch: false,
+ defaultFileMap: {},
+ defaultClassMap: {},
+ defaultSheet: {},
+ }),
+ callback() {
+ callbacks++
+ throw failure
+ },
+ }
+ expect(() =>
+ Reflect.apply(cssLoader, context, [Buffer.from('ready')]),
+ ).toThrow(failure)
+ expect(callbacks).toBe(1)
+ resetCssInit()
+ })
+
+ it('retains original CSS compiler failure and cause in one callback', () => {
+ resetCssInit()
+ const cause = new Error('compiler cause')
+ const failure = new Error('compiler failure', { cause })
+ getCssSpy.mockImplementation(() => {
+ throw failure
+ })
+ const received: (Error | null)[] = []
+ Reflect.apply(
+ cssLoader,
+ {
+ resourcePath: 'df/devup-ui/devup-ui.css',
+ getOptions: () => ({
+ watch: true,
+ sheetFile: 'sheet',
+ classMapFile: 'classes',
+ fileMapFile: 'files',
+ themeFile: 'theme',
+ }),
+ callback(error: Error | null) {
+ received.push(error)
+ },
+ },
+ [Buffer.from('ready')],
+ )
+ expect(received).toEqual([failure])
+ expect(received[0]?.cause).toBe(cause)
+ resetCssInit()
+ })
+
+ it('caches each selected physical compiler and rejects invalid engines', () => {
+ setWasmForTesting(undefined)
+ const native = importGraphModule.loadCompiler('native')
+ const explicitWasm = importGraphModule.loadCompiler('wasm')
+ expect(loadWasm()).toBe(native)
+ expect(loadWasm('native')).toBe(native)
+ expect(loadWasm('wasm')).toBe(explicitWasm)
+ expect(loadWasm('wasm')).toBe(explicitWasm)
+ expect(native).not.toBe(explicitWasm)
+ expect(() => Reflect.apply(loadWasm, undefined, ['other'])).toThrow(
+ importGraphModule.InvalidEngineError,
+ )
+ expect(() => Reflect.apply(loadWasm, undefined, [null])).toThrow(
+ importGraphModule.InvalidEngineError,
+ )
+ })
+
describe('webpack', () => {
+ it.each(['native', 'wasm'] as const)(
+ 'retains %s in a deferred Webpack callback',
+ (engine) => {
+ delete process.env.TURBOPACK
+ const options = { engine }
+ const config = DevupUI({}, options)
+ options.engine = engine === 'native' ? 'wasm' : 'native'
+ config.webpack?.(createWebpackConfig(), createWebpackContext())
+ expect(devupUIWebpackPluginSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ engine }),
+ )
+ },
+ )
+
it.each([
['omitted', {}],
['undefined', { atomHoist: undefined }],
@@ -265,6 +419,7 @@ describe('DevupUINextPlugin', () => {
// Then: explicit zero/thresholds and singleCss reach the real adapter seam.
expect(devupUIWebpackPluginSpy).toHaveBeenCalledWith({
...options,
+ engine: 'native',
singleCss: true,
cssDir: resolve('.next/cache', 'devup-ui'),
productionPrewarmFiles: [],
@@ -279,6 +434,7 @@ describe('DevupUINextPlugin', () => {
expect(devupUIWebpackPluginSpy).toHaveBeenCalledWith({
cssDir: resolve('.next/cache', 'devup-ui'),
+ engine: 'native',
productionPrewarmFiles: [],
})
})
@@ -290,6 +446,7 @@ describe('DevupUINextPlugin', () => {
expect(devupUIWebpackPluginSpy).toHaveBeenCalledWith({
cssDir: resolve('df', 'devup-ui_tmpBuildId'),
+ engine: 'native',
watch: true,
})
})
@@ -306,6 +463,7 @@ describe('DevupUINextPlugin', () => {
expect(devupUIWebpackPluginSpy).toHaveBeenCalledWith({
package: 'new-package',
+ engine: 'native',
cssDir: resolve('.next/cache', 'devup-ui'),
productionPrewarmFiles: [],
})
@@ -326,6 +484,7 @@ describe('DevupUINextPlugin', () => {
expect(devupUIWebpackPluginSpy).toHaveBeenCalledWith({
package: 'new-package',
+ engine: 'native',
cssDir: resolve('.next/cache', 'devup-ui'),
productionPrewarmFiles: [],
})
@@ -520,6 +679,7 @@ describe('DevupUINextPlugin', () => {
{
loader: '@devup-ui/next-plugin/css-loader',
options: {
+ engine: 'native',
watch: false,
coordinatorPortFile: join('df', 'coordinator.port'),
sheetFile: join('df', 'sheet.json'),
@@ -545,6 +705,7 @@ describe('DevupUINextPlugin', () => {
{
loader: '@devup-ui/next-plugin/loader',
options: {
+ engine: 'native',
package: '@devup-ui/react',
cssDir: resolve('df', 'devup-ui'),
coordinatorPortFile: join('df', 'coordinator.port'),
@@ -606,6 +767,7 @@ describe('DevupUINextPlugin', () => {
{
loader: '@devup-ui/next-plugin/css-loader',
options: {
+ engine: 'native',
watch: false,
coordinatorPortFile: join('df', 'coordinator.port'),
sheetFile: join('df', 'sheet.json'),
@@ -643,6 +805,7 @@ describe('DevupUINextPlugin', () => {
{
loader: '@devup-ui/next-plugin/loader',
options: {
+ engine: 'native',
package: '@devup-ui/react',
cssDir: resolve('df', 'devup-ui'),
coordinatorPortFile: join('df', 'coordinator.port'),
@@ -700,6 +863,7 @@ describe('DevupUINextPlugin', () => {
{
loader: '@devup-ui/next-plugin/css-loader',
options: {
+ engine: 'native',
watch: false,
coordinatorPortFile: join('df', 'coordinator.port'),
sheetFile: join('df', 'sheet.json'),
@@ -737,6 +901,7 @@ describe('DevupUINextPlugin', () => {
{
loader: '@devup-ui/next-plugin/loader',
options: {
+ engine: 'native',
package: '@devup-ui/react',
cssDir: resolve('df', 'devup-ui'),
coordinatorPortFile: join('df', 'coordinator.port'),
diff --git a/packages/next-plugin/src/css-loader.ts b/packages/next-plugin/src/css-loader.ts
index b74038ca3..5a414c8cd 100644
--- a/packages/next-plugin/src/css-loader.ts
+++ b/packages/next-plugin/src/css-loader.ts
@@ -1,7 +1,11 @@
import { existsSync, readFileSync } from 'node:fs'
import { Agent, request } from 'node:http'
-import { getFileNumByFilename } from '@devup-ui/plugin-utils'
+import {
+ type DevupCompiler,
+ type DevupEngine,
+ getFileNumByFilename,
+} from '@devup-ui/plugin-utils'
import type { RawLoaderDefinitionFunction } from 'webpack'
import {
@@ -13,6 +17,7 @@ import {
import { loadWasm } from './wasm'
export interface DevupUICssLoaderOptions {
+ engine?: DevupEngine
// turbo
watch: boolean
coordinatorPortFile?: string
@@ -26,7 +31,7 @@ export interface DevupUICssLoaderOptions {
defaultFileMap: object
}
-let init = false
+let initializedCompilers = new WeakSet()
let cachedPort: number | null = null
const keepAliveAgent = new Agent({ keepAlive: true })
@@ -75,6 +80,7 @@ const devupUICssLoader: RawLoaderDefinitionFunction =
function (source, map, meta) {
const {
watch,
+ engine,
coordinatorPortFile,
sheetFile,
classMapFile,
@@ -135,48 +141,53 @@ const devupUICssLoader: RawLoaderDefinitionFunction =
return
}
- const {
- getCss,
- importClassMap,
- importFileMap,
- importSheet,
- registerTheme,
- } = loadWasm()
- if (!init) {
- init = true
- if (watch) {
- // restart loader issue
- // loader should read files when they exist in watch mode
- if (existsSync(sheetFile))
- importSheet(JSON.parse(readFileSync(sheetFile, 'utf-8')))
- if (existsSync(classMapFile))
- importClassMap(JSON.parse(readFileSync(classMapFile, 'utf-8')))
- if (existsSync(fileMapFile))
- importFileMap(JSON.parse(readFileSync(fileMapFile, 'utf-8')))
- if (existsSync(themeFile))
- registerTheme(
- JSON.parse(readFileSync(themeFile, 'utf-8'))?.['theme'] ?? {},
- )
- } else {
- importFileMap(defaultFileMap)
- importClassMap(defaultClassMap)
- importSheet(defaultSheet)
- registerTheme(theme)
+ let content: string | typeof source
+ try {
+ const compiler = loadWasm(engine)
+ const {
+ getCss,
+ importClassMap,
+ importFileMap,
+ importSheet,
+ registerTheme,
+ } = compiler
+ if (!initializedCompilers.has(compiler)) {
+ if (watch) {
+ // restart loader issue
+ // loader should read files when they exist in watch mode
+ if (existsSync(sheetFile))
+ importSheet(JSON.parse(readFileSync(sheetFile, 'utf-8')))
+ if (existsSync(classMapFile))
+ importClassMap(JSON.parse(readFileSync(classMapFile, 'utf-8')))
+ if (existsSync(fileMapFile))
+ importFileMap(JSON.parse(readFileSync(fileMapFile, 'utf-8')))
+ if (existsSync(themeFile))
+ registerTheme(
+ JSON.parse(readFileSync(themeFile, 'utf-8'))?.['theme'] ?? {},
+ )
+ } else {
+ importFileMap(defaultFileMap)
+ importClassMap(defaultClassMap)
+ importSheet(defaultSheet)
+ registerTheme(theme)
+ }
+ initializedCompilers.add(compiler)
}
- }
- this.callback(
- null,
- !watch ? source : getCss(getFileNumByFilename(this.resourcePath), true),
- map,
- meta,
- )
+ content = !watch
+ ? source
+ : getCss(getFileNumByFilename(this.resourcePath), true)
+ } catch (error) {
+ this.callback(error instanceof Error ? error : new Error(String(error)))
+ return
+ }
+ this.callback(null, content, map, meta)
}
export default devupUICssLoader
/** @internal Reset init state for testing purposes only */
export const resetInit = () => {
- init = false
+ initializedCompilers = new WeakSet()
cachedPort = null
}
diff --git a/packages/next-plugin/src/loader.ts b/packages/next-plugin/src/loader.ts
index 1843997fb..917b43c87 100644
--- a/packages/next-plugin/src/loader.ts
+++ b/packages/next-plugin/src/loader.ts
@@ -5,11 +5,14 @@ import { basename, dirname, join, relative, resolve } from 'node:path'
import {
createStateWriter,
+ type DevupCompiler,
+ type DevupEngine,
tailwindCssFiles,
withTailwindCss,
} from '@devup-ui/plugin-utils'
import type { RawLoaderDefinitionFunction } from 'webpack'
+import { takeExtractOutput } from './coordinator'
import {
isConnectionError,
missingPortFileError,
@@ -23,6 +26,7 @@ const stateWriter = createStateWriter((path, content, encoding) =>
)
export interface DevupUILoaderOptions {
+ engine?: DevupEngine
package: string
cssDir: string
sheetFile: string
@@ -39,7 +43,7 @@ export interface DevupUILoaderOptions {
defaultFileMap: object
importAliases?: Record
}
-let init = false
+let initializedCompilers = new WeakSet()
const cachedPorts = new Map()
const keepAliveAgent = new Agent({ keepAlive: true })
@@ -142,6 +146,7 @@ const devupUILoader: RawLoaderDefinitionFunction =
function (source) {
const {
watch,
+ engine,
package: libPackage,
cssDir,
sheetFile,
@@ -218,49 +223,50 @@ const devupUILoader: RawLoaderDefinitionFunction =
return
}
- // Non-coordinator mode: local WASM extraction
- const {
- codeExtract,
- exportClassMap,
- exportFileMap,
- exportSheet,
- getCss,
- importClassMap,
- importFileMap,
- importSheet,
- registerTheme,
- } = loadWasm()
- const promises: Promise[] = []
- if (!init) {
- init = true
- if (watch) {
- this.addDependency(sheetFile)
- this.addDependency(classMapFile)
- this.addDependency(fileMapFile)
- this.addDependency(themeFile)
- // restart loader issue
- // loader should read files when they exist in watch mode
- if (existsSync(sheetFile))
- importSheet(JSON.parse(readFileSync(sheetFile, 'utf-8')))
- if (existsSync(classMapFile))
- importClassMap(JSON.parse(readFileSync(classMapFile, 'utf-8')))
- if (existsSync(fileMapFile))
- importFileMap(JSON.parse(readFileSync(fileMapFile, 'utf-8')))
- if (existsSync(themeFile)) {
- const config = JSON.parse(readFileSync(themeFile, 'utf-8')) ?? {}
- registerTheme(withTailwindCss(config.theme ?? {}, config))
- for (const file of tailwindCssFiles(config)) this.addDependency(file)
- }
- } else {
- importFileMap(defaultFileMap)
- importClassMap(defaultClassMap)
- importSheet(defaultSheet)
- registerTheme(theme)
- }
- }
-
+ // Non-coordinator mode retains one selected compiler through callbacks.
const callback = this.async()
try {
+ const compiler = loadWasm(engine)
+ const {
+ codeExtract,
+ exportClassMap,
+ exportFileMap,
+ exportSheet,
+ getCss,
+ importClassMap,
+ importFileMap,
+ importSheet,
+ registerTheme,
+ } = compiler
+ const promises: Promise[] = []
+ if (!initializedCompilers.has(compiler)) {
+ if (watch) {
+ this.addDependency(sheetFile)
+ this.addDependency(classMapFile)
+ this.addDependency(fileMapFile)
+ this.addDependency(themeFile)
+ // restart loader issue
+ // loader should read files when they exist in watch mode
+ if (existsSync(sheetFile))
+ importSheet(JSON.parse(readFileSync(sheetFile, 'utf-8')))
+ if (existsSync(classMapFile))
+ importClassMap(JSON.parse(readFileSync(classMapFile, 'utf-8')))
+ if (existsSync(fileMapFile))
+ importFileMap(JSON.parse(readFileSync(fileMapFile, 'utf-8')))
+ if (existsSync(themeFile)) {
+ const config = JSON.parse(readFileSync(themeFile, 'utf-8')) ?? {}
+ registerTheme(withTailwindCss(config.theme ?? {}, config))
+ for (const file of tailwindCssFiles(config))
+ this.addDependency(file)
+ }
+ } else {
+ importFileMap(defaultFileMap)
+ importClassMap(defaultClassMap)
+ importSheet(defaultSheet)
+ registerTheme(theme)
+ }
+ initializedCompilers.add(compiler)
+ }
const id = this.resourcePath
let relCssDir = relative(dirname(id), cssDir).replaceAll('\\', '/')
@@ -269,22 +275,25 @@ const devupUILoader: RawLoaderDefinitionFunction =
const relativePath = relative(process.cwd(), id).replaceAll('\\', '/')
if (!relCssDir.startsWith('./')) relCssDir = `./${relCssDir}`
+ const snapshot = takeExtractOutput(
+ codeExtract(
+ relativePath,
+ source.toString(),
+ libPackage,
+ relCssDir,
+ singleCss,
+ false,
+ true,
+ importAliases,
+ ),
+ )
const {
code,
map,
cssFile,
updatedBaseStyle,
dependencies = [],
- } = codeExtract(
- relativePath,
- source.toString(),
- libPackage,
- relCssDir,
- singleCss,
- false,
- true,
- importAliases,
- )
+ } = snapshot
for (const dependency of dependencies) {
this.addDependency(resolve(dependency))
}
@@ -324,7 +333,7 @@ export default devupUILoader
/** @internal Reset init state for testing purposes only */
export const resetInit = () => {
- init = false
+ initializedCompilers = new WeakSet()
cachedPorts.clear()
}
diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts
index 18a83d497..4ce23d33e 100644
--- a/packages/next-plugin/src/plugin.ts
+++ b/packages/next-plugin/src/plugin.ts
@@ -136,6 +136,7 @@ export function DevupUI(
options: DevupUINextPluginOptions = {},
): NextConfig {
const normalizedShorthands = normalizeShorthands(options.shorthands ?? {})
+ const engine = options.engine === undefined ? 'native' : options.engine
const pluginStartedAt = profileStart()
const isTurbo =
process.env.TURBOPACK === '1' || process.env.TURBOPACK === 'auto'
@@ -185,6 +186,7 @@ export function DevupUI(
cssDir,
devupFile,
distDir,
+ engine,
hoistV: process.env.DEVUP_HOIST_V,
importAliases,
include,
@@ -231,7 +233,7 @@ export function DevupUI(
})
if (!existsSync(gitignoreFile)) writeFileSync(gitignoreFile, '*')
- const wasm = loadWasm()
+ const wasm = loadWasm(engine)
const {
codeExtract,
codeExtractWithoutSourceMap,
@@ -557,6 +559,7 @@ export function DevupUI(
loader: '@devup-ui/next-plugin/css-loader',
options: {
watch,
+ engine,
coordinatorPortFile,
sheetFile,
classMapFile,
@@ -580,6 +583,7 @@ export function DevupUI(
loader: '@devup-ui/next-plugin/loader',
options: {
package: libPackage,
+ engine,
cssDir,
coordinatorPortFile,
sheetFile,
@@ -653,6 +657,7 @@ export function DevupUI(
config.plugins.push(
new DevupUIWebpackPlugin({
...options,
+ engine,
watch: _options.dev,
productionPrewarmFiles: _options.dev
? undefined
diff --git a/packages/next-plugin/src/wasm.ts b/packages/next-plugin/src/wasm.ts
index 634340c2a..16ca38748 100644
--- a/packages/next-plugin/src/wasm.ts
+++ b/packages/next-plugin/src/wasm.ts
@@ -2,14 +2,19 @@ import { existsSync, realpathSync } from 'node:fs'
import { createRequire } from 'node:module'
import { join, relative } from 'node:path'
-import { createModuleResolver } from '@devup-ui/plugin-utils'
+import {
+ createModuleResolver,
+ type DevupCompiler,
+ type DevupEngine,
+ loadCompiler,
+} from '@devup-ui/plugin-utils'
-export type DevupWasm = typeof import('@devup-ui/wasm')
+export type DevupWasm = DevupCompiler
export type DevupWebpackPlugin = typeof import('@devup-ui/webpack-plugin')
let wasmForTesting: DevupWasm | undefined
let webpackPluginForTesting: DevupWebpackPlugin | undefined
-let loadedWasm: DevupWasm | undefined
+const loadedCompilers = new Map()
let webpackPlugin: DevupWebpackPlugin | undefined
/** @internal Resolve dependencies from the plugin's physical install location. */
@@ -46,11 +51,13 @@ export function withModuleResolver(wasm: DevupWasm): DevupWasm {
}
/** Load the extraction engine once for the lifetime of a Next config. */
-export function loadWasm(): DevupWasm {
+export function loadWasm(engine: DevupEngine = 'native'): DevupWasm {
if (wasmForTesting) return wasmForTesting
- return (loadedWasm ??= withModuleResolver(
- requireFromPlugin('@devup-ui/wasm'),
- ))
+ const cached = loadedCompilers.get(engine)
+ if (cached) return cached
+ const compiler = withModuleResolver(loadCompiler(engine))
+ loadedCompilers.set(engine, compiler)
+ return compiler
}
/** Keep the Webpack adapter out of Turbopack startup. */
@@ -77,6 +84,6 @@ export function setWebpackPluginForTesting(
export function resetWasmForTesting(): void {
wasmForTesting = undefined
webpackPluginForTesting = undefined
- loadedWasm = undefined
+ loadedCompilers.clear()
webpackPlugin = undefined
}
diff --git a/packages/plugin-utils/package.json b/packages/plugin-utils/package.json
index 1ef59ee54..7859b2745 100644
--- a/packages/plugin-utils/package.json
+++ b/packages/plugin-utils/package.json
@@ -18,8 +18,8 @@
"version": "1.0.17",
"scripts": {
"lint": "eslint",
- "test:types": "bun ../../node_modules/@typescript/native/bin/tsc --ignoreConfig --noEmit --strict --skipLibCheck --moduleResolution bundler --module esnext --target esnext --types bun src/__tests__/shorthands.test.ts",
- "build": "bun run test:types && bun ../../node_modules/@typescript/native/bin/tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external"
+ "test:types": "bun ../../node_modules/@typescript/native/bin/tsc --ignoreConfig --noEmit --strict --skipLibCheck --moduleResolution bundler --module esnext --target esnext --types bun src/__tests__/shorthands.test.ts src/__tests__/compiler-types.test.ts src/__tests__/compiler.test.ts src/__tests__/native-platform.test.ts",
+ "build": "bun run test:types && bun ../../node_modules/@typescript/native/bin/tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external --define import.meta.url=module.filename && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external"
},
"publishConfig": {
"access": "public"
@@ -37,6 +37,10 @@
"files": [
"dist"
],
+ "dependencies": {
+ "@devup-ui/binding": "workspace:^",
+ "@devup-ui/wasm": "workspace:^"
+ },
"devDependencies": {
"typescript": "^7.0.2"
}
diff --git a/packages/plugin-utils/src/__tests__/compiler-types.test.ts b/packages/plugin-utils/src/__tests__/compiler-types.test.ts
new file mode 100644
index 000000000..10e4921eb
--- /dev/null
+++ b/packages/plugin-utils/src/__tests__/compiler-types.test.ts
@@ -0,0 +1,295 @@
+import * as native from '@devup-ui/binding'
+import * as wasm from '@devup-ui/wasm'
+import { describe, expect, it } from 'bun:test'
+
+import {
+ compilerRealpath,
+ createCompilerSelector,
+ InvalidEngineError,
+ loadCompiler,
+} from '../compiler'
+import type {
+ DevupCompiler,
+ DevupCompilerOutput,
+ DevupEngine,
+} from '../compiler-types'
+import type { DevupUIBasePluginOptions } from '../shared'
+
+const nativeCompiler: DevupCompiler = native
+const wasmCompiler: DevupCompiler = wasm
+type Accepted = T extends DevupEngine ? true : false
+type Resolver = NonNullable[0]>
+const optionProof: Pick = {}
+const engineProof: [Accepted<'native'>, Accepted<'wasm'>, Accepted<'other'>] = [
+ true,
+ true,
+ false,
+]
+const constructorProof: 'Output' extends keyof DevupCompiler ? true : false =
+ false
+const resolverProof: ((
+ specifier: string,
+ importer: string,
+) => number) extends Resolver
+ ? true
+ : false = false
+
+describe('compiler structural types', () => {
+ it('keeps literal options optional and resolver results precise', () => {
+ // Given / When: these assignments are also checked by test:types.
+ const configured: Pick = {
+ engine: 'native',
+ }
+ // Then
+ expect([
+ optionProof,
+ configured,
+ engineProof,
+ constructorProof,
+ resolverProof,
+ ]).toEqual([{}, { engine: 'native' }, [true, true, false], false, false])
+ })
+
+ for (const [name, compiler] of [
+ ['native', nativeCompiler],
+ ['wasm', wasmCompiler],
+ ] as const) {
+ it(`accepts the actual ${name} namespace and releases its structural output`, () => {
+ // Given
+ compiler.resetStateForTesting()
+ try {
+ // When: both concrete return types must assign without a constructor cast.
+ using output: DevupCompilerOutput = compiler.codeExtract(
+ 'compiler-type.tsx',
+ "import {Box} from '@devup-ui/react'; export const view=;",
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ )
+ // Then
+ expect(output.code).toContain('className=')
+ expect(output.css).toContain('padding:12px')
+ expect(output.dependencies).toEqual([])
+ expect(output.map).toBeDefined()
+ expect(
+ Object.keys(compiler).filter(
+ (key) => key !== 'Output' && key !== 'default',
+ ),
+ ).toHaveLength(28)
+ } finally {
+ compiler.resetStateForTesting()
+ }
+ })
+ }
+})
+
+// These assignments check the concrete extraction types, independently of the facade.
+const nativeOutput: (
+ output: ReturnType,
+) => DevupCompilerOutput = (output) => output
+const wasmOutput: (
+ output: ReturnType,
+) => DevupCompilerOutput = (output) => output
+
+for (const [name, compiler, accept] of [
+ ['native', native, nativeOutput],
+ ['wasm', wasm, wasmOutput],
+] as const) {
+ it(`accepts the concrete ${name} no-map output without retaining it`, () => {
+ // Given
+ compiler.resetStateForTesting()
+ try {
+ // When
+ using output = compiler.codeExtractWithoutSourceMap(
+ 'plain.ts',
+ 'export const n=1',
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ )
+ // Then
+ expect(accept(output).map).toBeUndefined()
+ } finally {
+ compiler.resetStateForTesting()
+ }
+ })
+}
+
+describe('compiler physical path', () => {
+ it('falls back once on the same path when native realpath throws EISDIR', () => {
+ // Given
+ const filename = 'compiler.js'
+ const nativePaths: string[] = []
+ const regularPaths: string[] = []
+ // When
+ const resolved = compilerRealpath(filename, {
+ native(path) {
+ nativePaths.push(path)
+ throw Object.assign(new Error('RAM disk'), { code: 'EISDIR' })
+ },
+ regular(path) {
+ regularPaths.push(path)
+ return 'physical/compiler.js'
+ },
+ })
+ // Then
+ expect(resolved).toBe('physical/compiler.js')
+ expect(nativePaths).toEqual([filename])
+ expect(regularPaths).toEqual([filename])
+ })
+
+ it.each([
+ Object.assign(new Error('missing path'), { code: 'ENOENT' }),
+ new Error('unknown failure'),
+ { thrown: 'value' },
+ null,
+ undefined,
+ 'failure',
+ ])('rethrows %s unchanged without regular realpath', (failure) => {
+ // Given
+ expect.assertions(2)
+ let fallback = 0
+ // When
+ try {
+ compilerRealpath('compiler.js', {
+ native() {
+ throw failure
+ },
+ regular() {
+ fallback++
+ return 'unexpected'
+ },
+ })
+ } catch (error) {
+ // Then
+ expect(error).toBe(failure)
+ }
+ expect(fallback).toBe(0)
+ })
+})
+
+describe('compiler selection contract', () => {
+ it.each([undefined, 'native', 'wasm'] as const)(
+ 'selects the actual namespace for %s',
+ (engine) => {
+ // Given / When
+ const compiler = loadCompiler(engine)
+ // Then
+ expect(compiler.codeExtract).toBe(
+ engine === 'wasm' ? wasm.codeExtract : native.codeExtract,
+ )
+ },
+ )
+
+ it.each([null, '', false, 0, 'other', {}, Symbol('invalid')])(
+ 'rejects %s before any host or backend access',
+ (engine) => {
+ // Given
+ let accesses = 0
+ const fail = (): never => {
+ accesses++
+ throw new Error('unexpected access')
+ }
+ const select = createCompilerSelector({
+ native: fail,
+ wasm: fail,
+ platform: fail,
+ })
+ // When / Then
+ expect(() => select(engine)).toThrow(InvalidEngineError)
+ expect(accesses).toBe(0)
+ },
+ )
+
+ it('retains the earlier namespace when the other engine is selected', () => {
+ // Given
+ const select = createCompilerSelector({
+ native: () => native,
+ wasm: () => wasm,
+ platform: () => '@devup-ui/binding-test',
+ })
+ const previous = select('wasm')
+ // When
+ const next = select()
+ // Then
+ expect(previous).toBe(wasm)
+ expect(next).toBe(native)
+ expect(select('wasm')).toBe(previous)
+ })
+
+ for (const cause of [
+ new Error('corrupt image'),
+ Object.assign(new Error('transitive library'), {
+ code: 'MODULE_NOT_FOUND',
+ }),
+ { thrown: 'value' },
+ ]) {
+ it('retains the exact native failure and escape without fallback', () => {
+ // Given
+ let fallback = 0
+ const platformPackage = '@devup-ui/binding-linux-x64-gnu'
+ const select = createCompilerSelector({
+ native() {
+ throw cause
+ },
+ wasm() {
+ fallback++
+ return wasm
+ },
+ platform: () => platformPackage,
+ })
+ // When / Then
+ try {
+ select('native')
+ throw new Error('native failure was swallowed')
+ } catch (error) {
+ if (!(error instanceof Error)) throw error
+ expect(error.cause).toBe(cause)
+ expect(error.message).toContain(platformPackage)
+ expect(error.message).toContain("engine: 'wasm'")
+ }
+ expect(fallback).toBe(0)
+ })
+ }
+
+ it('propagates host failure before native loading', () => {
+ // Given
+ const failure = new Error('unknown libc')
+ let loads = 0
+ const select = createCompilerSelector({
+ native() {
+ loads++
+ return native
+ },
+ wasm: () => wasm,
+ platform() {
+ throw failure
+ },
+ })
+ // When / Then
+ expect(() => select()).toThrow(failure)
+ expect(loads).toBe(0)
+ })
+
+ it('preserves a WASM load failure without probing native context', () => {
+ // Given
+ const failure = new Error('wasm unavailable')
+ const select = createCompilerSelector({
+ native: () => native,
+ wasm() {
+ throw failure
+ },
+ platform() {
+ throw new Error('unexpected host probe')
+ },
+ })
+ // When / Then
+ expect(() => select('wasm')).toThrow(failure)
+ })
+})
diff --git a/packages/plugin-utils/src/__tests__/compiler.test.ts b/packages/plugin-utils/src/__tests__/compiler.test.ts
new file mode 100644
index 000000000..5daa76778
--- /dev/null
+++ b/packages/plugin-utils/src/__tests__/compiler.test.ts
@@ -0,0 +1,215 @@
+import {
+ cpSync,
+ mkdirSync,
+ mkdtempSync,
+ rmSync,
+ symlinkSync,
+ writeFileSync,
+} from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+import { expect, it } from 'bun:test'
+
+import { nativePlatformPackage } from '../native-platform'
+
+const root = fileURLToPath(new URL('../../../../', import.meta.url))
+const platformPackage = nativePlatformPackage({
+ platform: process.platform,
+ arch: process.arch,
+ report: process.report.getReport,
+})
+
+type Layout = 'both' | 'wasm' | 'native' | 'missing' | 'corrupt' | 'transitive'
+
+function fixture(layout: Layout) {
+ const directory = mkdtempSync(join(tmpdir(), 'devup-compiler-'))
+ try {
+ const physical = join(directory, 'physical/node_modules/@devup-ui')
+ const caller = join(directory, 'caller')
+ mkdirSync(physical, { recursive: true })
+ mkdirSync(join(caller, 'node_modules/@devup-ui'), { recursive: true })
+ const utils = join(physical, 'plugin-utils')
+ cpSync(join(root, 'packages/plugin-utils/dist'), join(utils, 'dist'), {
+ recursive: true,
+ })
+ cpSync(
+ join(root, 'packages/plugin-utils/package.json'),
+ join(utils, 'package.json'),
+ )
+ symlinkSync(
+ utils,
+ join(caller, 'node_modules/@devup-ui/plugin-utils'),
+ process.platform === 'win32' ? 'junction' : 'dir',
+ )
+ if (layout === 'both' || layout === 'wasm') {
+ cpSync(
+ join(root, 'bindings/devup-ui-wasm/pkg'),
+ join(physical, 'wasm/pkg'),
+ {
+ recursive: true,
+ },
+ )
+ cpSync(
+ join(root, 'bindings/devup-ui-wasm/package.json'),
+ join(physical, 'wasm/package.json'),
+ )
+ }
+ if (layout !== 'wasm') {
+ const binding = join(physical, 'binding')
+ mkdirSync(join(binding, 'pkg'), { recursive: true })
+ for (const file of [
+ 'package.json',
+ 'index.mjs',
+ 'loader.cjs',
+ 'pkg/index.cjs',
+ ]) {
+ cpSync(
+ join(root, 'bindings/devup-ui-binding', file),
+ join(binding, file),
+ )
+ }
+ const sibling = join(physical, platformPackage.slice('@devup-ui/'.length))
+ if (layout === 'both' || layout === 'native') {
+ cpSync(
+ join(
+ root,
+ 'bindings',
+ `devup-ui-${platformPackage.slice('@devup-ui/'.length)}`,
+ ),
+ sibling,
+ {
+ recursive: true,
+ },
+ )
+ }
+ if (layout === 'corrupt' || layout === 'transitive') {
+ mkdirSync(sibling)
+ const entry = layout === 'corrupt' ? 'broken.node' : 'index.cjs'
+ writeFileSync(
+ join(sibling, 'package.json'),
+ JSON.stringify({
+ name: platformPackage,
+ main: entry,
+ }),
+ )
+ writeFileSync(
+ join(sibling, entry),
+ layout === 'corrupt'
+ ? 'deliberately invalid image'
+ : "require('devup-missing-transitive-library')",
+ )
+ }
+ }
+ for (const name of ['binding', 'wasm']) {
+ const decoy = join(caller, 'node_modules/@devup-ui', name)
+ mkdirSync(decoy)
+ writeFileSync(
+ join(decoy, 'package.json'),
+ JSON.stringify({
+ name: `@devup-ui/${name}`,
+ main: 'index.cjs',
+ }),
+ )
+ writeFileSync(
+ join(decoy, 'index.cjs'),
+ "throw new Error('caller decoy must never load')",
+ )
+ }
+ return {
+ caller,
+ [Symbol.dispose]() {
+ rmSync(directory, { recursive: true, force: true })
+ },
+ }
+ } catch (error) {
+ rmSync(directory, { recursive: true, force: true })
+ throw error
+ }
+}
+
+const child = String.raw`
+const assert = require('node:assert/strict');
+const {createRequire} = require('node:module');
+const {pathToFileURL} = require('node:url');
+const {realpathSync} = require('node:fs');
+const {dirname, join} = require('node:path');
+(async () => {
+ const [layout, engine, sibling] = process.argv.slice(1);
+ const facade = join(process.cwd(), 'node_modules/@devup-ui/plugin-utils/dist/index.cjs');
+ const physical = realpathSync.native(facade);
+ const own = createRequire(physical);
+ const originalReport = process.report.getReport;
+ if (engine === 'wasm') process.report.getReport = () => { throw new Error('unselected Linux probe'); };
+ const cjs = require(facade);
+ const esm = await import(pathToFileURL(join(dirname(physical), 'index.mjs')).href);
+ assert.throws(() => cjs.loadCompiler('invalid'), TypeError);
+ if (['missing', 'corrupt', 'transitive'].includes(layout)) {
+ let raw;
+ try { own('@devup-ui/binding'); } catch (error) { raw = error; }
+ assert.ok(raw);
+ assert.throws(() => cjs.loadCompiler('native'), error => {
+ assert.ok(error.message.includes(sibling));
+ assert.ok(error.message.includes("engine: 'wasm'"));
+ assert.ok(error.cause);
+ assert.equal(error.cause.message, raw.message);
+ if (layout === 'missing') assert.equal(error.cause.cause.code, 'MODULE_NOT_FOUND');
+ if (layout === 'transitive') assert.equal(error.cause.code, 'MODULE_NOT_FOUND');
+ return true;
+ });
+ } else {
+ const selected = engine === 'default' ? undefined : engine;
+ const compiler = cjs.loadCompiler(selected);
+ assert.equal(compiler, esm.loadCompiler(selected));
+ assert.equal(compiler, own(engine === 'wasm' ? '@devup-ui/wasm' : '@devup-ui/binding'));
+ if (engine !== 'wasm') {
+ const named = await import(pathToFileURL(join(dirname(dirname(own.resolve('@devup-ui/binding'))), 'index.mjs')).href);
+ for (const [name, value] of Object.entries(named)) assert.equal(value, compiler[name]);
+ }
+ compiler.resetStateForTesting();
+ const output = compiler.codeExtract('child.tsx', "import {Box} from '@devup-ui/react';export const v=;", '@devup-ui/react', 'df', true, false, false, {});
+ try {
+ assert.ok(output.code.includes('className=')); assert.ok(output.css.includes('padding:8px'));
+ assert.deepEqual(output.dependencies, []); assert.ok(output.map);
+ } finally { output.free(); compiler.resetStateForTesting(); }
+ }
+ process.report.getReport = originalReport;
+})().catch(error => { console.error(error); process.exitCode = 1; });
+`
+
+for (const runtime of ['node', process.execPath]) {
+ for (const [layout, engine] of [
+ ['both', 'default'],
+ ['both', 'native'],
+ ['both', 'wasm'],
+ ['wasm', 'wasm'],
+ ['native', 'native'],
+ ['missing', 'native'],
+ ['corrupt', 'native'],
+ ['transitive', 'native'],
+ ] as const) {
+ it(`loads physical built CJS/ESM with ${runtime}: ${layout}/${engine}`, async () => {
+ // Given: each child has its own relocated tree and no repository ancestor.
+ using fs = fixture(layout)
+ // When
+ const childProcess = Bun.spawn(
+ [runtime, '--eval', child, layout, engine, platformPackage],
+ {
+ cwd: fs.caller,
+ env: { ...Bun.env, NODE_PATH: '', NODE_OPTIONS: '', BUN_OPTIONS: '' },
+ timeout: 20000,
+ stdout: 'pipe',
+ stderr: 'pipe',
+ },
+ )
+ const [status, stderr] = await Promise.all([
+ childProcess.exited,
+ new Response(childProcess.stderr).text(),
+ new Response(childProcess.stdout).text(),
+ ])
+ // Then
+ expect({ status, stderr }).toEqual({ status: 0, stderr: '' })
+ }, 30000)
+ }
+}
diff --git a/packages/plugin-utils/src/__tests__/native-platform.test.ts b/packages/plugin-utils/src/__tests__/native-platform.test.ts
new file mode 100644
index 000000000..e8aef009b
--- /dev/null
+++ b/packages/plugin-utils/src/__tests__/native-platform.test.ts
@@ -0,0 +1,428 @@
+import { randomUUID } from 'node:crypto'
+import { readFileSync } from 'node:fs'
+import Module, { createRequire } from 'node:module'
+import { fileURLToPath } from 'node:url'
+
+import { describe, expect, it } from 'bun:test'
+
+import { compilerRealpath, loadCompiler } from '../compiler'
+import { NativePlatformError, nativePlatformPackage } from '../native-platform'
+
+const matrixUrl = new URL(
+ '../../../../e2e/binding/package/matrix.mjs',
+ import.meta.url,
+)
+const matrixProcess = Bun.spawn(
+ [
+ 'node',
+ '--input-type=module',
+ '--eval',
+ `import { targets } from ${JSON.stringify(matrixUrl.href)}; console.log(JSON.stringify(targets))`,
+ ],
+ { stdout: 'pipe', stderr: 'pipe', timeout: 10000 },
+)
+const [matrixStatus, matrixText, matrixError] = await Promise.all([
+ matrixProcess.exited,
+ new Response(matrixProcess.stdout).text(),
+ new Response(matrixProcess.stderr).text(),
+])
+if (matrixStatus !== 0) throw new Error(matrixError)
+const parsed: unknown = JSON.parse(matrixText)
+if (!Array.isArray(parsed))
+ throw new TypeError('Canonical target table must be an array')
+const rows: readonly unknown[] = parsed
+const targets = rows.map((row) => {
+ if (
+ typeof row !== 'object' ||
+ row === null ||
+ !('suffix' in row) ||
+ typeof row.suffix !== 'string' ||
+ !('os' in row) ||
+ typeof row.os !== 'string' ||
+ !('cpu' in row) ||
+ typeof row.cpu !== 'string'
+ )
+ throw new TypeError('Invalid canonical target row')
+ const libc = 'libc' in row ? row.libc : undefined
+ if (libc !== undefined && libc !== 'glibc' && libc !== 'musl') {
+ throw new TypeError('Invalid canonical target libc')
+ }
+ return { suffix: row.suffix, os: row.os, cpu: row.cpu, libc }
+})
+
+const ownRequire = createRequire(
+ compilerRealpath(fileURLToPath(import.meta.url)),
+)
+const authoredLoader = compilerRealpath(
+ fileURLToPath(
+ new URL(
+ '../../../../bindings/devup-ui-binding/loader.cjs',
+ import.meta.url,
+ ),
+ ),
+)
+const hostPackage = nativePlatformPackage({
+ platform: process.platform,
+ arch: process.arch,
+ report: process.report.getReport,
+})
+const hostAddon: unknown = ownRequire(authoredLoader)
+const hostCompiler = loadCompiler('native')
+const hostFunctions = Object.entries(hostCompiler)
+type LoaderTarget = Pick<(typeof targets)[number], 'os' | 'cpu' | 'libc'>
+type ResolverControl = 'ordinary' | 'missing' | 'other'
+
+function targetReport(row: LoaderTarget) {
+ return { header: row.libc === 'glibc' ? { glibcVersionRuntime: '2.39' } : {} }
+}
+
+// Keep the complete override transaction synchronous: only the authored loader is
+// evicted, never its public wrapper, platform sibling or native backend.
+function requireAuthoredLoader(
+ row: LoaderTarget,
+ control: ResolverControl = 'ordinary',
+) {
+ const descriptors = (['platform', 'arch', 'report'] as const).map((key) => ({
+ key,
+ descriptor: Object.getOwnPropertyDescriptor(process, key),
+ }))
+ const cacheDescriptor = Object.getOwnPropertyDescriptor(
+ ownRequire.cache,
+ authoredLoader,
+ )
+ const previous = ownRequire.cache[authoredLoader]
+ const original: unknown = Reflect.get(Module, '_resolveFilename')
+ if (typeof original !== 'function')
+ throw new TypeError('Expected Node filename resolver')
+ const originalResolver = original
+ const manifest =
+ control === 'ordinary' ? undefined : `${hostPackage}/package.json`
+ let reports = 0
+ let resolverCalls = 0
+ let result: unknown
+ let failure: Error | undefined
+ let resolverFailure: Error | undefined
+
+ if (control === 'other') {
+ try {
+ readFileSync(compilerRealpath(import.meta.dirname))
+ } catch (error) {
+ if (!(error instanceof Error)) throw error
+ resolverFailure = error
+ }
+ expect(resolverFailure).toBeInstanceOf(Error)
+ expect(
+ resolverFailure && 'code' in resolverFailure
+ ? resolverFailure.code
+ : undefined,
+ ).toBe('EISDIR')
+ }
+
+ // This is Node's concrete private resolver signature, not a namespace mock.
+ // Its four arguments must pass through unchanged on every non-target request.
+ function resolveFilename(
+ this: unknown,
+ ...args: [
+ request: string,
+ parent: NodeJS.Module | null | undefined,
+ isMain?: boolean,
+ options?: { paths?: string[] },
+ ]
+ ): unknown {
+ const [request, parent] = args
+ if (
+ request === manifest &&
+ typeof parent?.filename === 'string' &&
+ compilerRealpath(parent.filename) === authoredLoader
+ ) {
+ resolverCalls++
+ switch (control) {
+ case 'missing':
+ try {
+ return Reflect.apply(originalResolver, this, [
+ `${hostPackage}-w74t-${randomUUID()}`,
+ ...args.slice(1),
+ ])
+ } catch (error) {
+ if (!(error instanceof Error)) throw error
+ resolverFailure = error
+ throw error
+ }
+ case 'other':
+ throw resolverFailure
+ case 'ordinary':
+ break
+ default: {
+ const unreachable: never = control
+ return unreachable
+ }
+ }
+ }
+ const resolved: unknown = Reflect.apply(originalResolver, this, args)
+ return resolved
+ }
+
+ try {
+ Object.defineProperty(process, 'platform', {
+ configurable: true,
+ value: row.os,
+ })
+ Object.defineProperty(process, 'arch', {
+ configurable: true,
+ value: row.cpu,
+ })
+ Object.defineProperty(process, 'report', {
+ configurable: true,
+ value: {
+ getReport() {
+ reports++
+ return targetReport(row)
+ },
+ },
+ })
+ delete ownRequire.cache[authoredLoader]
+ if (control !== 'ordinary')
+ Reflect.set(Module, '_resolveFilename', resolveFilename)
+ try {
+ result = ownRequire(authoredLoader)
+ } catch (error) {
+ if (!(error instanceof Error)) throw error
+ failure = error
+ }
+ } finally {
+ if (control !== 'ordinary')
+ Reflect.set(Module, '_resolveFilename', original)
+ delete ownRequire.cache[authoredLoader]
+ if (previous) ownRequire.cache[authoredLoader] = previous
+ for (const { key, descriptor } of descriptors) {
+ if (descriptor) Object.defineProperty(process, key, descriptor)
+ else Reflect.deleteProperty(process, key)
+ }
+ expect(Reflect.get(Module, '_resolveFilename')).toBe(original)
+ expect(ownRequire.cache[authoredLoader]).toBe(previous)
+ expect(
+ Object.getOwnPropertyDescriptor(ownRequire.cache, authoredLoader),
+ ).toEqual(cacheDescriptor)
+ for (const { key, descriptor } of descriptors) {
+ expect(Object.getOwnPropertyDescriptor(process, key)).toEqual(descriptor)
+ }
+ const restoredCompiler = loadCompiler('native')
+ expect(restoredCompiler).toBe(hostCompiler)
+ for (const [name, value] of hostFunctions) {
+ expect(Reflect.get(restoredCompiler, name)).toBe(value)
+ }
+ }
+ expect(reports).toBe(row.os === 'linux' ? 1 : 0)
+ return { result, failure, resolverFailure, resolverCalls }
+}
+
+describe('authored native loader drift controls', () => {
+ for (const row of targets) {
+ it(`actually selects the helper package for ${row.suffix}`, () => {
+ // Given
+ const selected = nativePlatformPackage({
+ platform: row.os,
+ arch: row.cpu,
+ report: () => targetReport(row),
+ })
+ // When
+ const { result, failure } = requireAuthoredLoader(row)
+ // Then: linked foreign manifests reach final require, not the optional catch.
+ expect(selected).toBe(`@devup-ui/binding-${row.suffix}`)
+ if (selected === hostPackage) {
+ expect(failure).toBeUndefined()
+ expect(result).toBe(hostAddon)
+ } else {
+ expect(failure).toBeInstanceOf(Error)
+ expect(failure?.message).toContain(selected)
+ expect(failure?.message).not.toContain('Missing optional dependency')
+ }
+ })
+ }
+
+ it('actually rejects unsupported freebsd/x64 before reporting', () => {
+ // Given
+ const row = { os: 'freebsd', cpu: 'x64', libc: undefined }
+ // When
+ const { failure } = requireAuthoredLoader(row)
+ // Then
+ expect(failure?.message).toBe(
+ 'Unsupported @devup-ui/binding target: freebsd/x64/none',
+ )
+ })
+
+ for (const control of ['missing', 'other'] as const) {
+ it(`preserves the genuine ${control} resolver error for the host manifest`, () => {
+ // Given
+ const row = targets.find(
+ (target) => `@devup-ui/binding-${target.suffix}` === hostPackage,
+ )
+ if (!row)
+ throw new TypeError(
+ 'Host package must belong to the canonical target table',
+ )
+ // When
+ const { failure, resolverFailure, resolverCalls } = requireAuthoredLoader(
+ row,
+ control,
+ )
+ // Then
+ expect(resolverCalls).toBe(1)
+ expect(resolverFailure).toBeInstanceOf(Error)
+ switch (control) {
+ case 'missing':
+ expect(
+ resolverFailure && 'code' in resolverFailure
+ ? resolverFailure.code
+ : undefined,
+ ).toBe('MODULE_NOT_FOUND')
+ expect(failure?.message).toBe(
+ `Missing optional dependency ${hostPackage}. Reinstall @devup-ui/binding with optional dependencies enabled (do not use --omit=optional).`,
+ )
+ expect(failure?.cause).toBe(resolverFailure)
+ break
+ case 'other':
+ expect(failure).toBe(resolverFailure)
+ break
+ default: {
+ const unreachable: never = control
+ throw new TypeError(`Unexpected resolver control: ${unreachable}`)
+ }
+ }
+ })
+ }
+})
+
+describe('native diagnostic platform selection', () => {
+ it('supports exactly the packages in the canonical table', () => {
+ // Given
+ const supported = ['win32', 'darwin', 'linux'].flatMap((platform) =>
+ ['x64', 'arm64'].flatMap((arch) =>
+ (platform === 'linux' ? [false, true] : [false]).map((gnu) =>
+ nativePlatformPackage({
+ platform,
+ arch,
+ report: () => ({
+ header: gnu ? { glibcVersionRuntime: '2.39' } : {},
+ }),
+ }),
+ ),
+ ),
+ )
+ // When / Then
+ expect(new Set(supported)).toEqual(
+ new Set(targets.map((row) => `@devup-ui/binding-${row.suffix}`)),
+ )
+ })
+
+ for (const row of targets) {
+ it(`matches the canonical package for ${row.suffix}`, () => {
+ // Given
+ let reports = 0
+ const host = {
+ platform: row.os,
+ arch: row.cpu,
+ report() {
+ reports++
+ return {
+ header: row.libc === 'glibc' ? { glibcVersionRuntime: '2.39' } : {},
+ }
+ },
+ }
+ // When
+ const name = nativePlatformPackage(host)
+ // Then
+ expect(name).toBe(`@devup-ui/binding-${row.suffix}`)
+ expect(reports).toBe(row.os === 'linux' ? 1 : 0)
+ })
+ }
+
+ it.each([
+ ['freebsd', 'x64'],
+ ['win32', 'ia32'],
+ ['linux', 'riscv64'],
+ ])(
+ 'fails explicitly before reporting for unsupported %s/%s',
+ (platform, arch) => {
+ // Given
+ let reports = 0
+ const host = {
+ platform,
+ arch,
+ report() {
+ reports++
+ return { header: {} }
+ },
+ }
+ // When / Then
+ expect(() => nativePlatformPackage(host)).toThrow(NativePlatformError)
+ expect(reports).toBe(0)
+ },
+ )
+
+ it('retains a report failure instead of guessing musl', () => {
+ // Given
+ const failure = new Error('report unavailable')
+ const host = {
+ platform: 'linux',
+ arch: 'x64',
+ report() {
+ throw failure
+ },
+ }
+ // When / Then
+ expect(() => nativePlatformPackage(host)).toThrow(
+ expect.objectContaining({
+ name: 'NativePlatformError',
+ cause: failure,
+ }),
+ )
+ })
+
+ it('retains an inaccessible report header as the platform failure cause', () => {
+ // Given
+ const failure = new Error('header unavailable')
+ const host = {
+ platform: 'linux',
+ arch: 'x64',
+ report: () => ({
+ get header() {
+ throw failure
+ },
+ }),
+ }
+ // When / Then
+ expect(() => nativePlatformPackage(host)).toThrow(
+ expect.objectContaining({
+ name: 'NativePlatformError',
+ cause: failure,
+ }),
+ )
+ })
+
+ it.each([null, 'report text', {}, { header: null }, { header: 'bad' }])(
+ 'rejects malformed report %j without guessing a package',
+ (report) => {
+ // Given
+ const host = { platform: 'linux', arch: 'arm64', report: () => report }
+ // When / Then
+ expect(() => nativePlatformPackage(host)).toThrow(NativePlatformError)
+ },
+ )
+
+ it.each([undefined, '', false, 0])(
+ 'matches the loader truthiness rule for %j',
+ (glibcVersionRuntime) => {
+ // Given
+ const host = {
+ platform: 'linux',
+ arch: 'x64',
+ report: () => ({ header: { glibcVersionRuntime } }),
+ }
+ // When
+ const name = nativePlatformPackage(host)
+ // Then
+ expect(name).toBe('@devup-ui/binding-linux-x64-musl')
+ },
+ )
+})
diff --git a/packages/plugin-utils/src/compiler-types.ts b/packages/plugin-utils/src/compiler-types.ts
new file mode 100644
index 000000000..4db11c43f
--- /dev/null
+++ b/packages/plugin-utils/src/compiler-types.ts
@@ -0,0 +1,22 @@
+export type DevupEngine = 'wasm' | 'native'
+
+type NativeCompiler = typeof import('@devup-ui/binding')
+
+/** The public functions, not the backend-specific Output constructor. */
+export type DevupCompiler = Pick<
+ NativeCompiler,
+ Exclude
+>
+
+/** Output ownership stays with the caller; release it after reading its getters. */
+export type DevupCompilerOutput = Pick<
+ import('@devup-ui/binding').Output,
+ | 'code'
+ | 'css'
+ | 'cssFile'
+ | 'map'
+ | 'dependencies'
+ | 'updatedBaseStyle'
+ | 'free'
+ | typeof Symbol.dispose
+>
diff --git a/packages/plugin-utils/src/compiler.ts b/packages/plugin-utils/src/compiler.ts
new file mode 100644
index 000000000..3d242e053
--- /dev/null
+++ b/packages/plugin-utils/src/compiler.ts
@@ -0,0 +1,116 @@
+import { realpathSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { isAbsolute } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+import type { DevupCompiler, DevupEngine } from './compiler-types'
+import { nativePlatformPackage } from './native-platform'
+
+interface CompilerLoaders {
+ readonly native: () => typeof import('@devup-ui/binding')
+ readonly wasm: () => typeof import('@devup-ui/wasm')
+ readonly platform: () => string
+}
+
+export class InvalidEngineError extends TypeError {
+ readonly name = 'InvalidEngineError'
+
+ constructor(readonly engine: unknown) {
+ super("devup-ui engine must be 'native' or 'wasm'")
+ }
+}
+
+export class NativeCompilerLoadError extends Error {
+ readonly name = 'NativeCompilerLoadError'
+
+ constructor(
+ readonly platformPackage: string,
+ cause: unknown,
+ ) {
+ super(
+ `Cannot load ${platformPackage}. Install its optional dependency and native libraries, or use engine: 'wasm'.`,
+ { cause },
+ )
+ }
+}
+
+/** @internal Concrete load boundary for source tests; not exported by the barrel. */
+export function createCompilerSelector(loaders: CompilerLoaders) {
+ return (engine: unknown = undefined): DevupCompiler => {
+ switch (engine) {
+ case 'wasm':
+ return loaders.wasm()
+ case undefined:
+ case 'native': {
+ const platformPackage = loaders.platform()
+ try {
+ return loaders.native()
+ } catch (cause) {
+ throw new NativeCompilerLoadError(platformPackage, cause)
+ }
+ }
+ default:
+ throw new InvalidEngineError(engine)
+ }
+ }
+}
+
+interface RealpathFunctions {
+ readonly native: (filename: string) => string
+ readonly regular: (filename: string) => string
+}
+
+/** @internal Concrete path boundary for source tests; not exported by the barrel. */
+export function compilerRealpath(
+ filename: string,
+ realpath: RealpathFunctions = {
+ native: realpathSync.native,
+ regular: realpathSync,
+ },
+): string {
+ try {
+ return realpath.native(filename)
+ } catch (error) {
+ if (
+ typeof error === 'object' &&
+ error !== null &&
+ 'code' in error &&
+ error.code === 'EISDIR'
+ ) {
+ return realpath.regular(filename)
+ }
+ throw error
+ }
+}
+
+function physicalRequire() {
+ // CJS build substitutes module.filename; ESM keeps its own file URL.
+ const location = import.meta.url
+ const filename = isAbsolute(location) ? location : fileURLToPath(location)
+ return createRequire(compilerRealpath(filename))
+}
+
+const selectCompiler = createCompilerSelector({
+ native() {
+ const compiler: typeof import('@devup-ui/binding') =
+ physicalRequire()('@devup-ui/binding')
+ return compiler
+ },
+ wasm() {
+ const compiler: typeof import('@devup-ui/wasm') =
+ physicalRequire()('@devup-ui/wasm')
+ return compiler
+ },
+ platform() {
+ return nativePlatformPackage({
+ platform: process.platform,
+ arch: process.arch,
+ report: process.report.getReport,
+ })
+ },
+})
+
+/** Select synchronously without resolving or initializing the unselected engine. */
+export function loadCompiler(engine?: DevupEngine): DevupCompiler {
+ return selectCompiler(engine)
+}
diff --git a/packages/plugin-utils/src/index.ts b/packages/plugin-utils/src/index.ts
index 0157f5a73..bfc7b1e1b 100644
--- a/packages/plugin-utils/src/index.ts
+++ b/packages/plugin-utils/src/index.ts
@@ -3,6 +3,12 @@ export {
getBuildOwnership,
type ResettableEngine,
} from './build-session'
+export { loadCompiler } from './compiler'
+export type {
+ DevupCompiler,
+ DevupCompilerOutput,
+ DevupEngine,
+} from './compiler-types'
export {
type AtomHoistPlan,
buildCanonicalMap,
diff --git a/packages/plugin-utils/src/native-platform.ts b/packages/plugin-utils/src/native-platform.ts
new file mode 100644
index 000000000..c633c5e0d
--- /dev/null
+++ b/packages/plugin-utils/src/native-platform.ts
@@ -0,0 +1,58 @@
+export interface NativeHost {
+ readonly platform: string
+ readonly arch: string
+ readonly report: () => unknown
+}
+
+export class NativePlatformError extends Error {
+ readonly name = 'NativePlatformError'
+
+ constructor(
+ readonly host: Pick,
+ cause?: unknown,
+ ) {
+ super(
+ `Cannot select @devup-ui/binding target for ${host.platform}/${host.arch}`,
+ {
+ cause,
+ },
+ )
+ }
+}
+
+/** Diagnostic dispatch only; the binding's private loader remains authoritative. */
+export function nativePlatformPackage(host: NativeHost): string {
+ if (host.arch !== 'x64' && host.arch !== 'arm64') {
+ throw new NativePlatformError(host)
+ }
+ switch (host.platform) {
+ case 'win32':
+ return `@devup-ui/binding-win32-${host.arch}-msvc`
+ case 'darwin':
+ return `@devup-ui/binding-darwin-${host.arch}`
+ case 'linux': {
+ try {
+ const report = host.report()
+ if (
+ typeof report !== 'object' ||
+ report === null ||
+ !('header' in report)
+ ) {
+ throw new NativePlatformError(host)
+ }
+ const header = report.header
+ if (typeof header !== 'object' || header === null) {
+ throw new NativePlatformError(host)
+ }
+ const gnu =
+ 'glibcVersionRuntime' in header && header.glibcVersionRuntime
+ return `@devup-ui/binding-linux-${host.arch}-${gnu ? 'gnu' : 'musl'}`
+ } catch (cause) {
+ if (cause instanceof NativePlatformError) throw cause
+ throw new NativePlatformError(host, cause)
+ }
+ }
+ default:
+ throw new NativePlatformError(host)
+ }
+}
diff --git a/packages/plugin-utils/src/shared.ts b/packages/plugin-utils/src/shared.ts
index 9d5710be4..20b7aac0e 100644
--- a/packages/plugin-utils/src/shared.ts
+++ b/packages/plugin-utils/src/shared.ts
@@ -1,3 +1,4 @@
+import type { DevupEngine } from './compiler-types'
import type { CustomShorthands, ImportAliases } from './types'
/**
@@ -94,6 +95,7 @@ export function createThemeInterfaceArgs(
* Common plugin options shared across all devup-ui build plugins.
*/
export interface DevupUIBasePluginOptions {
+ engine?: DevupEngine
package: string
cssDir: string
devupFile: string
diff --git a/packages/rsbuild-plugin/README.md b/packages/rsbuild-plugin/README.md
index 404fc2997..c94984cf7 100644
--- a/packages/rsbuild-plugin/README.md
+++ b/packages/rsbuild-plugin/README.md
@@ -177,6 +177,26 @@ const box =
const box =
```
+## Compiler engine
+
+Rsbuild uses the native compiler by default. Explicit selections are supported:
+
+```ts
+DevupUI() // native
+DevupUI({ engine: 'native' })
+DevupUI({ engine: 'wasm' })
+```
+
+Both engines are installed through plugin-utils. Keep optional dependencies
+enabled for the native platform package. A native load failure names that
+package and preserves the original error as its cause. There is no automatic
+fallback; select `engine: 'wasm'` on hosts without a working native image or
+required native libraries. Malformed shorthands fail before backend selection.
+
+The selected compiler serves configuration, reservation, transforms and delayed
+watch/build callbacks together. Neither engine supports concurrent independent
+builds in the same process.
+
## Custom Shorthands
```ts
diff --git a/packages/rsbuild-plugin/package.json b/packages/rsbuild-plugin/package.json
index f12ffa121..b297ef947 100644
--- a/packages/rsbuild-plugin/package.json
+++ b/packages/rsbuild-plugin/package.json
@@ -44,7 +44,6 @@
"@devup-ui/wasm": "workspace:^"
},
"peerDependencies": {
- "@devup-ui/wasm": "*",
"@rsbuild/core": "*"
},
"devDependencies": {
diff --git a/packages/rsbuild-plugin/src/__tests__/audit-engine.cjs b/packages/rsbuild-plugin/src/__tests__/audit-engine.cjs
index 1b04a7c0b..df2b6b0cc 100644
--- a/packages/rsbuild-plugin/src/__tests__/audit-engine.cjs
+++ b/packages/rsbuild-plugin/src/__tests__/audit-engine.cjs
@@ -1,6 +1,5 @@
const { basename } = require('node:path')
-/** @type {typeof import('@devup-ui/wasm')} */
-const wasm = require('@devup-ui/wasm')
+const wasm = require('@devup-ui/plugin-utils').loadCompiler('wasm')
/** @type {unknown[]} */
const batches = []
/** @type {{name:string,code:string,map:string|undefined,before:number,after:number}[]} */
diff --git a/packages/rsbuild-plugin/src/__tests__/audit-native.cjs b/packages/rsbuild-plugin/src/__tests__/audit-native.cjs
index f8bcaab65..b31f1b87a 100644
--- a/packages/rsbuild-plugin/src/__tests__/audit-native.cjs
+++ b/packages/rsbuild-plugin/src/__tests__/audit-native.cjs
@@ -53,7 +53,7 @@ async function native() {
plugins:
mode === 'reachable-native'
? []
- : [DevupUI({ singleCss: true }), observer],
+ : [DevupUI({ engine: 'wasm', singleCss: true }), observer],
source: { entry: { main: './src/entry.js' } },
output: {
distPath: { root: 'out' },
diff --git a/packages/rsbuild-plugin/src/__tests__/checkout-isolation.test.ts b/packages/rsbuild-plugin/src/__tests__/checkout-isolation.test.ts
index f607852db..c85723550 100644
--- a/packages/rsbuild-plugin/src/__tests__/checkout-isolation.test.ts
+++ b/packages/rsbuild-plugin/src/__tests__/checkout-isolation.test.ts
@@ -2,7 +2,7 @@ import * as fs from 'node:fs'
import * as fsPromises from 'node:fs/promises'
import { join } from 'node:path'
-import * as wasm from '@devup-ui/wasm'
+import { type DevupCompilerOutput, loadCompiler } from '@devup-ui/plugin-utils'
import {
afterAll,
beforeAll,
@@ -15,7 +15,8 @@ import {
import { DevupUI } from '../plugin'
-type CodeExtractResult = ReturnType
+const wasm = loadCompiler('wasm')
+type CodeExtractResult = DevupCompilerOutput
type RsbuildPlugin = ReturnType
type RsbuildSetupContext = Parameters[0]
@@ -45,10 +46,11 @@ beforeAll(() => {
css: '',
cssFile: 'devup-ui-0.css',
map: undefined,
+ dependencies: [],
updatedBaseStyle: false,
free: mock(),
[Symbol.dispose]: mock(),
- } as unknown as CodeExtractResult)
+ } satisfies CodeExtractResult)
})
afterAll(() => {
@@ -63,6 +65,7 @@ afterAll(() => {
async function extractedCssDirIn(checkout: string) {
const transform = mock()
const plugin = DevupUI({
+ engine: 'wasm',
distDir: join(checkout, 'df'),
cssDir: join(checkout, 'df', 'devup-ui'),
})
diff --git a/packages/rsbuild-plugin/src/__tests__/entry-revisions.cjs b/packages/rsbuild-plugin/src/__tests__/entry-revisions.cjs
index 576394699..fcab3be25 100644
--- a/packages/rsbuild-plugin/src/__tests__/entry-revisions.cjs
+++ b/packages/rsbuild-plugin/src/__tests__/entry-revisions.cjs
@@ -43,7 +43,7 @@ async function main() {
cwd: root,
config: {
mode: 'production',
- plugins: [DevupUI({ singleCss: true }), observer],
+ plugins: [DevupUI({ engine: 'wasm', singleCss: true }), observer],
source: { entry: { main: './src/entry.js' } },
environments: {
first: { output: { distPath: { root: 'out-first' } } },
diff --git a/packages/rsbuild-plugin/src/__tests__/plugin.test.ts b/packages/rsbuild-plugin/src/__tests__/plugin.test.ts
index 1194b58c9..15afbd3ad 100644
--- a/packages/rsbuild-plugin/src/__tests__/plugin.test.ts
+++ b/packages/rsbuild-plugin/src/__tests__/plugin.test.ts
@@ -4,7 +4,6 @@ import * as fsPromises from 'node:fs/promises'
import { join, resolve } from 'node:path'
import * as pluginUtils from '@devup-ui/plugin-utils'
-import * as wasm from '@devup-ui/wasm'
import {
afterAll,
afterEach,
@@ -16,9 +15,58 @@ import {
spyOn,
} from 'bun:test'
-import { DevupUI } from '../plugin'
+import { DevupUI as createPlugin } from '../plugin'
+
+const wasm = pluginUtils.loadCompiler('wasm')
+const DevupUI = (options: Parameters[0] = {}) =>
+ createPlugin({ ...options, engine: 'wasm' })
describe('shorthand creation boundary', () => {
+ it.each([undefined, 'native'] as const)(
+ 'registers real native shorthands when engine=%s',
+ (engine) => {
+ // Given
+ const native = pluginUtils.loadCompiler('native')
+ native.resetStateForTesting()
+ // When
+ createPlugin({ engine, shorthands: { insetX: ['left', 'right'] } })
+ const output = native.codeExtract(
+ 'engine.tsx',
+ 'import { Box } from "@devup-ui/react"; export const box = ',
+ '@devup-ui/react',
+ './df',
+ true,
+ false,
+ true,
+ {},
+ )
+ try {
+ // Then
+ expect(output.css).toContain('left:19px')
+ expect(output.css).toContain('right:19px')
+ } finally {
+ output.free()
+ native.resetStateForTesting()
+ }
+ },
+ )
+
+ it('rejects malformed shorthand before consulting the selector', () => {
+ // Given
+ const selection = spyOn(pluginUtils, 'loadCompiler')
+ try {
+ // When / Then
+ expect(() =>
+ Reflect.apply(createPlugin, undefined, [
+ { engine: 'invalid', shorthands: { bgx: ['backgroundColour'] } },
+ ]),
+ ).toThrow(pluginUtils.ShorthandConfigError)
+ expect(selection).not.toHaveBeenCalled()
+ } finally {
+ selection.mockRestore()
+ }
+ })
+
it('registers canonical targets when the plugin is created', () => {
const registration = spyOn(wasm, 'registerShorthands').mockReturnValue(
undefined,
@@ -54,7 +102,7 @@ describe('shorthand creation boundary', () => {
})
})
-type CodeExtractResult = ReturnType
+type CodeExtractResult = pluginUtils.DevupCompilerOutput
type RsbuildPlugin = ReturnType
type RsbuildSetupContext = Parameters[0]
@@ -66,11 +114,12 @@ function createCodeExtractResult(
css: '',
cssFile: 'devup-ui.css',
map: undefined,
+ dependencies: [],
updatedBaseStyle: false,
free: mock(),
[Symbol.dispose]: mock(),
...overrides,
- } as unknown as CodeExtractResult
+ }
}
function createSetupContext(
@@ -473,7 +522,7 @@ const App = () => `,
expect(writeFileSpy).toHaveBeenCalledWith(
resolve('df', 'devup-ui', 'devup-ui.css'),
- expect.stringMatching(/\/\* src\/App\.tsx \d+ \*\//),
+ wasm.getCss(null, false),
'utf-8',
)
@@ -516,6 +565,7 @@ const App = () => `,
await watchRun()
expect(registerThemeSpy).not.toHaveBeenCalled()
statSpy.mockResolvedValueOnce({ mtimeMs: 2 } as fs.Stats)
+ createPlugin({ engine: 'native' })
await watchRun()
expect(registerThemeSpy).toHaveBeenCalledWith({
tailwindCss: '@theme { --color-brand: #0af; }',
@@ -1048,6 +1098,7 @@ const App = () => `,
const compiler = {
options: {},
watchMode: false,
+ modifiedFiles: new Set(),
rspack: { Compilation: { PROCESS_ASSETS_STAGE_REPORT: 5000 } },
hooks: {
run: { tap: tap('run') },
@@ -1071,9 +1122,44 @@ const App = () => `,
resourcePath,
environment: { name: 'web' },
})
- return { onBeforeBuild, compiler, compilation, taps, serve }
+ return {
+ onBeforeBuild,
+ compiler,
+ compilation,
+ taps,
+ serve,
+ transform,
+ }
}
+ it.each([
+ ['A', 'B'],
+ ['B', 'A'],
+ ])(
+ 'writes stylesheet bytes when sources run %s then %s',
+ async (first, second) => {
+ // Given
+ const { transform } = await setup({ singleCss: true })
+ const css = '.a{color:red}.b{color:blue}'
+ getCssSpy.mockReturnValue(css)
+ writeFileSpy.mockClear()
+ // When
+ for (const name of [first, second]) {
+ await transform.mock.calls[1][1]({
+ code: 'source',
+ resourcePath: resolve('src', `${name}.tsx`),
+ addDependency: mock(),
+ })
+ }
+ // Then
+ expect(writeFileSpy.mock.calls.at(-1)).toEqual([
+ resolve('df', 'devup-ui', 'devup-ui.css'),
+ css,
+ 'utf-8',
+ ])
+ },
+ )
+
it('extracts the files the entries reach before building', async () => {
const { onBeforeBuild } = await setup({ atomHoist: 0 })
codeExtractSpy.mockClear()
@@ -1185,11 +1271,40 @@ const App = () => `,
taps.run!()
expect(taps.needAdditionalPass!()).toBe(true)
- // the dev server rebuilds through the files the transforms write
+ // Given: watch revisions cache the previously consumed stylesheet.
compiler.watchMode = true
- taps.start!(compilation)
- taps.finishModules!()
- expect(taps.needAdditionalPass!()).toBe(false)
+ const { watchRun, start } = taps
+ assert(watchRun && start)
+ const source = resolve('src', 'App.tsx')
+ const perFile = resolve('df', 'devup-ui', 'devup-ui-1.css')
+ const base = resolve('df', 'devup-ui', 'devup-ui.css')
+ for (const css of ['watch-one', 'watch-two']) {
+ // When: a new watchRun changes styles without serving the cached CSS again.
+ watchRun()
+ const original = new Set([source])
+ compiler.modifiedFiles = original
+ start(compilation)
+ getCssSpy.mockImplementation((fileNum: number | null) =>
+ fileNum === null ? 'watch-one' : css,
+ )
+ taps.finishModules?.()
+ compilation.deleteAsset.mockClear()
+ taps.processAssets?.()
+ // Then: each edit invalidates stale CSS while preserving the modified source.
+ expect(compilation.deleteAsset).toHaveBeenCalledWith('index.js')
+ expect(taps.needAdditionalPass?.()).toBe(true)
+ expect(compiler.modifiedFiles).not.toBe(original)
+ expect(original).toEqual(new Set([source]))
+ expect(compiler.modifiedFiles).toEqual(
+ new Set([source, perFile, ...(css === 'watch-one' ? [base] : [])]),
+ )
+ compilation.deleteAsset.mockClear()
+ start(compilation)
+ taps.finishModules?.()
+ taps.processAssets?.()
+ expect(compilation.deleteAsset).not.toHaveBeenCalled()
+ expect(taps.needAdditionalPass?.()).toBe(false)
+ }
})
it('keeps the pass when every stylesheet is current', async () => {
diff --git a/packages/rsbuild-plugin/src/__tests__/reservation-dev.cjs b/packages/rsbuild-plugin/src/__tests__/reservation-dev.cjs
index 92ea8a51e..11e2d3cf6 100644
--- a/packages/rsbuild-plugin/src/__tests__/reservation-dev.cjs
+++ b/packages/rsbuild-plugin/src/__tests__/reservation-dev.cjs
@@ -10,7 +10,16 @@ async function runDev(rsbuild, state) {
const first = nextCompile()
rsbuild.onAfterDevCompile(({ stats }) => {
if (stats.hasErrors()) complete.reject(new Error(stats.toString()))
- else complete.resolve(stats)
+ else if (
+ (stats.stats ?? [stats]).some(
+ ({ compilation }) => compilation.needAdditionalPass,
+ )
+ ) {
+ for (const { compilation } of stats.stats ?? [stats]) {
+ assert(compilation.needAdditionalPass)
+ assert.equal(compilation.getAssets().length, 0)
+ }
+ } else complete.resolve(stats)
})
let server
try {
diff --git a/packages/rsbuild-plugin/src/__tests__/reservation-native.cjs b/packages/rsbuild-plugin/src/__tests__/reservation-native.cjs
index 5c8e2d3e0..b8d9e9a9d 100644
--- a/packages/rsbuild-plugin/src/__tests__/reservation-native.cjs
+++ b/packages/rsbuild-plugin/src/__tests__/reservation-native.cjs
@@ -2,7 +2,7 @@ const assert = require('node:assert/strict')
const { writeFileSync } = require('node:fs')
const { basename } = require('node:path')
const { createRsbuild } = require('@rsbuild/core')
-const wasm = require('@devup-ui/wasm')
+const wasm = require('@devup-ui/plugin-utils').loadCompiler('wasm')
const { put } = require('./reservation-fixture.cjs')
const { runDev } = require('./reservation-dev.cjs')
const { runWatch } = require('./reservation-watch.cjs')
@@ -101,7 +101,7 @@ async function main() {
config: {
mode: mode === 'dev' ? 'development' : 'production',
plugins: [
- DevupUI({ singleCss: true, include: ['included-ui'] }),
+ DevupUI({ engine: 'wasm', singleCss: true, include: ['included-ui'] }),
observer,
],
source: { entry: { main: './src/entry.js' } },
diff --git a/packages/rsbuild-plugin/src/__tests__/reservation-scenarios.cjs b/packages/rsbuild-plugin/src/__tests__/reservation-scenarios.cjs
index 3b83bc435..fced3369b 100644
--- a/packages/rsbuild-plugin/src/__tests__/reservation-scenarios.cjs
+++ b/packages/rsbuild-plugin/src/__tests__/reservation-scenarios.cjs
@@ -2,7 +2,7 @@ const assert = require('node:assert/strict')
const { writeFileSync } = require('node:fs')
const { join } = require('node:path')
const { createRsbuild } = require('@rsbuild/core')
-const wasm = require('@devup-ui/wasm')
+const wasm = require('@devup-ui/plugin-utils').loadCompiler('wasm')
const { put } = require('./reservation-fixture.cjs')
const [root, scenario, receipt] = process.argv.slice(2)
@@ -41,7 +41,11 @@ wasm.setAtomHoist = (value) => {
const { DevupUI } = require('../../dist/index.cjs')
put(root, 'devup.json', '{}')
const atom = scenario === 'atom' || scenario === 'noop'
-const options = { singleCss: !atom, atomHoist: atom ? 2 : undefined }
+const options = {
+ engine: 'wasm',
+ singleCss: !atom,
+ atomHoist: atom ? 2 : undefined,
+}
const config = {
mode: 'production',
plugins: [DevupUI(options)],
diff --git a/packages/rsbuild-plugin/src/plugin.ts b/packages/rsbuild-plugin/src/plugin.ts
index 41223de1e..91dae66c8 100644
--- a/packages/rsbuild-plugin/src/plugin.ts
+++ b/packages/rsbuild-plugin/src/plugin.ts
@@ -13,8 +13,11 @@ import {
createStateWriter,
createThemeInterfaceArgs,
type CustomShorthands,
+ type DevupCompiler,
+ type DevupUIBasePluginOptions,
getFileNumByFilename,
type ImportAliases,
+ loadCompiler,
loadDevupConfig,
mergeImportAliases,
normalizeShorthands,
@@ -23,28 +26,16 @@ import {
tailwindCssFiles,
withTailwindCss,
} from '@devup-ui/plugin-utils'
-import {
- codeExtract,
- getCss,
- getDefaultTheme,
- getThemeInterface,
- importCanonicalMap,
- importFileRoutes,
- registerShorthands,
- registerTheme,
- resetBuildState,
- setAtomHoist,
- setDebug,
- setModuleResolver,
- setPrefix,
-} from '@devup-ui/wasm'
import type { RsbuildPlugin, Rspack } from '@rsbuild/core'
import { createReservation, staticEntries } from './reservation'
const PLUGIN_NAME = 'devup-ui-rsbuild-plugin'
-export interface DevupUIRsbuildPluginOptions {
+export interface DevupUIRsbuildPluginOptions extends Pick<
+ DevupUIBasePluginOptions,
+ 'engine'
+> {
package: string
cssDir: string
devupFile: string
@@ -84,15 +75,16 @@ async function writeDataFiles(
DevupUIRsbuildPluginOptions,
'extractCss' | 'debug' | 'include'
>,
+ compiler: DevupCompiler,
): Promise {
let tailwindFiles: string[] = []
try {
const config = await loadDevupConfig(options.devupFile)
const theme = config.theme ?? {}
- registerTheme(withTailwindCss(theme, config))
+ compiler.registerTheme(withTailwindCss(theme, config))
tailwindFiles = tailwindCssFiles(config)
- const interfaceCode = getThemeInterface(
+ const interfaceCode = compiler.getThemeInterface(
...createThemeInterfaceArgs(options.package),
)
@@ -105,14 +97,17 @@ async function writeDataFiles(
}
} catch (error) {
console.error(error)
- registerTheme({})
+ compiler.registerTheme({})
}
await Promise.all([
!existsSync(options.cssDir)
? mkdir(options.cssDir, { recursive: true })
: Promise.resolve(),
!options.singleCss
- ? writeFile(join(options.cssDir, 'devup-ui.css'), getCss(null, false))
+ ? writeFile(
+ join(options.cssDir, 'devup-ui.css'),
+ compiler.getCss(null, false),
+ )
: Promise.resolve(),
])
return tailwindFiles
@@ -139,10 +134,13 @@ export const DevupUI = ({
singleCss = false,
prefix,
shorthands,
+ engine,
atomHoist = 2,
importAliases: userImportAliases,
}: Partial = {}): RsbuildPlugin => {
- registerShorthands(normalizeShorthands(shorthands ?? {}))
+ const normalizedShorthands = normalizeShorthands(shorthands ?? {})
+ const compiler = loadCompiler(engine)
+ compiler.registerShorthands(normalizedShorthands)
const importAliases = mergeImportAliases(
userImportAliases,
readJsxImportSource(),
@@ -156,10 +154,10 @@ export const DevupUI = ({
async setup(api) {
// A build starts from its own options, not from what an earlier build
// in this process left in the engine
- const endBuild = beginBuild({ resetBuildState })
+ const endBuild = beginBuild(compiler)
api.onCloseBuild?.(endBuild)
- setDebug(debug)
- setPrefix(prefix ?? null)
+ compiler.setDebug(debug)
+ compiler.setPrefix(prefix ?? null)
if (!existsSync(distDir)) await mkdir(distDir, { recursive: true })
await writeFile(join(distDir, '.gitignore'), '*', 'utf-8')
@@ -169,13 +167,16 @@ export const DevupUI = ({
'utf-8',
)
- let tailwindFiles = await writeDataFiles({
- package: libPackage,
- cssDir,
- devupFile,
- distDir,
- singleCss,
- })
+ let tailwindFiles = await writeDataFiles(
+ {
+ package: libPackage,
+ cssDir,
+ devupFile,
+ distDir,
+ singleCss,
+ },
+ compiler,
+ )
if (!extractCss) return
// The project's Tailwind CSS defines what a Tailwind class compiles to:
@@ -183,7 +184,7 @@ export const DevupUI = ({
api.modifyRspackConfig((config) => {
config.plugins ??= []
config.plugins.push({
- apply(compiler: {
+ apply(owner: {
hooks: {
watchRun: {
tapPromise: (name: string, fn: () => Promise) => void
@@ -191,7 +192,7 @@ export const DevupUI = ({
}
}) {
let stamp: string | undefined
- compiler.hooks.watchRun.tapPromise(
+ owner.hooks.watchRun.tapPromise(
'DevupUIRsbuildPlugin',
async () => {
const current = (
@@ -205,13 +206,16 @@ export const DevupUI = ({
)
).join()
if (stamp !== undefined && stamp !== current) {
- tailwindFiles = await writeDataFiles({
- package: libPackage,
- cssDir,
- devupFile,
- distDir,
- singleCss,
- })
+ tailwindFiles = await writeDataFiles(
+ {
+ package: libPackage,
+ cssDir,
+ devupFile,
+ distDir,
+ singleCss,
+ },
+ compiler,
+ )
}
stamp = current
},
@@ -229,7 +233,7 @@ export const DevupUI = ({
// absolute keys (keyBy: 'absolute') and the extraction filename is
// POSIX-normalized to match.
const atomMode = !singleCss && Number.isFinite(atomHoist) && atomHoist > 0
- setModuleResolver(
+ compiler.setModuleResolver(
createModuleResolver({
toId: (path) => (atomMode ? path.replaceAll('\\', '/') : path),
}),
@@ -245,7 +249,7 @@ export const DevupUI = ({
cwd: root,
keyBy: 'absolute',
})
- importCanonicalMap(canonicalMap)
+ compiler.importCanonicalMap(canonicalMap)
const fileReach = computeFileReach({
srcDir,
tsconfigPath,
@@ -254,8 +258,8 @@ export const DevupUI = ({
})
const plan = planAtomHoist(canonicalMap, fileReach, atomHoist)
if (plan) {
- importFileRoutes(plan.reachByBucket)
- setAtomHoist(plan.threshold)
+ compiler.importFileRoutes(plan.reachByBucket)
+ compiler.setAtomHoist(plan.threshold)
} else {
console.info(
'[devup-ui] fewer than 2 routes were detected; atom hoisting is a no-op (single-entry/SPA).',
@@ -266,15 +270,18 @@ export const DevupUI = ({
}
}
- const reservation = createReservation({
- root: api.context.rootPath,
- package: libPackage,
- include,
- cssDir,
- singleCss,
- atomMode,
- importAliases,
- })
+ const reservation = createReservation(
+ {
+ root: api.context.rootPath,
+ package: libPackage,
+ include,
+ cssDir,
+ singleCss,
+ atomMode,
+ importAliases,
+ },
+ compiler,
+ )
const entryFiles = new Set()
// Extract the source files under `src` that the entries reach, in path
// order, the same way the transform does, so that a stylesheet built on
@@ -294,7 +301,7 @@ export const DevupUI = ({
)
if (!extractCssDir.startsWith('./'))
extractCssDir = `./${extractCssDir}`
- codeExtract(
+ compiler.codeExtract(
atomMode ? file.replaceAll('\\', '/') : file,
readFileSync(file, 'utf-8'),
libPackage,
@@ -324,7 +331,7 @@ export const DevupUI = ({
// where the entry code imports the base itself so that hoisted atoms
// load once (the injected splitChunks cacheGroup, see
// modifyRsbuildConfig, emits the base once)
- getCss(getFileNumByFilename(basename(resourcePath)), !atomMode)
+ compiler.getCss(getFileNumByFilename(basename(resourcePath)), !atomMode)
api.transform(
{
@@ -339,8 +346,8 @@ export const DevupUI = ({
// A stylesheet module is built on its first import, which can come before
// the modules whose styles it holds are extracted. When one was, compile
- // once more: every module is extracted by then. The dev server rebuilds it
- // through the stylesheet files the transforms write instead.
+ // once more: every module is extracted by then. Keep the consumed CSS
+ // across watch revisions so cached stylesheet modules are checked too.
api.modifyRspackConfig((config, { environment }) => {
for (const request of staticEntries(environment.entry))
entryFiles.add(resolve(api.context.rootPath, request))
@@ -348,6 +355,8 @@ export const DevupUI = ({
config.plugins.push({
apply(compiler: Rspack.Compiler) {
reservation.trackCompiler(environment.name, compiler)
+ const served = new Map()
+ servedCss.set(environment.name, served)
let passes = 0
compiler.hooks.run.tap(PLUGIN_NAME, () => {
passes = 0
@@ -355,17 +364,15 @@ export const DevupUI = ({
})
compiler.hooks.invalid.tap(PLUGIN_NAME, reservation.invalidate)
compiler.hooks.watchRun.tap(PLUGIN_NAME, () => {
+ passes = 0
reservation.reserve()
})
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
- const served = new Map()
- servedCss.set(environment.name, served)
const basePath = join(cssDir, 'devup-ui.css')
const base = stylesheet(basePath)
writeChanged(basePath, base)
let stale: string[] = []
compilation.hooks.finishModules.tap(PLUGIN_NAME, () => {
- if (compiler.watchMode) return
const changed = new Set(
[...served]
.filter(([path, css]) => stylesheet(path) !== css)
@@ -394,6 +401,10 @@ export const DevupUI = ({
passes += 1
for (const path of stale)
writeFileSync(path, stylesheet(path), 'utf-8')
+ compiler.modifiedFiles = new Set([
+ ...(compiler.modifiedFiles ?? []),
+ ...stale,
+ ])
return true
})
})
@@ -402,12 +413,13 @@ export const DevupUI = ({
})
api.modifyRsbuildConfig((config) => {
- const theme = getDefaultTheme()
+ const theme = compiler.getDefaultTheme()
if (theme) {
config.source ??= {}
config.source.define = {
- 'process.env.DEVUP_UI_DEFAULT_THEME':
- JSON.stringify(getDefaultTheme()),
+ 'process.env.DEVUP_UI_DEFAULT_THEME': JSON.stringify(
+ compiler.getDefaultTheme(),
+ ),
...config.source.define,
}
}
@@ -482,7 +494,7 @@ export const DevupUI = ({
cssFile,
updatedBaseStyle,
dependencies = [],
- } = codeExtract(
+ } = compiler.codeExtract(
extractName,
code,
libPackage,
@@ -499,7 +511,7 @@ export const DevupUI = ({
promises.push(
stateWriter.write(
join(cssDir, 'devup-ui.css'),
- getCss(null, false),
+ compiler.getCss(null, false),
'utf-8',
),
)
@@ -509,7 +521,7 @@ export const DevupUI = ({
promises.push(
stateWriter.write(
join(cssDir, basename(cssFile)),
- `/* ${resourcePath} ${Date.now()} */`,
+ stylesheet(cssFile),
'utf-8',
),
)
diff --git a/packages/rsbuild-plugin/src/reservation.ts b/packages/rsbuild-plugin/src/reservation.ts
index 829d04abb..897a03f6d 100644
--- a/packages/rsbuild-plugin/src/reservation.ts
+++ b/packages/rsbuild-plugin/src/reservation.ts
@@ -2,9 +2,9 @@ import { dirname, relative, resolve } from 'node:path'
import {
createSharedSlotReservation,
+ type DevupCompiler,
type WasmImportAliases,
} from '@devup-ui/plugin-utils'
-import { reserveClassSlots } from '@devup-ui/wasm'
import type { Rspack } from '@rsbuild/core'
import type { DevupUIRsbuildPluginOptions } from './plugin'
@@ -37,8 +37,9 @@ export function createReservation(
readonly atomMode: boolean
readonly importAliases: WasmImportAliases
},
+ compiler: Pick,
) {
- const slots = createSharedSlotReservation({ reserveClassSlots })
+ const slots = createSharedSlotReservation(compiler)
const roots = new Set(
[process.cwd(), options.root].flatMap((root) =>
['src', 'app'].map((name) => resolve(root, name)),
diff --git a/packages/vite-plugin/README.md b/packages/vite-plugin/README.md
index a943c028c..61bf1278b 100644
--- a/packages/vite-plugin/README.md
+++ b/packages/vite-plugin/README.md
@@ -177,6 +177,24 @@ const box =
const box =
```
+## Compiler engine
+
+Vite uses the native compiler by default. Select either engine explicitly:
+
+```ts
+DevupUI()
+DevupUI({ engine: 'native' })
+DevupUI({ engine: 'wasm' })
+```
+
+Keep optional dependencies enabled for the native platform package. Native load
+failures name that package and preserve the original error as their cause;
+there is no automatic fallback. Select `engine: 'wasm'` on hosts without a
+working native image or its required libraries. Invalid shorthands fail before
+engine selection. Configuration, reservation, transforms and delayed callbacks
+use the same selected compiler. Neither engine supports concurrent independent
+builds in one process; Vite environments within a build share its compiler state.
+
## Custom Shorthands
Custom shorthands are build-plugin options, not theme tokens. Every target
diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json
index d49c413e7..83518c347 100644
--- a/packages/vite-plugin/package.json
+++ b/packages/vite-plugin/package.json
@@ -49,7 +49,6 @@
"vite": "^8.3.5"
},
"peerDependencies": {
- "@devup-ui/wasm": "*",
"vite": "*"
}
}
diff --git a/packages/vite-plugin/src/__tests__/atom-hoist-policy.test.ts b/packages/vite-plugin/src/__tests__/atom-hoist-policy.test.ts
index 49be34851..563ce9d4f 100644
--- a/packages/vite-plugin/src/__tests__/atom-hoist-policy.test.ts
+++ b/packages/vite-plugin/src/__tests__/atom-hoist-policy.test.ts
@@ -1,7 +1,6 @@
import { join } from 'node:path'
import * as utils from '@devup-ui/plugin-utils'
-import * as wasm from '@devup-ui/wasm'
import { expect, it, spyOn } from 'bun:test'
import {
@@ -10,6 +9,8 @@ import {
} from '../../../plugin-utils/src/__tests__/reservation-fixture'
import { createViteReservation } from '../reservation'
+const wasm = utils.loadCompiler('wasm')
+
it.each([undefined, 0, -1, NaN, Infinity, -Infinity, 0.5, 1, 2, 3])(
'installs the numeric policy for %s before scratch, ignoring it in singleCss',
async (atomHoist) => {
@@ -35,7 +36,7 @@ it.each([undefined, 0, -1, NaN, Infinity, -Infinity, 0.5, 1, 2, 3])(
expect(canonical).toHaveBeenCalledTimes(enabled ? 1 : 0)
expect(threshold).toHaveBeenCalledTimes(enabled ? 1 : 0)
})
- const slots = createViteReservation({
+ const slots = createViteReservation(wasm, {
package: '@devup-ui/react',
include: [],
importAliases: {},
@@ -96,7 +97,7 @@ it('folds canonical reach and skips global buckets before installing the thresho
using _engine = spyOn(wasm, 'reserveClassSlots').mockImplementation(() => {
events.push('scratch')
})
- const slots = createViteReservation({
+ const slots = createViteReservation(wasm, {
package: '@devup-ui/react',
include: [],
importAliases: {},
diff --git a/packages/vite-plugin/src/__tests__/cache-invalidation-metadata-native.cjs b/packages/vite-plugin/src/__tests__/cache-invalidation-metadata-native.cjs
index da62ad1f2..f0c072507 100644
--- a/packages/vite-plugin/src/__tests__/cache-invalidation-metadata-native.cjs
+++ b/packages/vite-plugin/src/__tests__/cache-invalidation-metadata-native.cjs
@@ -69,6 +69,7 @@ async function main() {
const graph = {
idToModuleMap: modules,
getModuleById: (/** @type {string} */ id) => modules.get(id),
+ getModulesByFile: () => undefined,
invalidateModule(/** @type {unknown[]} */ ...args) {
invalidated.push(args)
},
diff --git a/packages/vite-plugin/src/__tests__/plugin-lifecycle-fixture.ts b/packages/vite-plugin/src/__tests__/plugin-lifecycle-fixture.ts
index 9d25ff6b1..d6bd4fe62 100644
--- a/packages/vite-plugin/src/__tests__/plugin-lifecycle-fixture.ts
+++ b/packages/vite-plugin/src/__tests__/plugin-lifecycle-fixture.ts
@@ -89,6 +89,12 @@ export function graph(ids: readonly string[] = []) {
return {
idToModuleMap,
getModuleById: (id: string) => idToModuleMap.get(id),
+ getModulesByFile: (file: string) => {
+ const nodes = [...idToModuleMap.values()].filter(
+ ({ id }) => id.split('?')[0] === file,
+ )
+ return nodes.length ? new Set(nodes) : undefined
+ },
invalidateModule: mock(),
}
}
@@ -109,6 +115,7 @@ export async function lifecycle(options: Parameters[0] = {}) {
const project = fixture()
project.write('devup.json', '{}')
const plugins = DevupUI({
+ engine: 'wasm',
singleCss: true,
debug: true,
distDir: join(project.root, 'df'),
diff --git a/packages/vite-plugin/src/__tests__/plugin-lifecycle.test.ts b/packages/vite-plugin/src/__tests__/plugin-lifecycle.test.ts
index 46b9f52ef..cc7f4edc5 100644
--- a/packages/vite-plugin/src/__tests__/plugin-lifecycle.test.ts
+++ b/packages/vite-plugin/src/__tests__/plugin-lifecycle.test.ts
@@ -1,7 +1,11 @@
import { join } from 'node:path'
-import type { ReservationRow } from '@devup-ui/plugin-utils'
-import * as wasm from '@devup-ui/wasm'
+import {
+ type DevupCompiler,
+ loadCompiler,
+ type ReservationEngine,
+ type ReservationRow,
+} from '@devup-ui/plugin-utils'
import { expect, it, mock, spyOn } from 'bun:test'
import { STYLE } from '../../../plugin-utils/src/__tests__/reservation-fixture'
@@ -12,6 +16,8 @@ import {
watcher,
} from './plugin-lifecycle-fixture'
+const wasm: DevupCompiler & ReservationEngine = loadCompiler('wasm')
+
it('reserves actual server environment union and detaches each watcher lifetime on replacement and close', async () => {
// Given
using reserve = spyOn(wasm, 'reserveClassSlots').mockImplementation(() => {})
diff --git a/packages/vite-plugin/src/__tests__/plugin.test.ts b/packages/vite-plugin/src/__tests__/plugin.test.ts
index c10009e80..cbc3643da 100644
--- a/packages/vite-plugin/src/__tests__/plugin.test.ts
+++ b/packages/vite-plugin/src/__tests__/plugin.test.ts
@@ -4,7 +4,6 @@ import * as os from 'node:os'
import * as nodePath from 'node:path'
import * as pluginUtils from '@devup-ui/plugin-utils'
-import * as wasm from '@devup-ui/wasm'
import {
afterEach,
beforeEach,
@@ -18,7 +17,26 @@ import type { Plugin } from 'vite'
import { DevupUI } from '../plugin'
+const wasm = pluginUtils.loadCompiler('wasm')
+
describe('shorthand creation boundary', () => {
+ it.each([undefined, 'native', 'wasm'] as const)(
+ 'passes engine=%s to the selector',
+ (engine) => {
+ using selection = spyOn(pluginUtils, 'loadCompiler').mockReturnValue(wasm)
+ // When
+ const [plugin] = DevupUI(
+ engine === undefined ? undefined : { engine },
+ ) as unknown as [ViteTestPlugin]
+ try {
+ // Then
+ expect(selection.mock.calls).toEqual([[engine]])
+ } finally {
+ plugin.closeBundle()
+ }
+ },
+ )
+
it('registers canonical targets when the plugin is created', () => {
const registration = spyOn(wasm, 'registerShorthands').mockReturnValue(
undefined,
@@ -38,16 +56,18 @@ describe('shorthand creation boundary', () => {
})
it('rejects a typo before registration when runtime options bypass types', () => {
+ using selection = spyOn(pluginUtils, 'loadCompiler')
const registration = spyOn(wasm, 'registerShorthands').mockReturnValue(
undefined,
)
try {
expect(() =>
Reflect.apply(DevupUI, undefined, [
- { shorthands: { bgx: ['backgroundColour'] } },
+ { engine: 'invalid', shorthands: { bgx: ['backgroundColour'] } },
]),
).toThrow(pluginUtils.ShorthandConfigError)
expect(registration).not.toHaveBeenCalled()
+ expect(selection).not.toHaveBeenCalled()
} finally {
registration.mockRestore()
}
@@ -81,7 +101,10 @@ interface ViteConfig {
interface HotUpdateEnvironment {
config: { consumer: 'client' | 'server' }
- moduleGraph: { invalidateModule: (...args: unknown[]) => void }
+ moduleGraph: {
+ invalidateModule: (...args: unknown[]) => void
+ getModulesByFile: (file: string) => Set