From 6b744b27b6092a31d066e40343d6c2bb6ffe7674 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Thu, 23 Jul 2026 13:29:10 -0700 Subject: [PATCH] feat(web): use `vite` for generation --- .changeset/vite-web-generator.md | 5 + .prettierignore | 1 + eslint.config.mjs | 10 +- package-lock.json | 623 ++++++++++++++---- package.json | 6 +- src/generators/web/README.md | 153 ++--- .../web/__tests__/generate.test.mjs | 106 ++- src/generators/web/generate.mjs | 20 +- src/generators/web/index.mjs | 18 +- src/generators/web/template.html | 9 +- src/generators/web/types.d.ts | 19 +- .../web/ui/components/Layout/index.jsx | 2 +- src/generators/web/ui/hooks/useTheme.mjs | 4 +- src/generators/web/ui/index.css | 5 +- src/generators/web/ui/types.d.ts | 7 +- .../web/utils/__tests__/vite.test.mjs | 131 ++++ src/generators/web/utils/bundle.mjs | 175 ----- src/generators/web/utils/chunks.mjs | 43 -- src/generators/web/utils/config.mjs | 8 +- src/generators/web/utils/css.mjs | 122 ---- src/generators/web/utils/generate.mjs | 4 +- src/generators/web/utils/processing.mjs | 114 +--- src/generators/web/utils/vite.mjs | 346 ++++++++++ 23 files changed, 1210 insertions(+), 721 deletions(-) create mode 100644 .changeset/vite-web-generator.md create mode 100644 src/generators/web/utils/__tests__/vite.test.mjs delete mode 100644 src/generators/web/utils/bundle.mjs delete mode 100644 src/generators/web/utils/chunks.mjs delete mode 100644 src/generators/web/utils/css.mjs create mode 100644 src/generators/web/utils/vite.mjs diff --git a/.changeset/vite-web-generator.md b/.changeset/vite-web-generator.md new file mode 100644 index 00000000..02261dd1 --- /dev/null +++ b/.changeset/vite-web-generator.md @@ -0,0 +1,5 @@ +--- +'@node-core/doc-kit': minor +--- + +Rebuild the `web` generator around Vite 8.1 diff --git a/.prettierignore b/.prettierignore index e3d545c2..4cc96956 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,6 +12,7 @@ out/ # Docs-site content, assembled by scripts/build-docs-content.mjs www/content/ +www/out/ # Generated Files src/generators/metadata/maps/mdn.json diff --git a/eslint.config.mjs b/eslint.config.mjs index 67be361c..79d5205b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,7 +10,11 @@ export default defineConfig([ importX.flatConfigs.recommended, react.configs.recommended, { - ignores: ['out/', 'src/generators/api-links/__tests__/fixtures/'], + ignores: [ + 'www/out', + 'out/', + 'src/generators/api-links/__tests__/fixtures/', + ], }, { files: ['**/*.{mjs,jsx,js}'], @@ -97,10 +101,6 @@ export default defineConfig([ languageOptions: { globals: { ...globals.browser, - // SERVER and CLIENT denote server-only and client-only - // codepaths in our web generator - CLIENT: 'readonly', - SERVER: 'readonly', }, ecmaVersion: 'latest', }, diff --git a/package-lock.json b/package-lock.json index e0ce748d..e258460f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,13 @@ "version": "1.4.3", "dependencies": { "@actions/core": "^3.0.0", + "@fontsource-variable/open-sans": "^5.3.0", + "@fontsource/ibm-plex-mono": "^5.3.0", "@heroicons/react": "^2.2.0", "@node-core/rehype-shiki": "^1.4.2", "@node-core/ui-components": "^1.7.2", "@orama/orama": "^3.1.18", "@orama/ui": "^1.5.4", - "@rollup/plugin-virtual": "^3.0.2", "@swc/html-wasm": "^1.15.43", "acorn": "^8.17.0", "commander": "^15.0.0", @@ -25,7 +26,6 @@ "glob-parent": "^6.0.2", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", - "lightningcss-wasm": "^1.32.0", "mdast-util-slice-markdown": "^2.0.1", "oxc-parser": "^0.139.0", "piscina": "^5.2.0", @@ -41,7 +41,6 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-stringify": "^11.0.0", - "rolldown": "1.1.3", "semver": "^7.8.5", "shiki": "^4.3.0", "tinyglobby": "^0.2.17", @@ -52,6 +51,7 @@ "unist-util-remove": "^4.0.0", "unist-util-select": "^5.1.0", "unist-util-visit": "^5.1.0", + "vite": "~8.1.5", "yaml": "^2.9.0" }, "bin": { @@ -781,6 +781,24 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@fontsource-variable/open-sans": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/open-sans/-/open-sans-5.3.0.tgz", + "integrity": "sha512-VOfhZfLIOBVa6K/dTEu7scqQXYdGa4jBX8quFpQ0S6574B+W2EOBJf9PkqyVtMEI7WWIWNEbpvPc3vY1mN2mmQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/ibm-plex-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz", + "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@heroicons/react": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", @@ -1940,9 +1958,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", - "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -2766,9 +2784,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", - "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -2782,9 +2800,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", - "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -2798,9 +2816,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", - "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -2814,9 +2832,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", - "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -2830,9 +2848,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", - "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -2846,12 +2864,15 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", - "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2862,12 +2883,15 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", - "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2878,12 +2902,15 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", - "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2894,12 +2921,15 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", - "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2910,12 +2940,15 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", - "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2926,12 +2959,15 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", - "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2942,9 +2978,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", - "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -2958,9 +2994,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", - "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ "wasm32" ], @@ -2976,9 +3012,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", - "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ "arm64" ], @@ -2992,9 +3028,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", - "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -3013,23 +3049,6 @@ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, - "node_modules/@rollup/plugin-virtual": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", - "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, "node_modules/@shikijs/core": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.1.tgz", @@ -3249,7 +3268,7 @@ "version": "24.10.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.15.tgz", "integrity": "sha512-BgjLoRuSr0MTI5wA6gMw9Xy0sFudAaUuvrnjgGx9wZ522fYYLA5SYJ+1Y30vTcJEG+DRCyDHx/gzQVfofYzSdg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -4738,6 +4757,15 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -7044,17 +7072,47 @@ "node": ">= 0.8.0" } }, - "node_modules/lightningcss-wasm": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-wasm/-/lightningcss-wasm-1.32.0.tgz", - "integrity": "sha512-SteAkCtRuSCDYPGHKhLV/dDs5Bk+7I4QUxWxfk4xwsTI1rQk8MQyYtpGcd3NECsUGzK0q2/KqoVS+YHCqKHUTQ==", - "bundleDependencies": [ - "napi-wasm" - ], + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "license": "MPL-2.0", "dependencies": { - "napi-wasm": "^1.0.1" + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">= 12.0.0" }, @@ -7063,10 +7121,217 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-wasm/node_modules/napi-wasm": { - "version": "1.1.3", - "inBundle": true, - "license": "MIT" + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, "node_modules/lint-staged": { "version": "17.0.8", @@ -8450,6 +8715,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -8715,15 +8998,6 @@ "@oxc-parser/binding-win32-x64-msvc": "0.139.0" } }, - "node_modules/oxc-parser/node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/p-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", @@ -8908,7 +9182,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -8987,6 +9260,34 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/preact": { "version": "10.29.4", "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.4.tgz", @@ -9611,12 +9912,12 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", - "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.137.0", + "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -9626,21 +9927,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.3", - "@rolldown/binding-darwin-arm64": "1.1.3", - "@rolldown/binding-darwin-x64": "1.1.3", - "@rolldown/binding-freebsd-x64": "1.1.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", - "@rolldown/binding-linux-arm64-gnu": "1.1.3", - "@rolldown/binding-linux-arm64-musl": "1.1.3", - "@rolldown/binding-linux-ppc64-gnu": "1.1.3", - "@rolldown/binding-linux-s390x-gnu": "1.1.3", - "@rolldown/binding-linux-x64-gnu": "1.1.3", - "@rolldown/binding-linux-x64-musl": "1.1.3", - "@rolldown/binding-openharmony-arm64": "1.1.3", - "@rolldown/binding-wasm32-wasi": "1.1.3", - "@rolldown/binding-win32-arm64-msvc": "1.1.3", - "@rolldown/binding-win32-x64-msvc": "1.1.3" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/run-parallel": { @@ -9964,6 +10265,15 @@ "node": ">= 12" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -10582,7 +10892,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unified": { @@ -10902,6 +11212,97 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/package.json b/package.json index b043ceb6..5a4d144b 100644 --- a/package.json +++ b/package.json @@ -62,12 +62,13 @@ }, "dependencies": { "@actions/core": "^3.0.0", + "@fontsource-variable/open-sans": "^5.3.0", + "@fontsource/ibm-plex-mono": "^5.3.0", "@heroicons/react": "^2.2.0", "@node-core/rehype-shiki": "^1.4.2", "@node-core/ui-components": "^1.7.2", "@orama/orama": "^3.1.18", "@orama/ui": "^1.5.4", - "@rollup/plugin-virtual": "^3.0.2", "@swc/html-wasm": "^1.15.43", "acorn": "^8.17.0", "commander": "^15.0.0", @@ -78,7 +79,6 @@ "glob-parent": "^6.0.2", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", - "lightningcss-wasm": "^1.32.0", "mdast-util-slice-markdown": "^2.0.1", "oxc-parser": "^0.139.0", "piscina": "^5.2.0", @@ -94,7 +94,6 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-stringify": "^11.0.0", - "rolldown": "1.1.3", "semver": "^7.8.5", "shiki": "^4.3.0", "tinyglobby": "^0.2.17", @@ -105,6 +104,7 @@ "unist-util-remove": "^4.0.0", "unist-util-select": "^5.1.0", "unist-util-visit": "^5.1.0", + "vite": "~8.1.5", "yaml": "^2.9.0" } } diff --git a/src/generators/web/README.md b/src/generators/web/README.md index bd9b33b7..433a76cc 100644 --- a/src/generators/web/README.md +++ b/src/generators/web/README.md @@ -1,27 +1,29 @@ # `web` Generator -The `web` generator transforms JSX AST entries into complete web bundles, producing server-side rendered HTML pages, client-side JavaScript with code splitting, and bundled CSS styles. +The `web` generator transforms JSX AST entries into complete web bundles. Vite +builds server-rendered HTML and hashed client-side JavaScript, CSS, and imported +assets, then writes the complete static site to `output`. The generator is +output-only and does not return an in-memory copy of its HTML or CSS. ## Configuring The `web` generator accepts the following configuration options: -| Name | Type | Default | Description | -| ----------------- | --------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| `output` | `string` | - | The directory where HTML, JavaScript, and CSS files will be written | -| `templatePath` | `string` | `'template.html'` | Path to the HTML template file | -| `project` | `string` | `'Node.js'` | Project name used in page titles and the version selector | -| `title` | `string` | `'{project} v{version} Documentation'` | Title template for HTML pages (supports `{project}`, `{version}`) | -| `useAbsoluteURLs` | `boolean` | `false` | When `true`, all internal links use absolute URLs based on `baseURL` | -| `editURL` | `string` | `'${GITHUB_EDIT_URL}/doc/api{path}.md'` | URL template for "edit this page" links | -| `pageURL` | `string` | `'{baseURL}/latest-{version}/api{path}.html'` | URL template for documentation page links | -| `remoteConfigUrl` | `string` | `'https://nodejs.org/site.json'` | URL fetched client-side at runtime for remote site config (currently used to power the announcement banner) | -| `head` | `object` | See below | Configurable ``, ``, and raw markup for the document head | -| `lightningcss` | `object` | `{}` | Options spread into LightningCSS while bundling CSS (see below) | -| `imports` | `object` | See below | Object mapping `#theme/` aliases to component paths for customization | -| `virtualImports` | `object` | `{}` | Additional virtual module mappings merged into the build | -| `components` | `object` | `{}` | Maps JSX tag names to component imports, enabling JSX-in-MDX (see below) | -| `rolldown` | `object` | `{}` | Options merged into the Rolldown build — extra plugins, etc. (see below) | +| Name | Type | Default | Description | +| ----------------- | ------------ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `output` | `string` | Required | The directory where HTML and Vite's hashed client output are written | +| `templatePath` | `string` | `'template.html'` | Path to the HTML template file | +| `project` | `string` | `'Node.js'` | Project name used in page titles and the version selector | +| `title` | `string` | `'{project} v{version} Documentation'` | Title template for HTML pages (supports `{project}`, `{version}`) | +| `useAbsoluteURLs` | `boolean` | `false` | When `true`, all internal links use absolute URLs based on `baseURL` | +| `editURL` | `string` | `'${GITHUB_EDIT_URL}/doc/api{path}.md'` | URL template for "edit this page" links | +| `pageURL` | `string` | `'{baseURL}/latest-{version}/api{path}.html'` | URL template for documentation page links | +| `remoteConfigUrl` | `string` | `'https://nodejs.org/site.json'` | URL fetched client-side at runtime for remote site config (currently used to power the announcement banner) | +| `head` | `object` | See below | Configurable ``, ``, and raw markup for the document head | +| `imports` | `object` | See below | Object mapping `#theme/` aliases to component paths for customization | +| `virtualImports` | `object` | `{}` | Additional virtual module mappings merged into both Vite builds | +| `components` | `object` | `{}` | Maps JSX tag names to component imports, enabling JSX-in-MDX (see below) | +| `vite` | `UserConfig` | `{}` | Vite configuration merged into the client and SSR builds (see below) | ### `head` @@ -64,96 +66,58 @@ export default { ``` > Structural and theme-bound tags are emitted by the template itself rather than -> via `head`: `og:title` (mirrors the per-page title), `og:type`, and the font -> preconnects/stylesheet the bundled UI components rely on. +> via `head`, including `og:title` (which mirrors the per-page title) and +> `og:type`. The UI stylesheet bundles its fonts locally. -### Custom LightningCSS options +### Custom Vite options -The `lightningcss` object is spread directly into [LightningCSS][lightningcss] -while CSS is bundled, so any of its options — `visitor` (custom plugins), -`customAtRules`, `targets`, `drafts`, and so on — are supported. The generator -manages `filename`, `code`, `cssModules`, and `resolver`, so those are ignored. +The `vite` object is merged into both production builds. This supports Vite +plugins, aliases, `define`, asset options, and native Lightning CSS settings. +Use `import.meta.env.SSR` inside application code or plugin transforms when +behavior must differ between the SSR and browser builds. ```js // doc-kit.config.mjs -export default { - web: { - lightningcss: { - customAtRules: { - mixin: { prelude: '', body: 'style-block' }, - }, - visitor: { - Color(color) { - // e.g. transform every color through a design-token map - return color; - }, - }, - }, - }, -}; -``` - -To apply more than one visitor, compose them with LightningCSS's -`composeVisitors` helper and pass the result as `visitor`. - -[lightningcss]: https://lightningcss.dev/transforms.html - -### Custom Rolldown options - -The `rolldown` object is merged into the [Rolldown][rolldown] `build` call used -for **both** the client and server bundles, so you can register extra plugins, -inject compile-time constants, add module aliases, or set any other Rolldown -option. The same config is applied to both builds — branch inside a plugin (or -read the `SERVER`/`CLIENT` defines) if you need per-target behavior. - -The merge follows these rules: - -- **`plugins`** are registered after the built-in virtual-module plugin (so - they see the in-memory entry modules) and before the CSS loader. -- **`transform.define`** and **`resolve.alias`** are merged key-by-key, so you - can add entries without dropping the generator's. The built-in `SERVER`/ - `CLIENT` defines and the `react`/`react-dom` → `preact/compat` aliases - (plus anything from `imports`) always win. -- **All other options** (e.g. `output.minify`, `treeshake`, `external`, - `platform`, `logLevel`) override the generator's defaults. -- **`input`** and the built-in plugins are managed by the generator and cannot - be overridden. - -> Functions (plugins, hooks, alias resolvers, etc.) are allowed here because the -> `web` generator runs entirely on the main thread. Unlike the chunked -> generators, its config is never serialized to a worker, so values that can't -> be structured-cloned are safe. Keep this in mind if `web` ever gains parallel -> processing: function-valued options would then need a serializable form. - -```js -// doc-kit.config.mjs -import myRolldownPlugin from './my-rolldown-plugin.mjs'; +import myVitePlugin from './my-vite-plugin.mjs'; export default { web: { - rolldown: { - // Register additional plugins (run after the built-in ones). - plugins: [myRolldownPlugin()], - - // Inject extra compile-time constants (merged with SERVER/CLIENT). - transform: { - define: { - 'process.env.ANALYTICS_ID': JSON.stringify('UA-XXXXX'), - }, + vite: { + plugins: [myVitePlugin()], + define: { + 'process.env.ANALYTICS_ID': JSON.stringify('UA-XXXXX'), }, - - // Extend module resolution with custom aliases. resolve: { alias: { '@components': './src/components', }, }, + css: { + lightningcss: { + targets: { + chrome: 100 << 16, + }, + }, + }, }, }, }; ``` -[rolldown]: https://rolldown.rs/ +The generator owns the fields required to coordinate its builds: config-file +loading, app type and base, virtual inputs, Preact compatibility aliases and +automatic JSX runtime, the Lightning CSS transformer, output/write mode, SSR +format and temporary output, and SSR dependency bundling. Values supplied for +those fields are replaced after configuration is merged. User plugins are +registered after the generator's virtual-module plugin; other Vite options are +preserved. + +Vite manifests are optional. Set `web.vite.build.manifest` to `true` or a file +name when another tool needs one. The generated HTML already references the +correct hashed scripts, stylesheets, imported assets, and module preloads. + +Function-valued plugins and hooks are supported because the `web` generator +runs on the main thread and does not serialize this configuration to a worker. ### Default `imports` @@ -300,9 +264,9 @@ The HTML template file (set via `templatePath`) uses JavaScript template literal | ------------------ | -------- | ----------------------------------------------------------------- | | `title` | `string` | Fully resolved page title (e.g. `'File system \| Node.js v22.x'`) | | `dehydrated` | `string` | Server-rendered HTML for the page content | -| `importMap` | `string` | JSON import map for client-side module resolution | -| `entrypoint` | `string` | Client-side entry point filename with cache-bust query | +| `entrypoint` | `string` | Virtual Vite module for this page's client hydration | | `speculationRules` | `string` | Speculation rules JSON for prefetching | +| `themeScript` | `string` | Inline script that applies the saved theme before paint | | `root` | `string` | Relative or absolute path to the site root | | `metadata` | `object` | Full page metadata (frontmatter, path, heading, etc.) | | `config` | `object` | The resolved web generator configuration | @@ -312,8 +276,9 @@ Since the template supports arbitrary JS expressions, you can use conditionals a ```html ${title} - - + ``` + +Vite processes each populated template as an HTML entry. It replaces the +virtual `entrypoint` with the hashed client script and injects that page's +stylesheets and module preloads in their required order. diff --git a/src/generators/web/__tests__/generate.test.mjs b/src/generators/web/__tests__/generate.test.mjs index e5f70c76..2670f86a 100644 --- a/src/generators/web/__tests__/generate.test.mjs +++ b/src/generators/web/__tests__/generate.test.mjs @@ -1,4 +1,7 @@ import assert from 'node:assert/strict'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { describe, it } from 'node:test'; import { jsx, toJs } from 'estree-util-to-js'; @@ -44,33 +47,50 @@ const createEntry = (api, name) => { }; }; -describe('web generate', () => { - it('omits View As links for synthetic pages only', async () => { - await setConfig({}); +const createTestConfiguration = async context => { + const output = await mkdtemp(join(tmpdir(), 'doc-kit-web-test-')); + context.after(() => rm(output, { recursive: true, force: true })); + + const config = await setConfig({ + output, + version: 'v22.0.0', + changelog: [], + generators: { + web: {}, + }, + }); + + return { config, output }; +}; +describe('web generate', () => { + it('writes Vite HTML entries and omits View As links for synthetic pages', async context => { + const { output } = await createTestConfiguration(context); const fs = createEntry('fs', 'File system'); + fs.path = '/api/fs'; const notFoundPage = buildNotFoundPage(); const contents = await Promise.all([ buildContent([fs], fs), buildContent(notFoundPage.entries, notFoundPage.head), ]); - const input = contents.map(toCodeItem); - const [fsPage, notFoundResult] = await generate(input); + await generate(contents.map(toCodeItem)); - assert.match(fsPage.html, /View As/); - assert.match(fsPage.html, /href=fs\.json/); - assert.match(fsPage.html, /href=fs\.md/); + const [fsHTML, notFoundHTML] = await Promise.all([ + readFile(join(output, 'api/fs.html'), 'utf8'), + readFile(join(output, '404.html'), 'utf8'), + ]); - assert.doesNotMatch(notFoundResult.html, /View As/); - assert.doesNotMatch(notFoundResult.html, /href=404\.json/); - assert.doesNotMatch(notFoundResult.html, /href=404\.md/); + assert.match(fsHTML, /View As/); + assert.match(fsHTML, /href=fs\.json/); + assert.match(fsHTML, /href=fs\.md/); + assert.doesNotMatch(notFoundHTML, /View As/); + assert.match(fsHTML, /src=\.\.\/assets\//); + assert.match(notFoundHTML, /src=\.\/assets\//); }); - it('renders the configurable head without hardcoded defaults', async () => { - // `setConfig` resolves generator defaults; mutate the live config to apply - // per-generator overrides (the same object `getConfig('web')` returns). - const config = await setConfig({}); + it('renders the configurable head without hardcoded defaults', async context => { + const { config, output } = await createTestConfiguration(context); config.web.head = { meta: [ { name: 'description', content: 'Custom project docs' }, @@ -81,18 +101,54 @@ describe('web generate', () => { }; const fs = createEntry('fs', 'File system'); - const [fsPage] = await generate([toCodeItem(await buildContent([fs], fs))]); + await generate([toCodeItem(await buildContent([fs], fs))]); + const html = await readFile(join(output, 'fs.html'), 'utf8'); + + assert.match(html, /Custom project docs/); + assert.match(html, /https:\/\/example\.com\/og\.png/); + assert.match(html, /href=https:\/\/example\.com\/favicon\.ico/); + assert.match(html, /content=#abcdef/); + assert.doesNotMatch(html, /nodejs\.org/); + assert.match(html, /property=og:type content=website/); + }); - assert.match(fsPage.html, /Custom project docs/); - assert.match(fsPage.html, /https:\/\/example\.com\/og\.png/); - assert.match(fsPage.html, /href=https:\/\/example\.com\/favicon\.ico/); - assert.match(fsPage.html, /content=#abcdef/); + it('uses Vite base URLs for absolute client assets', async context => { + const { config, output } = await createTestConfiguration(context); + config.web.useAbsoluteURLs = true; + config.web.baseURL = 'https://example.com/docs'; - // Project-branding `head` config no longer leaks Node.js defaults. - assert.doesNotMatch(fsPage.html, /nodejs\.org/); + const notFoundPage = buildNotFoundPage(); + const content = await buildContent(notFoundPage.entries, notFoundPage.head); + await generate([toCodeItem(content)]); + const html = await readFile(join(output, '404.html'), 'utf8'); + + assert.match(html, /src=https:\/\/example\.com\/docs\/assets\//); + assert.match(html, /href=https:\/\/example\.com\/docs\/assets\//); + }); + + it('applies configured Vite plugins', async context => { + const { config, output } = await createTestConfiguration(context); + config.web.vite = { + plugins: [ + { + name: 'test-html-transform', + transformIndexHtml() { + return [ + { + tag: 'meta', + attrs: { name: 'vite-plugin', content: 'enabled' }, + injectTo: 'head', + }, + ]; + }, + }, + ], + }; + + const fs = createEntry('fs', 'File system'); + await generate([toCodeItem(await buildContent([fs], fs))]); + const html = await readFile(join(output, 'fs.html'), 'utf8'); - // Structural/theme tags stay hardcoded in the template regardless. - assert.match(fsPage.html, /property=og:type content=website/); - assert.match(fsPage.html, /href=https:\/\/fonts\.googleapis\.com/); + assert.match(html, /name=vite-plugin/); }); }); diff --git a/src/generators/web/generate.mjs b/src/generators/web/generate.mjs index d3526900..5fba3a80 100644 --- a/src/generators/web/generate.mjs +++ b/src/generators/web/generate.mjs @@ -1,12 +1,10 @@ 'use strict'; import { readFile } from 'node:fs/promises'; -import { join } from 'node:path'; import { copyStaticAssets } from './utils/copying.mjs'; import { createCodeConverter, processBundles } from './utils/processing.mjs'; import getConfig from '../../utils/configuration/index.mjs'; -import { writeFile } from '../../utils/file.mjs'; /** * Main generation function that bundles per-page JSX code into web output. @@ -40,7 +38,7 @@ export async function generate(input) { .filter(data => data.synthetic !== true) .map(data => ({ data })); - const { results, css, chunks } = await processBundles({ + await processBundles({ serverCodeMap: converter.serverCodeMap, clientCodeMap: converter.clientCodeMap, datas, @@ -48,19 +46,5 @@ export async function generate(input) { template, }); - if (config.output) { - for (const { html, path } of results) { - await writeFile(join(config.output, `${path}.html`), html, 'utf-8'); - } - - for (const chunk of chunks) { - await writeFile(join(config.output, chunk.fileName), chunk.code, 'utf-8'); - } - - await writeFile(join(config.output, 'styles.css'), css, 'utf-8'); - - await copyStaticAssets(config); - } - - return results.map(({ html }) => ({ html: html.toString(), css })); + await copyStaticAssets(config); } diff --git a/src/generators/web/index.mjs b/src/generators/web/index.mjs index 97d6f3c8..e9ee4bab 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -13,6 +13,9 @@ import { createLazyGenerator } from '../../utils/generators.mjs'; * - Client-side JavaScript with code splitting * - Bundled CSS styles * + * Vite writes the complete static site to the configured output directory; + * this terminal generator does not return an in-memory copy. + * * `jsx-ast` serializes each page's JSX AST to a `code` string inside its worker, * so this generator only ever handles small `{ data, code }` items — the heavy * ASTs (notably the giant `all` page) never reach the main thread. Bundling and @@ -48,8 +51,8 @@ export default createLazyGenerator({ // Project-specific document `` contents. `meta` and `links` are // arrays of attribute bags (boolean `true` renders a valueless attribute, // e.g. `crossorigin`); `html` holds arbitrary raw markup as an escape - // hatch. Structural/theme tags (`og:type`, font preconnects/stylesheets) - // are hardcoded in the template instead. + // hatch. Structural/theme tags such as `og:type` are hardcoded in the + // template instead. head: { meta: [ { @@ -81,11 +84,6 @@ export default createLazyGenerator({ html: [], }, - // Options spread directly into LightningCSS when bundling CSS, e.g. - // `visitor`, `customAtRules`, `targets`, or `drafts`. See - // https://lightningcss.dev/transforms.html for the full set. - lightningcss: {}, - imports: { '#theme/Logo': '@node-core/ui-components/Common/NodejsLogo', '#theme/Navigation': join(import.meta.dirname, './ui/components/NavBar'), @@ -100,8 +98,8 @@ export default createLazyGenerator({ // see the web generator README for the shape and shorthand. components: {}, - // Options merged into the Rolldown build (client and server), e.g. extra - // `plugins`. See the README for the merge semantics. - rolldown: {}, + // Vite options merged into both client and SSR builds. The generator owns + // the fields that connect its virtual entries and output. + vite: {}, }), }); diff --git a/src/generators/web/template.html b/src/generators/web/template.html index 458543b2..e3792678 100644 --- a/src/generators/web/template.html +++ b/src/generators/web/template.html @@ -5,25 +5,18 @@ ${title} - ${head} - - - - -
${dehydrated}
- + diff --git a/src/generators/web/types.d.ts b/src/generators/web/types.d.ts index 68588e60..1b877179 100644 --- a/src/generators/web/types.d.ts +++ b/src/generators/web/types.d.ts @@ -1,5 +1,4 @@ -import type { BundleAsyncOptions, CustomAtRules } from 'lightningcss-wasm'; -import type { BuildOptions } from 'rolldown'; +import type { UserConfig } from 'vite'; import type { JSXContent } from '../jsx-ast/utils/buildContent.mjs'; @@ -32,14 +31,6 @@ export type Configuration = { title: string; useAbsoluteURLs: boolean; head: HeadConfig; - // Options spread into LightningCSS while bundling CSS. `filename`, `code`, - // `cssModules`, and `resolver` are managed by the generator and ignored here. - lightningcss: Partial< - Omit< - BundleAsyncOptions, - 'filename' | 'code' | 'cssModules' | 'resolver' - > - >; imports: Record; virtualImports: Record; // Maps a JSX tag name to its import, enabling JSX-in-MDX. The string shorthand @@ -47,12 +38,12 @@ export type Configuration = { // `JSX_IMPORTS`. Pair each entry with a matching `imports` alias to resolve the // `source` to a real module path. components: Record; - // Options merged into the Rolldown build for the client and server bundles. - // See the web generator README for the merge semantics. - rolldown: Partial; + // Vite options merged into the client and SSR builds. The generator owns the + // fields that connect its virtual entries and output. + vite: UserConfig; }; export type Generator = GeneratorMetadata< Configuration, - Generate, Promise>> + Generate, Promise> >; diff --git a/src/generators/web/ui/components/Layout/index.jsx b/src/generators/web/ui/components/Layout/index.jsx index f7e9ce7f..03a15d32 100644 --- a/src/generators/web/ui/components/Layout/index.jsx +++ b/src/generators/web/ui/components/Layout/index.jsx @@ -19,7 +19,7 @@ import SideBar from '#theme/Sidebar'; */ export default ({ metadata, headings, readingTime, children }) => ( <> - {CLIENT && } + {!import.meta.env.SSR && }
diff --git a/src/generators/web/ui/hooks/useTheme.mjs b/src/generators/web/ui/hooks/useTheme.mjs index 207f2b7d..95f610cc 100644 --- a/src/generators/web/ui/hooks/useTheme.mjs +++ b/src/generators/web/ui/hooks/useTheme.mjs @@ -29,7 +29,7 @@ const applySystemTheme = () => applyTheme('system'); export const useTheme = () => { // Read stored preference once on mount; default to 'system'. const [pref, setPref] = useState(() => - SERVER ? 'system' : (localStorage.getItem('theme') ?? 'system') + import.meta.env.SSR ? 'system' : (localStorage.getItem('theme') ?? 'system') ); // Apply theme on every preference change, and if 'system', @@ -49,7 +49,7 @@ export const useTheme = () => { /** Updates the preference in both React state and localStorage. */ const setTheme = useCallback(next => { setPref(next); - if (CLIENT) { + if (!import.meta.env.SSR) { localStorage.setItem('theme', next); } }, []); diff --git a/src/generators/web/ui/index.css b/src/generators/web/ui/index.css index fb436e2e..1b375458 100644 --- a/src/generators/web/ui/index.css +++ b/src/generators/web/ui/index.css @@ -1,9 +1,12 @@ +@import '@fontsource-variable/open-sans/wght.css'; +@import '@fontsource-variable/open-sans/wght-italic.css'; +@import '@fontsource/ibm-plex-mono/400.css'; @import '@node-core/ui-components/styles/index.css'; @import '@node-core/rehype-shiki/index.css'; /* Fonts */ :root { - --font-open-sans: 'Open Sans', sans-serif; + --font-open-sans: 'Open Sans Variable', sans-serif; --font-ibm-plex-mono: 'IBM Plex Mono', monospace; } diff --git a/src/generators/web/ui/types.d.ts b/src/generators/web/ui/types.d.ts index b1eb7d25..f30f2d69 100644 --- a/src/generators/web/ui/types.d.ts +++ b/src/generators/web/ui/types.d.ts @@ -1,13 +1,10 @@ +/// + import { GlobalConfiguration } from '../../../utils/configuration/types'; import { MetadataEntry } from '../../metadata/types'; import { Configuration } from '../types'; import { SemVer } from 'semver'; -declare global { - const SERVER: boolean; - const CLIENT: boolean; -} - declare module '#theme/config' { // From global configuration export const repository: GlobalConfiguration['repository']; diff --git a/src/generators/web/utils/__tests__/vite.test.mjs b/src/generators/web/utils/__tests__/vite.test.mjs new file mode 100644 index 00000000..7b49266b --- /dev/null +++ b/src/generators/web/utils/__tests__/vite.test.mjs @@ -0,0 +1,131 @@ +import assert from 'node:assert/strict'; +import { access, mkdtemp } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { describe, it } from 'node:test'; + +import { + default as getConfig, + setConfig, +} from '../../../../utils/configuration/index.mjs'; +import { + createVirtualModulesPlugin, + createViteConfig, + renderServerEntries, +} from '../vite.mjs'; + +const output = join(tmpdir(), 'doc-kit-vite-test-output'); + +await setConfig({ + output, + version: 'v22.0.0', + changelog: [], + generators: { + web: {}, + }, +}); + +describe('Vite virtual modules', () => { + it('resolves and loads only exact in-memory module identifiers', () => { + const htmlId = resolve('api/fs.html'); + const plugin = createVirtualModulesPlugin( + new Map([ + ['virtual:entry', 'export default 42;'], + [htmlId, ''], + ]) + ); + + const entryId = plugin.resolveId('virtual:entry'); + assert.ok(entryId); + assert.strictEqual(plugin.load(entryId), 'export default 42;'); + assert.strictEqual(plugin.resolveId('virtual:missing'), undefined); + assert.strictEqual(plugin.resolveId(htmlId), htmlId); + assert.strictEqual( + plugin.load(htmlId), + '' + ); + }); +}); + +describe('Vite configuration', () => { + it('uses the generated client entries and configured output', () => { + getConfig('web').vite = { + base: '/custom/', + build: { + outDir: 'custom-output', + manifest: true, + rolldownOptions: { + input: 'custom-entry.js', + }, + }, + }; + + const input = { fs: 'virtual:doc-kit/client/fs.jsx' }; + const config = createViteConfig({ + sources: new Map(), + input, + server: false, + }); + + assert.strictEqual(config.base, './'); + assert.strictEqual(config.build.outDir, output); + assert.strictEqual(config.build.manifest, true); + assert.strictEqual(config.build.rolldownOptions.input, input); + }); + + it('keeps the temporary SSR build self-contained', () => { + getConfig('web').vite = { + ssr: { + external: ['preact'], + noExternal: false, + }, + build: { + minify: true, + rolldownOptions: { + external: ['preact'], + }, + }, + }; + + const input = { fs: 'virtual:doc-kit/server/fs.jsx' }; + const serverOutput = join(tmpdir(), 'doc-kit-vite-ssr-test'); + const config = createViteConfig({ + sources: new Map(), + input, + server: true, + serverOutDir: serverOutput, + }); + + assert.strictEqual(config.build.ssr, true); + assert.strictEqual(config.build.outDir, serverOutput); + assert.strictEqual(config.build.minify, false); + assert.deepStrictEqual(config.build.rolldownOptions.external, []); + assert.deepStrictEqual(config.ssr.external, []); + assert.strictEqual(config.ssr.noExternal, true); + }); +}); + +describe('Vite SSR temporary output', () => { + it('always removes temporary output after a renderer throws', async () => { + getConfig('web').vite = {}; + const temporaryDirectory = await mkdtemp( + join(tmpdir(), 'doc-kit-vite-cleanup-test-') + ); + + await assert.rejects( + renderServerEntries( + new Map([ + [ + 'broken.jsx', + 'export default () => { throw new Error("render failed"); };', + ], + ]), + {}, + async () => temporaryDirectory + ), + /render failed/ + ); + + await assert.rejects(access(temporaryDirectory), { code: 'ENOENT' }); + }); +}); diff --git a/src/generators/web/utils/bundle.mjs b/src/generators/web/utils/bundle.mjs deleted file mode 100644 index 482d176d..00000000 --- a/src/generators/web/utils/bundle.mjs +++ /dev/null @@ -1,175 +0,0 @@ -import { resolve } from 'node:path'; - -import virtual from '@rollup/plugin-virtual'; -import { build } from 'rolldown'; - -import cssLoader from './css.mjs'; -import getConfig from '../../../utils/configuration/index.mjs'; -import { lazy } from '../../../utils/misc.mjs'; - -// Resolve node_modules relative to this package (doc-kit), not cwd. -// We do this by finding where one of our dependencies (preact) is stored, -// and using it's NODE_MODULES -// -// FIXME(@avivkeller): When our CI (in Node.js Core) supports v22.x, -// this lazy-loading solution can be replaced by a simple import. For that -// matter, glob can also be replaced in other files throughout this repo. -const getNodeModules = lazy(async () => { - const { findPackageJSON } = await import('node:module'); - - return resolve( - findPackageJSON(new URL(import.meta.resolve('preact'))), - '../..' - ); -}); - -/** - * Asynchronously bundles JavaScript source code (and its CSS imports), - * targeting either browser (client) or server (Node.js) environments. - * - * @param {Map} codeMap - Map of {fileName: code} for all builds. - * @param {Object} [options] - Build configuration object. - * @param {Object} [virtualImports] - Virtual imports - * @param {boolean} [options.server=false] - Whether this is a server-side build. - */ -export default async function bundleCode( - codeMap, - virtualImports = {}, - { server = false } = {} -) { - const config = getConfig('web'); - - const { rolldown = {} } = config; - - const result = await build({ - ...rolldown, - - // Entry points: array of virtual module names that the virtual plugin provides - input: Array.from(codeMap.keys()), - - // Experimental features: import maps for client, none for server - experimental: { - chunkImportMap: !server, - ...rolldown.experimental, - }, - - checks: { - // Disable plugin timing logs for cleaner output. This can be re-enabled for debugging performance issues. - pluginTimings: false, - ...rolldown.checks, - }, - - // Output configuration - output: { - // Output module format: - // - "cjs" for CommonJS (used in Node.js environments) - // - "esm" for browser environments (Using Chunk Code-Splitting) - format: server ? 'cjs' : 'esm', - - // Minify output only for browser builds to optimize file size. - // Server builds are usually not minified to preserve stack traces and debuggability. - minify: !server, - - ...rolldown.output, - }, - - // Platform informs Rolldown of the environment-specific code behavior: - // - 'node' enables things like `require`, and skips polyfills. - // - 'browser' enables inlining of polyfills and uses native browser features. - platform: rolldown.platform ?? (server ? 'node' : 'browser'), - - // External dependencies to exclude from bundling. - // These are expected to be available at runtime in the server environment. - // This reduces bundle size and avoids bundling shared server libs. - external: - rolldown.external ?? - (server - ? ['preact', 'preact-render-to-string', '@node-core/ui-components'] - : []), - - transform: { - ...rolldown.transform, - - // Inject global compile-time constants that will be replaced in code. - // These are useful for tree-shaking and conditional branching. - // Be sure to update type declarations (`types.d.ts`) if these change. - define: { - ...rolldown.transform?.define, - - // Boolean flags used for conditional logic in source code: - // Example: `if (SERVER) {...}` or `if (CLIENT) {...}` - // These flags help split logic for server/client environments. - // Unused branches will be removed via tree-shaking. - SERVER: String(server), - CLIENT: String(!server), - }, - - // JSX transformation configuration. - // `'react-jsx'` enables the automatic JSX runtime, which doesn't require `import React`. - // Since we're using Preact via aliasing, this setting works well with `preact/compat`. - jsx: rolldown.transform?.jsx ?? 'react-jsx', - }, - - // Module resolution configuration. - resolve: { - // exports condition to use - conditionNames: ['rolldown'], - - // Tell the bundler where to find node_modules. - // We use our custom `NODE_MODULES`, and then the cwd's `node_modules`. - modules: [await getNodeModules(), 'node_modules'], - - ...rolldown.resolve, - - // Alias react imports to preact/compat for smaller bundle sizes. - // Explicit jsx-runtime aliases are required for the automatic JSX transform. - alias: { - react: 'preact/compat', - 'react-dom': 'preact/compat', - ...rolldown.resolve?.alias, - ...config.imports, - }, - }, - - // Array of plugins to apply during the build. - plugins: [ - // Virtual plugin: provides in-memory modules from codeMap, - // plus the #theme/config virtual module - virtual({ - ...Object.fromEntries(codeMap), - ...virtualImports, - }), - - ...(rolldown.plugins ?? []), - - // Load CSS imports via the custom plugin. - // This plugin will collect imported CSS files and return them as `source` chunks. - cssLoader(), - ], - - // Enable tree-shaking to remove unused code - treeshake: rolldown.treeshake ?? true, - - // Return chunks in memory instead of writing to disk - write: false, - }); - - // Separate CSS assets from JavaScript chunks - const assets = result.output.filter(c => c.type === 'asset'); - const chunks = result.output.filter(c => c.type === 'chunk'); - - const importMap = assets.find(c => c.fileName === 'importmap.json'); - - return { - css: assets - .filter(c => c.fileName.endsWith('.css')) - .map(f => f.source) - .join(''), - chunks: chunks.map(({ fileName, code, isEntry }) => ({ - fileName: fileName.replace('_virtual_', ''), - isEntry, - code, - })), - importMap: importMap?.source.toString(), - }; -} diff --git a/src/generators/web/utils/chunks.mjs b/src/generators/web/utils/chunks.mjs deleted file mode 100644 index 143dfa8c..00000000 --- a/src/generators/web/utils/chunks.mjs +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Creates an enhanced require function that can resolve code-split chunks - * from a virtual file system before falling back to Node.js require. - * - * @param {Array<{fileName: string, code: string}>} chunks - Array of code-split chunks from bundler. - * @param {ReturnType} requireFn - Node.js require function for external packages. - */ -export function createChunkedRequire(chunks, requireFn) { - // Create a virtual file system from code-split chunks - const chunkModules = Object.fromEntries( - chunks.map(c => [`./${c.fileName}`, c.code]) - ); - - /** - * Enhanced require function that resolves code-split chunks from virtual file system. - * - * @param {string} modulePath - Module path to require. - * @returns {*} Module exports. - */ - const chunkedRequire = modulePath => { - // Check virtual file system first for code-split chunks - if (chunkModules[modulePath]) { - const mod = { exports: {} }; - - // Execute chunk code in isolated context with its own module.exports - const chunkFn = new Function( - 'module', - 'exports', - 'require', - chunkModules[modulePath] - ); - - chunkFn(mod, mod.exports, chunkedRequire); - - return mod.exports; - } - - // Fall back to Node.js require for external packages - return requireFn(modulePath); - }; - - return chunkedRequire; -} diff --git a/src/generators/web/utils/config.mjs b/src/generators/web/utils/config.mjs index 8262a660..74aaf42e 100644 --- a/src/generators/web/utils/config.mjs +++ b/src/generators/web/utils/config.mjs @@ -84,16 +84,16 @@ export default function createConfigSource(input) { const exports = { ...omitKeys( config, - // These are keys that are large, build-time only (e.g. the document head - // and CSS options), or not serializable (e.g. `lightningcss` visitor - // functions), so they are never exposed to client components. + // These are large or build-time-only keys, or may contain functions, so + // they are never exposed to client components. [ 'changelog', 'index', 'imports', 'virtualImports', + 'components', 'head', - 'lightningcss', + 'vite', ] ), version: configVersion, diff --git a/src/generators/web/utils/css.mjs b/src/generators/web/utils/css.mjs deleted file mode 100644 index ad57a04c..00000000 --- a/src/generators/web/utils/css.mjs +++ /dev/null @@ -1,122 +0,0 @@ -import { readFile } from 'node:fs/promises'; - -import { bundleAsync } from 'lightningcss-wasm'; - -import getConfig from '../../../utils/configuration/index.mjs'; - -// Since we use rolldown to bundle multiple times, -// we re-use a lot of CSS files, so there is no -// need to re-transpile. -const fileCache = new Map(); - -/** - * Rolldown plugin to support `.css` files with CSS Modules semantics. - * - * This plugin performs the following: - * 1. Intercepts `.css` files during the build - * 2. Processes them with Lightning CSS (including CSS Module transformation) - * 3. Collects the resulting CSS to emit as a single `styles.css` file - * 4. Exports the transformed class names back to the JS file - * - * @returns {import('rolldown').Plugin} - */ -export default () => { - const cssChunks = new Set(); - - // User-supplied LightningCSS options (e.g. `visitor`, `customAtRules`), - // spread into every bundle call below. - const { lightningcss = {} } = getConfig('web'); - - return { - name: 'css-loader', // Required plugin name for debugging - - // Hook into the module loading phase of Rolldown - load: { - // Match only files ending with `.css` - filter: { - id: { - include: /\.css$/, - }, - }, - - /** - * Load handler to process matched `.css` files - * - * @param {string} id - Absolute file path to the CSS file - */ - async handler(id) { - // Return from cache if already processed - if (fileCache.has(id)) { - const cached = fileCache.get(id); - - // Collect the CSS as normal - cssChunks.add(cached.code); - - return { - code: `export default ${JSON.stringify(cached.exports)};`, - moduleType: 'js', - }; - } - - // Read the raw CSS file from disk - const source = await (lightningcss.resolver?.read ?? readFile)( - id, - 'utf8' - ); - - // Use Lightning CSS to compile the file with CSS Modules enabled - const { code, exports } = await bundleAsync({ - ...lightningcss, - filename: id, - code: Buffer.from(source), - cssModules: id.endsWith('module.css'), - resolver: { - ...lightningcss.resolver, - /** - * Resolve specifiers with Rolldown's resolution - */ - resolve: (...args) => this.resolve(...args).then(r => r.id), - }, - }); - - const css = code.toString(); - - // Add the compiled CSS to our in-memory collection - cssChunks.add(css); - - // Map exported class names to their scoped identifiers - // e.g., { button: '_button_abc123' } - const mappedExports = Object.fromEntries( - Object.entries(exports ?? {}).map(([key, value]) => [key, value.name]) - ); - - // Cache result - fileCache.set(id, { code: css, exports: mappedExports }); - - // Return a JS module that exports the scoped class names - return { - code: `export default ${JSON.stringify(mappedExports)};`, - moduleType: 'js', - }; - }, - }, - - /** - * buildEnd hook runs once all modules have been processed. - * We use this opportunity to emit the final bundled CSS file. - */ - buildEnd() { - // If no CSS chunks were processed, skip emitting - if (cssChunks.size === 0) { - return; - } - - // Concatenate all collected CSS strings and emit as a build asset - this.emitFile({ - type: 'asset', - name: 'styles.css', - source: Array.from(cssChunks).join(''), - }); - }, - }; -}; diff --git a/src/generators/web/utils/generate.mjs b/src/generators/web/utils/generate.mjs index 0a735d1f..89c09192 100644 --- a/src/generators/web/utils/generate.mjs +++ b/src/generators/web/utils/generate.mjs @@ -105,8 +105,8 @@ export default () => { false ), - // Render component to HTML string and return it - `return renderToStringAsync(${componentCode});`, + // Export a renderer that the temporary Vite SSR build can import. + `export default () => renderToStringAsync(${componentCode});`, ].join('\n'); }; diff --git a/src/generators/web/utils/processing.mjs b/src/generators/web/utils/processing.mjs index c4f23af7..3e0fe830 100644 --- a/src/generators/web/utils/processing.mjs +++ b/src/generators/web/utils/processing.mjs @@ -1,16 +1,13 @@ -import { randomUUID } from 'node:crypto'; -import { createRequire } from 'node:module'; - -import { transform } from 'lightningcss-wasm'; - -import bundleCode from './bundle.mjs'; -import { createChunkedRequire } from './chunks.mjs'; import createConfigSource from './config.mjs'; import createASTBuilder from './generate.mjs'; import { relativeOrAbsolute } from './relativeOrAbsolute.mjs'; +import { + buildClientPages, + getClientEntryId, + renderServerEntries, +} from './vite.mjs'; import getConfig from '../../../utils/configuration/index.mjs'; import { populate } from '../../../utils/configuration/templates.mjs'; -import { minifyHTML } from '../../../utils/html-minifier.mjs'; import { SPECULATION_RULES } from '../constants.mjs'; import { THEME_SCRIPT } from '../ui/theme-script.mjs'; @@ -114,40 +111,8 @@ export function createCodeConverter() { } /** - * Bundles and executes server-side code, returning dehydrated HTML pages. - * - * @param {Map} serverCodeMap - Map of fileName to server-side JavaScript code. - * @param {ReturnType} requireFn - Node.js require function for external packages. - * @param {Object} virtualImports - virtual imports to pass to Rolldown - * @returns {{ pages: Map, css: string }} - */ -async function executeServerCode(serverCodeMap, requireFn, virtualImports) { - // Bundle all server-side code, which may produce code-split chunks - const { chunks, css } = await bundleCode(serverCodeMap, virtualImports, { - server: true, - }); - - const entryChunks = chunks.filter(c => c.isEntry); - const otherChunks = chunks.filter(c => !c.isEntry); - - // Create enhanced require function that can resolve code-split chunks - const enhancedRequire = createChunkedRequire(otherChunks, requireFn); - - const pages = new Map(); - - // Execute each bundled entry and collect dehydrated HTML results - for (const chunk of entryChunks) { - const executedFunction = new Function('require', chunk.code); - const dehydratedHtml = await executedFunction(enhancedRequire); - pages.set(chunk.fileName, dehydratedHtml); - } - - return { pages, css }; -} - -/** - * Bundles pre-converted JSX code into complete HTML pages, client JS bundles, - * and CSS. Conversion (JSX AST → code) happens upstream via + * Bundles pre-converted JSX code into complete HTML pages and client assets. + * Conversion (JSX AST → code) happens upstream via * {@link createCodeConverter} so the heavy ASTs are already discarded; this * step needs every entry together for code-splitting and the shared sidebar. * @@ -166,18 +131,14 @@ export async function processBundles({ template, }) { const config = getConfig('web'); - const requireFn = createRequire(import.meta.url); const virtualImports = { '#theme/config': createConfigSource(sidebarEntries), ...config.virtualImports, }; - // Bundle server and client code in parallel. Both need all entries for - // code-splitting, but are independent of each other. - const [serverBundle, clientBundle] = await Promise.all([ - executeServerCode(serverCodeMap, requireFn, virtualImports), - bundleCode(clientCodeMap, virtualImports), - ]); + // The SSR build is written and executed as one complete temporary output + // because its entries can share chunks. It is removed before this returns. + const serverPages = await renderServerEntries(serverCodeMap, virtualImports); const titleSuffix = populate(config.title, { ...config, @@ -189,38 +150,35 @@ export async function processBundles({ // template authors avoid nested template-literal escaping. const head = buildHead(config.head); - // Render final HTML pages - const results = await Promise.all( - datas.map(async data => { + // Render the templates with virtual module entrypoints. Vite then consumes + // these pages as HTML entries and owns scripts, stylesheets, preloads, and + // imported assets. + const pages = new Map( + datas.map(data => { const root = resolvePageRoot(data); const title = data.title ?? data.heading.data.name; - - // Replace template placeholders with actual content - const renderedHtml = populateWithEvaluation(template, { - title: title - ? titleSuffix - ? `${title} | ${titleSuffix}` - : title - : titleSuffix, - dehydrated: serverBundle.pages.get(`${data.api}.js`) ?? '', - importMap: clientBundle.importMap?.replaceAll('/', root) ?? '', - entrypoint: `${data.api}.js?${randomUUID()}`, - speculationRules: SPECULATION_RULES, - themeScript: THEME_SCRIPT, - root, - metadata: data, - config, - head, - }); - - return { html: await minifyHTML(renderedHtml), path: data.path }; + const fileName = `${data.path.replace(/^\/+/, '')}.html`; + + return [ + fileName, + populateWithEvaluation(template, { + title: title + ? titleSuffix + ? `${title} | ${titleSuffix}` + : title + : titleSuffix, + dehydrated: serverPages.get(data.api) ?? '', + entrypoint: getClientEntryId(data.api), + speculationRules: SPECULATION_RULES, + themeScript: THEME_SCRIPT, + root, + metadata: data, + config, + head, + }), + ]; }) ); - const { code: minifiedCSS } = transform({ - code: Buffer.from(`${serverBundle.css}\n${clientBundle.css}`), - minify: true, - }); - - return { results, chunks: clientBundle.chunks, css: minifiedCSS }; + await buildClientPages(clientCodeMap, virtualImports, pages); } diff --git a/src/generators/web/utils/vite.mjs b/src/generators/web/utils/vite.mjs new file mode 100644 index 00000000..677badc3 --- /dev/null +++ b/src/generators/web/utils/vite.mjs @@ -0,0 +1,346 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { basename, isAbsolute, join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { + build, + defaultClientConditions, + defaultServerConditions, + mergeConfig, +} from 'vite'; + +import getConfig from '../../../utils/configuration/index.mjs'; +import { minifyHTML } from '../../../utils/html-minifier.mjs'; + +const VIRTUAL_PREFIX = 'virtual:doc-kit/'; +const RESOLVED_VIRTUAL_PREFIX = '\0doc-kit:'; + +/** + * Resolves relative theme aliases against Vite's configured project root. + * + * @param {Record} aliases + * @param {string} root + * @returns {Record} + */ +const resolveThemeAliases = (aliases, root) => + Object.fromEntries( + Object.entries(aliases).map(([find, replacement]) => [ + find, + replacement.startsWith('.') ? resolve(root, replacement) : replacement, + ]) + ); + +/** + * Creates a Vite plugin that serves an exact map of in-memory modules and HTML + * entries. HTML keeps its absolute identifier so Vite emits it at the matching + * path relative to the configured root. + * + * @param {Map} sources + * @returns {import('vite').Plugin} + */ +export const createVirtualModulesPlugin = sources => { + return { + name: 'doc-kit:virtual-modules', + enforce: 'pre', + /** + * Resolves an exact in-memory identifier. + * + * @param {string} id + * @returns {string|undefined} + */ + resolveId(id) { + if (sources.has(id)) { + return isAbsolute(id) && id.endsWith('.html') + ? id + : `${RESOLVED_VIRTUAL_PREFIX}${id}`; + } + }, + /** + * Loads an exact resolved identifier. + * + * @param {string} id + * @returns {string|undefined} + */ + load(id) { + return sources.get( + id.startsWith(RESOLVED_VIRTUAL_PREFIX) + ? id.slice(RESOLVED_VIRTUAL_PREFIX.length) + : id + ); + }, + }; +}; + +/** + * Returns the virtual client entry imported by one HTML page. + * + * @param {string} api + * @returns {string} + */ +export const getClientEntryId = api => `${VIRTUAL_PREFIX}client/${api}.jsx`; + +/** + * Finalizes Vite's generated HTML before its normal write phase. + * + * @returns {import('vite').Plugin} + */ +const createHTMLFinalizerPlugin = () => ({ + name: 'doc-kit:finalize-html', + /** + * Minifies every generated HTML entry after Vite has injected its scripts, + * stylesheets, and module preloads. + */ + generateBundle: { + order: 'post', + /** + * @param {object} _ + * @param {Record} bundle + */ + async handler(_, bundle) { + await Promise.all( + Object.values(bundle) + .filter( + item => item.type === 'asset' && item.fileName.endsWith('.html') + ) + .map(async asset => { + const source = + typeof asset.source === 'string' + ? asset.source + : Buffer.from(asset.source).toString('utf8'); + + asset.source = await minifyHTML(source); + }) + ); + }, + }, +}); + +/** + * Converts generated page programs into named Vite inputs and virtual modules. + * + * @param {Map} codeMap + * @param {'client'|'server'} environment + */ +const createBuildEntries = (codeMap, environment) => { + const input = {}; + const sources = new Map(); + + for (const [fileName, code] of codeMap) { + const name = basename(fileName, '.jsx'); + const id = + environment === 'client' + ? getClientEntryId(name) + : `${VIRTUAL_PREFIX}server/${fileName}`; + + input[name] = id; + sources.set(id, code); + } + + return { input, sources }; +}; + +/** + * Produces the complete inline Vite config for one generator build. + * User configuration is merged first; generator invariants are then applied. + * + * @param {object} options + * @param {Map} options.sources + * @param {Record|Array} options.input + * @param {boolean} options.server + * @param {string} [options.serverOutDir] + * @returns {import('vite').InlineConfig} + */ +export const createViteConfig = ({ sources, input, server, serverOutDir }) => { + const webConfig = getConfig('web'); + const vite = webConfig.vite ?? {}; + const root = resolve(vite.root ?? process.cwd()); + const conditions = server ? defaultServerConditions : defaultClientConditions; + + return { + ...vite, + + // The generator is the complete Vite configuration boundary. + configFile: false, + root, + base: webConfig.useAbsoluteURLs + ? String(webConfig.baseURL).replace(/\/?$/, '/') + : './', + appType: 'custom', + publicDir: false, + clearScreen: vite.clearScreen ?? false, + logLevel: vite.logLevel ?? 'warn', + + // Virtual entries must resolve before user plugins, while user plugins can + // still transform every module and generated HTML page. + plugins: [ + createVirtualModulesPlugin(sources), + ...(vite.plugins ?? []), + ...(!server && webConfig.minify ? [createHTMLFinalizerPlugin()] : []), + ], + + resolve: mergeConfig( + { resolve: vite.resolve }, + { + resolve: { + conditions: ['rolldown', ...conditions], + dedupe: ['preact'], + + // Vite applies string aliases to matching package subpaths too. + alias: { + react: 'preact/compat', + 'react-dom': 'preact/compat', + ...resolveThemeAliases(webConfig.imports, root), + }, + }, + } + ).resolve, + + // Oxc supplies Preact's automatic JSX runtime in both builds. + oxc: { + ...vite.oxc, + jsx: { + ...vite.oxc?.jsx, + runtime: 'automatic', + importSource: 'preact', + }, + }, + + // CSS imports, modules, URLs, splitting, and minification are all Vite + // responsibilities. Native Lightning CSS options remain configurable. + css: { + ...vite.css, + transformer: 'lightningcss', + }, + + build: { + ...vite.build, + + // Both builds are complete Vite outputs. SSR uses a private directory + // because its entries can share chunks; the client writes the final site. + outDir: server ? serverOutDir : resolve(webConfig.output), + write: true, + emptyOutDir: false, + copyPublicDir: false, + watch: null, + lib: false, + + ...(server ? { manifest: false } : {}), + ssr: server, + + // Browser output follows the generator's minification setting. Temporary + // server output stays readable and disappears immediately after render. + minify: server ? false : (vite.build?.minify ?? webConfig.minify), + + rolldownOptions: { + ...vite.build?.rolldownOptions, + input, + ...(server ? { external: [] } : {}), + output: { + ...vite.build?.rolldownOptions?.output, + format: 'es', + ...(server + ? { + entryFileNames: '[name].mjs', + chunkFileNames: 'assets/[name]-[hash].mjs', + } + : {}), + }, + }, + }, + + ...(server + ? { + ssr: { + ...vite.ssr, + external: [], + noExternal: true, + resolve: { + ...vite.ssr?.resolve, + conditions: [ + 'rolldown', + ...defaultServerConditions, + ...(vite.ssr?.resolve?.conditions ?? []), + ], + }, + }, + } + : {}), + }; +}; + +/** + * Builds and executes the server entries through Vite's SSR pipeline. + * + * @param {Map} codeMap + * @param {Record} virtualImports + * @param {() => Promise} [createTemporaryDirectory] + * @returns {Promise>} + */ +export const renderServerEntries = async ( + codeMap, + virtualImports, + createTemporaryDirectory = () => mkdtemp(join(tmpdir(), 'doc-kit-vite-ssr-')) +) => { + const { input, sources } = createBuildEntries(codeMap, 'server'); + + for (const [id, code] of Object.entries(virtualImports)) { + sources.set(id, code); + } + + const temporaryDirectory = await createTemporaryDirectory(); + + try { + await build( + createViteConfig({ + sources, + input, + server: true, + serverOutDir: temporaryDirectory, + }) + ); + + const pages = new Map(); + + await Promise.all( + Object.keys(input).map(async name => { + const module = await import( + pathToFileURL(join(temporaryDirectory, `${name}.mjs`)).href + ); + + pages.set(name, await module.default()); + }) + ); + + return pages; + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }); + } +}; + +/** + * Lets Vite transform the rendered pages as HTML entries. Vite injects their + * hashed scripts, stylesheets, and module preloads, then writes the site. + * + * @param {Map} codeMap + * @param {Record} virtualImports + * @param {Map} htmlPages + * @returns {Promise} + */ +export const buildClientPages = async (codeMap, virtualImports, htmlPages) => { + const { sources } = createBuildEntries(codeMap, 'client'); + const root = resolve(getConfig('web').vite?.root ?? process.cwd()); + const input = []; + + for (const [fileName, html] of htmlPages) { + const id = resolve(root, fileName); + input.push(id); + sources.set(id, html); + } + + for (const [id, code] of Object.entries(virtualImports)) { + sources.set(id, code); + } + + await build(createViteConfig({ sources, input, server: false })); +};