From 4714c8664768ada69eb08e3c94006cbe89bb3fbf Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 19 Oct 2026 09:23:02 +0800 Subject: [PATCH 1/7] fix(core): resolve bundled dev runtime from core --- ecosystem-ci/repo.json | 2 +- .../__tests__/bundled-dev-runtime.spec.ts | 104 ++++++++++++++++++ packages/core/build.ts | 12 ++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 packages/core/__tests__/bundled-dev-runtime.spec.ts diff --git a/ecosystem-ci/repo.json b/ecosystem-ci/repo.json index 6f5d3177c8..4f83106a30 100644 --- a/ecosystem-ci/repo.json +++ b/ecosystem-ci/repo.json @@ -13,7 +13,7 @@ "vibe-dashboard": { "repository": "https://github.com/voidzero-dev/vibe-dashboard.git", "branch": "main", - "hash": "92a143a57478723987e778439a355291835135ec", + "hash": "b61318e0d574536d3c8b8015318a61123e92aa01", "forceFreshMigration": true, "playwright": true }, diff --git a/packages/core/__tests__/bundled-dev-runtime.spec.ts b/packages/core/__tests__/bundled-dev-runtime.spec.ts new file mode 100644 index 0000000000..505f98f05e --- /dev/null +++ b/packages/core/__tests__/bundled-dev-runtime.spec.ts @@ -0,0 +1,104 @@ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { expect, it } from 'vite-plus/test'; + +const coreDir = fileURLToPath(new URL('..', import.meta.url)); + +it.each(['missing', 'unrelated'])( + 'serves the bundled dev runtime when the rolldown package is %s', + (rolldownPackage) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-bundled-dev-runtime-')); + try { + // Copy the package outside the checkout so its devDependency on rolldown + // cannot hide an unresolved runtime import. Install it under the `vite` + // alias used by vite-plus to also exercise package self-references. + const installedCore = path.join(root, 'node_modules/vite'); + fs.mkdirSync(installedCore, { recursive: true }); + fs.copyFileSync(path.join(coreDir, 'package.json'), path.join(installedCore, 'package.json')); + fs.cpSync(path.join(coreDir, 'dist'), path.join(installedCore, 'dist'), { recursive: true }); + const pkg = JSON.parse(fs.readFileSync(path.join(coreDir, 'package.json'), 'utf8')); + for (const name of Object.keys({ ...pkg.dependencies, ...pkg.optionalDependencies })) { + const source = path.join(coreDir, 'node_modules', name); + if (fs.existsSync(source)) { + const dest = path.join(root, 'node_modules', name); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.symlinkSync(fs.realpathSync(source), dest, 'junction'); + } + } + + if (rolldownPackage === 'unrelated') { + const unrelatedDir = path.join(root, 'node_modules/rolldown'); + fs.mkdirSync(unrelatedDir, { recursive: true }); + fs.writeFileSync( + path.join(unrelatedDir, 'package.json'), + JSON.stringify({ + name: 'rolldown', + type: 'module', + exports: { './experimental/runtime': './experimental-runtime.mjs' }, + }), + ); + fs.writeFileSync( + path.join(unrelatedDir, 'experimental-runtime.mjs'), + 'throw new Error("Unrelated rolldown runtime");', + ); + } + + fs.writeFileSync( + path.join(root, 'index.html'), + '', + ); + fs.writeFileSync(path.join(root, 'main.js'), 'document.body.textContent = "bundled dev";'); + const script = path.join(installedCore, 'verify.mjs'); + fs.writeFileSync( + script, + ` +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { createServer } from '@voidzero-dev/vite-plus-core'; + +const server = await createServer({ + configFile: false, + root: process.cwd(), + base: '/nested/', + logLevel: 'silent', + experimental: { bundledDev: true }, + server: { host: '127.0.0.1', port: 0 }, +}); +try { + await server.listen(); + const base = 'http://127.0.0.1:' + server.httpServer.address().port + '/nested/'; + const client = await fetch(base + 'bundledDevClient.mjs'); + assert.equal(client.status, 200); + assert.match(await client.text(), /from\\s*['"]\\/nested\\/@rolldown\\/experimental-runtime\\.mjs['"]/); + const runtime = await fetch(base + '@rolldown/experimental-runtime.mjs'); + assert.equal(runtime.status, 200); + const expectedRuntime = fs.readFileSync( + new URL(import.meta.resolve('@voidzero-dev/vite-plus-core/rolldown/experimental/runtime')), 'utf8', + ); + assert.ok(await runtime.text() === expectedRuntime, 'must serve the bundled rolldown runtime'); + console.log('served bundled runtime and rewrote client import'); +} finally { + await server.close(); +} +`, + ); + const env = { ...process.env }; + delete env.NODE_PATH; + expect( + execFileSync(process.execPath, [script], { + cwd: root, + env, + encoding: 'utf8', + timeout: 30_000, + }), + ).toBe('served bundled runtime and rewrote client import\n'); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }, + 30_000, +); diff --git a/packages/core/build.ts b/packages/core/build.ts index a0fc8c2b88..18ae81091c 100644 --- a/packages/core/build.ts +++ b/packages/core/build.ts @@ -205,6 +205,18 @@ async function buildVite() { }; } } + if (id.endsWith(join('vite', 'src', 'node', 'plugins', 'clientInjections.ts'))) { + const { magicString } = meta; + if (magicString) { + // Resolve the runtime bundled with core. Keep ROLLDOWN_DEV_RUNTIME_ENTRY + // unchanged: it also matches the import in bundledDevClient.mjs. + magicString.replace( + 'import.meta.resolve(ROLLDOWN_DEV_RUNTIME_ENTRY)', + `import.meta.resolve('${pkgJson.name}/rolldown/experimental/runtime')`, + ); + return { code: magicString }; + } + } return undefined; }, }, From 6a6d89e272c3d6d17ad56155a2f9648f386eba2e Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 19 Oct 2026 09:30:53 +0800 Subject: [PATCH 2/7] test(core): cover bundled dev runtime with packed CLI snapshots --- .../command_dev_bundled_dev/package.json | 7 +- .../command_dev_bundled_dev/snapshots.toml | 27 ++++- .../snapshots/command_dev_bundled_dev.md | 19 +++- ...mand_dev_bundled_dev_unrelated_rolldown.md | 42 +++++++ .../experimental-runtime.mjs | 1 + .../unrelated-rolldown/package.json | 7 ++ .../command_dev_bundled_dev/vite.config.ts | 53 ++++++++- .../__tests__/bundled-dev-runtime.spec.ts | 104 ------------------ 8 files changed, 144 insertions(+), 116 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots/command_dev_bundled_dev_unrelated_rolldown.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/unrelated-rolldown/experimental-runtime.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/unrelated-rolldown/package.json delete mode 100644 packages/core/__tests__/bundled-dev-runtime.spec.ts diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/package.json index fcf5005964..e6c5ab9df2 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/package.json +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/package.json @@ -1,4 +1,9 @@ { "name": "command-dev-bundled-dev-test", - "private": true + "private": true, + "type": "module", + "devDependencies": { + "vite": "npm:@voidzero-dev/vite-plus-core@latest", + "vite-plus": "latest" + } } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots.toml index 449c743099..cb7f128568 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots.toml @@ -2,13 +2,34 @@ # ENV_ENTRY to dist/vite/client/, but it left BUNDLED_DEV_CLIENT_ENTRY at # dist/client/. With `experimental.bundledDev` enabled, Vite reads that # path with fs.readFileSync, so `vp dev` fails at startup with ENOENT. +# Install packed packages for #3110: linking core to the checkout exposes its +# rolldown devDependency and hides missing runtime rewrites. Disable automatic +# peer installation so npm cannot add another rolldown through upstream Vite. [[case]] name = "command_dev_bundled_dev" -vp = "local" +vp = "global" skip-platforms = ["windows"] +local-registry = true +env = { ROLLDOWN_PACKAGE = "missing" } steps = [ - { argv = ["vp", "dev", "--host", "127.0.0.1", "--port", "0"], comment = "dev server with experimental.bundledDev enabled starts and serves the bundled dev client", continue-on-failure = true, interactions = [ - { "expect-milestone" = "dev-server:ready" }, + { argv = ["npm", "install", "--ignore-scripts", "--legacy-peer-deps", "--no-audit", "--no-fund"], snapshot = false }, + { argv = ["vp", "dev", "--host", "127.0.0.1", "--port", "0"], comment = "Bundled dev serves core's runtime without a separate rolldown package.", interactions = [ + { "expect-milestone" = "dev-server:runtime-verified" }, + { "write-key" = "ctrl-c" }, + ] }, +] + +[[case]] +name = "command_dev_bundled_dev_unrelated_rolldown" +vp = "global" +skip-platforms = ["windows"] +local-registry = true +env = { ROLLDOWN_PACKAGE = "unrelated" } +steps = [ + { argv = ["npm", "install", "--ignore-scripts", "--legacy-peer-deps", "--no-audit", "--no-fund"], snapshot = false }, + { argv = ["vpt", "cp", "-r", "unrelated-rolldown", "node_modules/rolldown"], snapshot = false }, + { argv = ["vp", "dev", "--host", "127.0.0.1", "--port", "0"], comment = "An unrelated rolldown package must not replace core's bundled runtime.", interactions = [ + { "expect-milestone" = "dev-server:runtime-verified" }, { "write-key" = "ctrl-c" }, ] }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots/command_dev_bundled_dev.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots/command_dev_bundled_dev.md index 070e0ded78..d76beefc8e 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots/command_dev_bundled_dev.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots/command_dev_bundled_dev.md @@ -1,28 +1,39 @@ # command_dev_bundled_dev +## `npm install --ignore-scripts --legacy-peer-deps --no-audit --no-fund` + + ## `vp dev --host 127.0.0.1 --port 0` -dev server with experimental.bundledDev enabled starts and serves the bundled dev client +Bundled dev serves core's runtime without a separate rolldown package. **Exit code:** 130 -**→ expect-milestone:** `dev-server:ready` +**→ expect-milestone:** `dev-server:runtime-verified` ``` +VITE+ - The Unified Toolchain for the Web VITE+ ready in - ➜ Local: http://127.0.0.1:/ + ➜ Local: http://127.0.0.1:/nested/ ➜ press h + enter to show help +Rolldown package: missing +Client imports /nested/@rolldown/experimental-runtime.mjs +Served runtime matches core ``` **← write-key:** `ctrl-c` ``` +VITE+ - The Unified Toolchain for the Web VITE+ ready in - ➜ Local: http://127.0.0.1:/ + ➜ Local: http://127.0.0.1:/nested/ ➜ press h + enter to show help +Rolldown package: missing +Client imports /nested/@rolldown/experimental-runtime.mjs +Served runtime matches core ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots/command_dev_bundled_dev_unrelated_rolldown.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots/command_dev_bundled_dev_unrelated_rolldown.md new file mode 100644 index 0000000000..9fc1fc7136 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots/command_dev_bundled_dev_unrelated_rolldown.md @@ -0,0 +1,42 @@ +# command_dev_bundled_dev_unrelated_rolldown + +## `npm install --ignore-scripts --legacy-peer-deps --no-audit --no-fund` + + +## `vpt cp -r unrelated-rolldown node_modules/rolldown` + + +## `vp dev --host 127.0.0.1 --port 0` + +An unrelated rolldown package must not replace core's bundled runtime. + +**Exit code:** 130 + +**→ expect-milestone:** `dev-server:runtime-verified` + +``` +VITE+ - The Unified Toolchain for the Web + + VITE+ ready in + + ➜ Local: http://127.0.0.1:/nested/ + ➜ press h + enter to show help +Rolldown package: unrelated +Client imports /nested/@rolldown/experimental-runtime.mjs +Served runtime matches core +``` + +**← write-key:** `ctrl-c` + +``` +VITE+ - The Unified Toolchain for the Web + + VITE+ ready in + + ➜ Local: http://127.0.0.1:/nested/ + ➜ press h + enter to show help +Rolldown package: unrelated +Client imports /nested/@rolldown/experimental-runtime.mjs +Served runtime matches core + +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/unrelated-rolldown/experimental-runtime.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/unrelated-rolldown/experimental-runtime.mjs new file mode 100644 index 0000000000..0c97657a10 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/unrelated-rolldown/experimental-runtime.mjs @@ -0,0 +1 @@ +throw new Error('Unrelated rolldown runtime'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/unrelated-rolldown/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/unrelated-rolldown/package.json new file mode 100644 index 0000000000..3ba1f0dea4 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/unrelated-rolldown/package.json @@ -0,0 +1,7 @@ +{ + "name": "rolldown", + "type": "module", + "exports": { + "./experimental/runtime": "./experimental-runtime.mjs" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/vite.config.ts index 971849d01a..cf8b02c2a3 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/vite.config.ts +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/vite.config.ts @@ -1,17 +1,62 @@ +import assert from 'node:assert/strict'; +import { readFileSync, realpathSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { join, sep } from 'node:path'; + export default { + base: '/nested/', clearScreen: false, experimental: { bundledDev: true, }, plugins: [ { - name: 'dev-server-ready-milestone', + name: 'verify-bundled-dev-runtime', configureServer(server) { + const projectRequire = createRequire(join(server.config.root, 'package.json')); + const cliRequire = createRequire(projectRequire.resolve('vite-plus/package.json')); + const corePackage = cliRequire.resolve('vite/package.json'); + // Reject a linked checkout: its development dependencies can mask #3110. + assert.ok(realpathSync(corePackage).startsWith(realpathSync('node_modules') + sep)); + const coreRequire = createRequire(corePackage); + assert.equal(coreRequire('./package.json').name, '@voidzero-dev/vite-plus-core'); + if (process.env.ROLLDOWN_PACKAGE === 'unrelated') { + assert.equal( + coreRequire.resolve('rolldown/experimental/runtime'), + realpathSync('node_modules/rolldown/experimental-runtime.mjs'), + ); + } else { + assert.throws(() => coreRequire.resolve('rolldown/experimental/runtime'), { + code: 'MODULE_NOT_FOUND', + }); + } + const expectedRuntime = readFileSync( + coreRequire.resolve('@voidzero-dev/vite-plus-core/rolldown/experimental/runtime'), + 'utf8', + ); server.httpServer?.once('listening', () => { // Let Vite print its startup banner after server.listen() resolves. - setImmediate(() => { - const name = Buffer.from('dev-server:ready').toString('base64url'); - process.stdout.write(`\x1b]2;vt-pty-terminal-test:${'0'.repeat(32)}:${name}\x1b\\`); + setImmediate(async () => { + try { + const base = `http://127.0.0.1:${server.httpServer.address().port}/nested/`; + const client = await fetch(base + 'bundledDevClient.mjs'); + assert.equal(client.status, 200); + assert.match( + await client.text(), + /from\s*['"]\/nested\/@rolldown\/experimental-runtime\.mjs['"]/, + ); + const runtime = await fetch(base + '@rolldown/experimental-runtime.mjs'); + assert.equal(runtime.status, 200); + assert.ok((await runtime.text()) === expectedRuntime, 'must serve core\'s runtime'); + console.log(`Rolldown package: ${process.env.ROLLDOWN_PACKAGE}`); + console.log('Client imports /nested/@rolldown/experimental-runtime.mjs'); + console.log('Served runtime matches core'); + const name = Buffer.from('dev-server:runtime-verified').toString('base64url'); + process.stdout.write(`\x1b]2;vt-pty-terminal-test:${'0'.repeat(32)}:${name}\x1b\\`); + } catch (error) { + console.error(error); + process.exit(1); + } }); }); }, diff --git a/packages/core/__tests__/bundled-dev-runtime.spec.ts b/packages/core/__tests__/bundled-dev-runtime.spec.ts deleted file mode 100644 index 505f98f05e..0000000000 --- a/packages/core/__tests__/bundled-dev-runtime.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { execFileSync } from 'node:child_process'; -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { expect, it } from 'vite-plus/test'; - -const coreDir = fileURLToPath(new URL('..', import.meta.url)); - -it.each(['missing', 'unrelated'])( - 'serves the bundled dev runtime when the rolldown package is %s', - (rolldownPackage) => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-bundled-dev-runtime-')); - try { - // Copy the package outside the checkout so its devDependency on rolldown - // cannot hide an unresolved runtime import. Install it under the `vite` - // alias used by vite-plus to also exercise package self-references. - const installedCore = path.join(root, 'node_modules/vite'); - fs.mkdirSync(installedCore, { recursive: true }); - fs.copyFileSync(path.join(coreDir, 'package.json'), path.join(installedCore, 'package.json')); - fs.cpSync(path.join(coreDir, 'dist'), path.join(installedCore, 'dist'), { recursive: true }); - const pkg = JSON.parse(fs.readFileSync(path.join(coreDir, 'package.json'), 'utf8')); - for (const name of Object.keys({ ...pkg.dependencies, ...pkg.optionalDependencies })) { - const source = path.join(coreDir, 'node_modules', name); - if (fs.existsSync(source)) { - const dest = path.join(root, 'node_modules', name); - fs.mkdirSync(path.dirname(dest), { recursive: true }); - fs.symlinkSync(fs.realpathSync(source), dest, 'junction'); - } - } - - if (rolldownPackage === 'unrelated') { - const unrelatedDir = path.join(root, 'node_modules/rolldown'); - fs.mkdirSync(unrelatedDir, { recursive: true }); - fs.writeFileSync( - path.join(unrelatedDir, 'package.json'), - JSON.stringify({ - name: 'rolldown', - type: 'module', - exports: { './experimental/runtime': './experimental-runtime.mjs' }, - }), - ); - fs.writeFileSync( - path.join(unrelatedDir, 'experimental-runtime.mjs'), - 'throw new Error("Unrelated rolldown runtime");', - ); - } - - fs.writeFileSync( - path.join(root, 'index.html'), - '', - ); - fs.writeFileSync(path.join(root, 'main.js'), 'document.body.textContent = "bundled dev";'); - const script = path.join(installedCore, 'verify.mjs'); - fs.writeFileSync( - script, - ` -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import { createServer } from '@voidzero-dev/vite-plus-core'; - -const server = await createServer({ - configFile: false, - root: process.cwd(), - base: '/nested/', - logLevel: 'silent', - experimental: { bundledDev: true }, - server: { host: '127.0.0.1', port: 0 }, -}); -try { - await server.listen(); - const base = 'http://127.0.0.1:' + server.httpServer.address().port + '/nested/'; - const client = await fetch(base + 'bundledDevClient.mjs'); - assert.equal(client.status, 200); - assert.match(await client.text(), /from\\s*['"]\\/nested\\/@rolldown\\/experimental-runtime\\.mjs['"]/); - const runtime = await fetch(base + '@rolldown/experimental-runtime.mjs'); - assert.equal(runtime.status, 200); - const expectedRuntime = fs.readFileSync( - new URL(import.meta.resolve('@voidzero-dev/vite-plus-core/rolldown/experimental/runtime')), 'utf8', - ); - assert.ok(await runtime.text() === expectedRuntime, 'must serve the bundled rolldown runtime'); - console.log('served bundled runtime and rewrote client import'); -} finally { - await server.close(); -} -`, - ); - const env = { ...process.env }; - delete env.NODE_PATH; - expect( - execFileSync(process.execPath, [script], { - cwd: root, - env, - encoding: 'utf8', - timeout: 30_000, - }), - ).toBe('served bundled runtime and rewrote client import\n'); - } finally { - fs.rmSync(root, { recursive: true, force: true }); - } - }, - 30_000, -); From 69849772bfb6ef71a3b22a5445b6503e89b9c071 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 19 Oct 2026 10:03:28 +0800 Subject: [PATCH 3/7] test(ecosystem): separate bundled dev from Vitest browser tests --- .github/workflows/reusable-ecosystem-ci.yml | 1 + ecosystem-ci/patch-project.ts | 19 ++++++++ ecosystem-ci/verify-vibe-dashboard.ts | 53 +++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 ecosystem-ci/verify-vibe-dashboard.ts diff --git a/.github/workflows/reusable-ecosystem-ci.yml b/.github/workflows/reusable-ecosystem-ci.yml index eb6fc99364..394b178c47 100644 --- a/.github/workflows/reusable-ecosystem-ci.yml +++ b/.github/workflows/reusable-ecosystem-ci.yml @@ -129,6 +129,7 @@ jobs: # FIXME: Failed to load JS plugin: ./plugins/debugger.js # vp run ready vp fmt + node "$GITHUB_WORKSPACE/ecosystem-ci/verify-vibe-dashboard.ts" vp test --api.port="$ECOSYSTEM_VITEST_PORT" vp run build # FIXME: TypeError: Failed to fetch dynamically imported module diff --git a/ecosystem-ci/patch-project.ts b/ecosystem-ci/patch-project.ts index 6c0ef7786b..390de96a7d 100644 --- a/ecosystem-ci/patch-project.ts +++ b/ecosystem-ci/patch-project.ts @@ -88,6 +88,25 @@ if (process.env.GITHUB_ENV) { await prepareWebdriverioProject(project, repoRoot, cli); +if (project === 'vibe-dashboard') { + // Vitest 5.0.5's browser runner needs Vite's unbundled module middleware. + // With bundledDev, its runner scripts receive HTML and the session times out. + // Keep bundled dev enabled outside Vitest; verify-vibe-dashboard.ts loads the + // actual app in Chromium with that mode enabled. + // Remove this workaround when https://github.com/vitest-dev/vitest/issues/11052 is fixed. + const viteConfigPath = join(repoRoot, 'apps', 'dashboard', 'vite.config.ts'); + const viteConfig = await readFile(viteConfigPath, 'utf-8'); + const bundledDev = 'bundledDev: true'; + if (viteConfig.split(bundledDev).length !== 2) { + throw new Error('vibe-dashboard patch: expected bundledDev to be enabled'); + } + await writeFile( + viteConfigPath, + viteConfig.replace(bundledDev, 'bundledDev: process.env.VITEST !== "true"'), + 'utf-8', + ); +} + if (project === 'rollipop') { const oxfmtrc = await readFile(join(repoRoot, '.oxfmtrc.json'), 'utf-8'); await writeFile( diff --git a/ecosystem-ci/verify-vibe-dashboard.ts b/ecosystem-ci/verify-vibe-dashboard.ts new file mode 100644 index 0000000000..ebae4c6973 --- /dev/null +++ b/ecosystem-ci/verify-vibe-dashboard.ts @@ -0,0 +1,53 @@ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +// Use the ecosystem project's installed packages, not the repository's links. +const require = createRequire(`${process.cwd()}/`); +const cliRequire = createRequire(require.resolve('vite-plus/package.json')); +const { createServer }: typeof import('vite') = await import( + pathToFileURL(cliRequire.resolve('vite')).href +); +const { chromium }: typeof import('playwright') = require('playwright'); + +const server = await createServer({ + root: resolve('apps/dashboard'), + server: { host: '127.0.0.1', port: 0 }, +}); + +try { + assert.equal(server.config.experimental.bundledDev, true); + await server.listen(); + const url = server.resolvedUrls?.local[0]; + assert.ok(url, 'The bundled dev server must have a local URL'); + + const browser = await chromium.launch({ headless: true }); + try { + const page = await browser.newPage(); + const errors: string[] = []; + page.on('pageerror', (error) => { + errors.push(error.message); + console.error(error); + }); + // Font downloads are unrelated to bundled dev and need no external service. + await page.route('https://fonts.googleapis.com/**', (route) => + route.fulfill({ contentType: 'text/css', body: '' }), + ); + + const [, runtime] = await Promise.all([ + page.goto(url), + page.waitForResponse( + (response) => new URL(response.url()).pathname === '/@rolldown/experimental-runtime.mjs', + ), + ]); + assert.equal(runtime.status(), 200); + await page.getByRole('heading', { name: 'VoidZero Dashboard', exact: true }).waitFor(); + assert.deepEqual(errors, [], 'The dashboard must render without browser errors'); + console.log('ok vibe-dashboard renders in Chromium with bundled dev enabled'); + } finally { + await browser.close(); + } +} finally { + await server.close(); +} From bc81afcda2ad0be4b28787caf511a7a399cf49d2 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 19 Oct 2026 10:51:12 +0800 Subject: [PATCH 4/7] test(ecosystem): use updated bundled dev dashboard revision --- ecosystem-ci/patch-project.ts | 19 ------------------- ecosystem-ci/repo.json | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/ecosystem-ci/patch-project.ts b/ecosystem-ci/patch-project.ts index 390de96a7d..6c0ef7786b 100644 --- a/ecosystem-ci/patch-project.ts +++ b/ecosystem-ci/patch-project.ts @@ -88,25 +88,6 @@ if (process.env.GITHUB_ENV) { await prepareWebdriverioProject(project, repoRoot, cli); -if (project === 'vibe-dashboard') { - // Vitest 5.0.5's browser runner needs Vite's unbundled module middleware. - // With bundledDev, its runner scripts receive HTML and the session times out. - // Keep bundled dev enabled outside Vitest; verify-vibe-dashboard.ts loads the - // actual app in Chromium with that mode enabled. - // Remove this workaround when https://github.com/vitest-dev/vitest/issues/11052 is fixed. - const viteConfigPath = join(repoRoot, 'apps', 'dashboard', 'vite.config.ts'); - const viteConfig = await readFile(viteConfigPath, 'utf-8'); - const bundledDev = 'bundledDev: true'; - if (viteConfig.split(bundledDev).length !== 2) { - throw new Error('vibe-dashboard patch: expected bundledDev to be enabled'); - } - await writeFile( - viteConfigPath, - viteConfig.replace(bundledDev, 'bundledDev: process.env.VITEST !== "true"'), - 'utf-8', - ); -} - if (project === 'rollipop') { const oxfmtrc = await readFile(join(repoRoot, '.oxfmtrc.json'), 'utf-8'); await writeFile( diff --git a/ecosystem-ci/repo.json b/ecosystem-ci/repo.json index 4f83106a30..d77e992824 100644 --- a/ecosystem-ci/repo.json +++ b/ecosystem-ci/repo.json @@ -13,7 +13,7 @@ "vibe-dashboard": { "repository": "https://github.com/voidzero-dev/vibe-dashboard.git", "branch": "main", - "hash": "b61318e0d574536d3c8b8015318a61123e92aa01", + "hash": "738b92584adf4d5ca4f1ad992519c5dffa12e200", "forceFreshMigration": true, "playwright": true }, From 01e548a986e677cd84e7253685e59169f61c6ee4 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 19 Oct 2026 12:27:04 +0800 Subject: [PATCH 5/7] test(ecosystem): run dashboard upstream dev smoke test --- .github/workflows/reusable-ecosystem-ci.yml | 2 +- ecosystem-ci/repo.json | 2 +- ecosystem-ci/verify-vibe-dashboard.ts | 53 --------------------- 3 files changed, 2 insertions(+), 55 deletions(-) delete mode 100644 ecosystem-ci/verify-vibe-dashboard.ts diff --git a/.github/workflows/reusable-ecosystem-ci.yml b/.github/workflows/reusable-ecosystem-ci.yml index 394b178c47..ae0c983f31 100644 --- a/.github/workflows/reusable-ecosystem-ci.yml +++ b/.github/workflows/reusable-ecosystem-ci.yml @@ -129,9 +129,9 @@ jobs: # FIXME: Failed to load JS plugin: ./plugins/debugger.js # vp run ready vp fmt - node "$GITHUB_WORKSPACE/ecosystem-ci/verify-vibe-dashboard.ts" vp test --api.port="$ECOSYSTEM_VITEST_PORT" vp run build + vp run test:dev # FIXME: TypeError: Failed to fetch dynamically imported module # - name: skeleton # node-version: 24 diff --git a/ecosystem-ci/repo.json b/ecosystem-ci/repo.json index d77e992824..1e2ae01307 100644 --- a/ecosystem-ci/repo.json +++ b/ecosystem-ci/repo.json @@ -13,7 +13,7 @@ "vibe-dashboard": { "repository": "https://github.com/voidzero-dev/vibe-dashboard.git", "branch": "main", - "hash": "738b92584adf4d5ca4f1ad992519c5dffa12e200", + "hash": "be10551d4b73b7bcb53491311b574fb214a38e1f", "forceFreshMigration": true, "playwright": true }, diff --git a/ecosystem-ci/verify-vibe-dashboard.ts b/ecosystem-ci/verify-vibe-dashboard.ts deleted file mode 100644 index ebae4c6973..0000000000 --- a/ecosystem-ci/verify-vibe-dashboard.ts +++ /dev/null @@ -1,53 +0,0 @@ -import assert from 'node:assert/strict'; -import { createRequire } from 'node:module'; -import { resolve } from 'node:path'; -import { pathToFileURL } from 'node:url'; - -// Use the ecosystem project's installed packages, not the repository's links. -const require = createRequire(`${process.cwd()}/`); -const cliRequire = createRequire(require.resolve('vite-plus/package.json')); -const { createServer }: typeof import('vite') = await import( - pathToFileURL(cliRequire.resolve('vite')).href -); -const { chromium }: typeof import('playwright') = require('playwright'); - -const server = await createServer({ - root: resolve('apps/dashboard'), - server: { host: '127.0.0.1', port: 0 }, -}); - -try { - assert.equal(server.config.experimental.bundledDev, true); - await server.listen(); - const url = server.resolvedUrls?.local[0]; - assert.ok(url, 'The bundled dev server must have a local URL'); - - const browser = await chromium.launch({ headless: true }); - try { - const page = await browser.newPage(); - const errors: string[] = []; - page.on('pageerror', (error) => { - errors.push(error.message); - console.error(error); - }); - // Font downloads are unrelated to bundled dev and need no external service. - await page.route('https://fonts.googleapis.com/**', (route) => - route.fulfill({ contentType: 'text/css', body: '' }), - ); - - const [, runtime] = await Promise.all([ - page.goto(url), - page.waitForResponse( - (response) => new URL(response.url()).pathname === '/@rolldown/experimental-runtime.mjs', - ), - ]); - assert.equal(runtime.status(), 200); - await page.getByRole('heading', { name: 'VoidZero Dashboard', exact: true }).waitFor(); - assert.deepEqual(errors, [], 'The dashboard must render without browser errors'); - console.log('ok vibe-dashboard renders in Chromium with bundled dev enabled'); - } finally { - await browser.close(); - } -} finally { - await server.close(); -} From 41cd8db42655d06a30d6a2b0a13f5498d12468dd Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 19 Oct 2026 13:42:56 +0800 Subject: [PATCH 6/7] fix(core): guard bundled Rolldown package references --- packages/core/BUNDLING.md | 9 ++ .../build-artifacts.spec.ts.snap | 42 +++++++++ .../core/__tests__/build-artifacts.spec.ts | 92 +++++++++++++++++++ packages/core/build.ts | 19 ++++ 4 files changed, 162 insertions(+) create mode 100644 packages/core/__tests__/__snapshots__/build-artifacts.spec.ts.snap diff --git a/packages/core/BUNDLING.md b/packages/core/BUNDLING.md index ceb7c85b48..44defda08d 100644 --- a/packages/core/BUNDLING.md +++ b/packages/core/BUNDLING.md @@ -42,6 +42,7 @@ await cp(join(rolldownPluginUtilsDir, 'dist'), join(projectDir, 'dist', 'pluginu - `@rolldown/pluginutils` → `@voidzero-dev/vite-plus-core/rolldown/pluginutils` - `rolldown/*` → `@voidzero-dev/vite-plus-core/rolldown/*` +- Replace the WebContainer fallback's `rolldown/package.json` lookup with the bundled Rolldown version. The WASM download must use that version, not the core package version. - In release builds: supported `@rolldown/binding-*` → `@voidzero-dev/vite-plus-*` **Input**: `rolldown/packages/rolldown/dist/` @@ -129,6 +130,14 @@ Located in `build-support/rewrite-module-specifiers.ts`, this utility rewrites s | `@rolldown/pluginutils` | `@voidzero-dev/vite-plus-core/rolldown/pluginutils` | | `@rolldown/pluginutils/*` | `@voidzero-dev/vite-plus-core/rolldown/pluginutils/*` | +### Detecting Missed Rewrites + +`__tests__/build-artifacts.spec.ts` snapshots the remaining bare `rolldown` and `@rolldown/pluginutils` names in all built JavaScript and declarations. It parses string literals and template literals, then records references to constants and their aliases. This includes package names passed to `require.resolve()` or `import.meta.resolve()`, which import-only checks miss. + +The snapshot allows CLI branding, embedded package metadata, and the browser runtime import that Vite rewrites before serving. Review new entries when upstream code changes; do not accept unresolved Node package lookups. Dynamically constructed names can escape static checks, so the installed-package bundled dev snapshots also verify runtime resolution without a separate Rolldown installation. + +Native `@rolldown/binding-*` references have separate rewrite checks, described below. + ### Release Build: Native Binding Rewriting During release builds (`RELEASE_BUILD=1`), `bundleRolldown()` rewrites Rolldown's native binding requires through `build-support/rewrite-rolldown-binding.ts`. For every platform in the CLI's `napi.targets` (mapped to napi platform suffixes with `parseTriple`), the loader's `@rolldown/binding-` requires become `@voidzero-dev/vite-plus-`, and that branch's version guard switches from the Rolldown version to core's version, which is what the Vite+ platform packages are published as. diff --git a/packages/core/__tests__/__snapshots__/build-artifacts.spec.ts.snap b/packages/core/__tests__/__snapshots__/build-artifacts.spec.ts.snap new file mode 100644 index 0000000000..ad846ae90b --- /dev/null +++ b/packages/core/__tests__/__snapshots__/build-artifacts.spec.ts.snap @@ -0,0 +1,42 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`build artifacts > snapshots remaining Rolldown package names and their uses in all built code 1`] = ` +[ + { + "code": "cac("rolldown")", + "file": "rolldown/cli.mjs", + }, + { + "code": "name: "rolldown"", + "file": "rolldown/shared/bindingify-input-options-[hash].mjs", + }, + { + "code": "keywords: [ "bundler", "esbuild", "parcel", "rolldown", "rollup", "webpack" ]", + "file": "rolldown/shared/bindingify-input-options-[hash].mjs", + }, + { + "code": ""rolldown": "./bin/cli.mjs"", + "file": "rolldown/shared/bindingify-input-options-[hash].mjs", + }, + { + "code": ""@rolldown/pluginutils": "catalog:"", + "file": "rolldown/shared/bindingify-input-options-[hash].mjs", + }, + { + "code": ""rolldown": "workspace:*"", + "file": "rolldown/shared/bindingify-input-options-[hash].mjs", + }, + { + "code": "import { DevRuntime } from "rolldown/experimental/runtime";", + "file": "vite/client/bundledDevClient.mjs", + }, + { + "code": "ROLLDOWN_DEV_RUNTIME_ENTRY = "rolldown/experimental/runtime"", + "file": "vite/node/chunks/node.js", + }, + { + "code": "new RegExp(\`(from\\\\s*['"])\${ROLLDOWN_DEV_RUNTIME_ENTRY}(['"])\`)", + "file": "vite/node/chunks/node.js", + }, +] +`; diff --git a/packages/core/__tests__/build-artifacts.spec.ts b/packages/core/__tests__/build-artifacts.spec.ts index d1fc9148c3..5f0946c750 100644 --- a/packages/core/__tests__/build-artifacts.spec.ts +++ b/packages/core/__tests__/build-artifacts.spec.ts @@ -3,13 +3,105 @@ import { createRequire } from 'node:module'; import path from 'node:path'; import url from 'node:url'; +import { Lang, parse as parseAst, type SgNode } from '@ast-grep/napi'; import { init, parse } from 'es-module-lexer'; +import { glob } from 'tinyglobby'; import { describe, expect, it } from 'vite-plus/test'; const coreDir = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..'); const distDir = path.join(coreDir, 'dist'); +const referenceContexts = new Set>([ + 'import_statement', + 'export_statement', + 'call_expression', + 'new_expression', + 'variable_declarator', + 'pair', + 'expression_statement', +]); + +function referenceContext(node: SgNode): string { + let parent = node.parent(); + while (parent && !referenceContexts.has(node.kind())) { + node = parent; + parent = node.parent(); + } + return node.text().replace(/\s+/g, ' '); +} + describe('build artifacts', () => { + it('snapshots remaining Rolldown package names and their uses in all built code', async () => { + const files = (await glob('**/*.{js,mjs,cjs,ts,mts,cts}', { cwd: distDir })).toSorted(); + expect(files.length).toBeGreaterThan(0); + const references: { file: string; code: string }[] = []; + + for (const file of files) { + const source = fs.readFileSync(path.join(distDir, file), 'utf8'); + const lang = /\.[cm]?ts$/.test(file) ? Lang.TypeScript : Lang.JavaScript; + const root = parseAst(lang, source).root(); + const record = (node: SgNode) => { + references.push({ + file: file.replace(/-[\w-]{8}(?=(?:\.d)?\.[cm]?[jt]s$)/, '-[hash]'), + code: referenceContext(node), + }); + }; + + // Inspect literals, not only imports: require.resolve, import.meta.resolve, + // templates, and new resolution APIs must not bypass this check. Comments + // are excluded by the parser. Native @rolldown/binding-* packages have a + // separate release rewrite and coverage in rewrite-rolldown-binding.spec.ts. + const literals = root.findAll({ + rule: { any: [{ kind: 'string' }, { kind: 'template_string' }] }, + }); + for (const literal of literals) { + if ( + !/^(?:rolldown|@rolldown\/pluginutils)(?:\/|$|\$\{)/.test(literal.text().slice(1, -1)) + ) { + continue; + } + record(literal); + + // The browser-import matching constant is intentional. Snapshot its uses + // too, so restoring import.meta.resolve(ROLLDOWN_DEV_RUNTIME_ENTRY) fails + // even though the declaration itself has not changed. Follow aliases. + const declaration = literal.parent(); + const name = declaration?.kind() === 'variable_declarator' && declaration.field('name'); + if (!name || name.kind() !== 'identifier') { + continue; + } + const bindings = new Set([name.text()]); + const identifiers = root.findAll({ rule: { kind: 'identifier' } }); + for (const binding of bindings) { + for (const identifier of identifiers) { + if (identifier.text() !== binding) { + continue; + } + const parent = identifier.parent(); + if ( + parent?.kind() === 'variable_declarator' && + parent.field('name')?.text() === binding + ) { + continue; + } + record(identifier); + if (parent?.kind() === 'variable_declarator') { + const alias = parent.field('name'); + if (alias?.kind() === 'identifier') { + bindings.add(alias.text()); + } + } + } + } + } + } + + // Only CLI branding, embedded package metadata, and the browser import that + // Vite rewrites before serving may remain. New entries require review; + // do not accept a snapshot containing an unresolved Node package lookup. + expect(references).toMatchSnapshot(); + }); + it('should include esm-shims.js in dist for tsdown shims support', () => { const shimsPath = path.join(distDir, 'esm-shims.js'); expect(fs.existsSync(shimsPath), `${shimsPath} should exist`).toBe(true); diff --git a/packages/core/build.ts b/packages/core/build.ts index 18ae81091c..203addbd8d 100644 --- a/packages/core/build.ts +++ b/packages/core/build.ts @@ -348,6 +348,12 @@ async function bundleRolldownPluginutils() { async function bundleRolldown() { await mkdir(join(projectDir, 'dist/rolldown'), { recursive: true }); + const { version: rolldownVersion }: { version: string } = JSON.parse( + await readFile(join(rolldownSourceDir, 'package.json'), 'utf-8'), + ); + const webContainerVersionLookup = + 'JSON.parse(fs.readFileSync(__require.resolve("rolldown/package.json"), "utf-8")).version'; + let webContainerVersionRewrites = 0; const rolldownFiles = new Set(); await cp(join(rolldownSourceDir, 'dist'), join(projectDir, 'dist/rolldown'), { @@ -385,6 +391,12 @@ async function bundleRolldown() { file.endsWith('.d.ts') ) { let source = await readFile(file, 'utf-8'); + // WebContainer downloads the upstream WASM binding. Inline Rolldown's + // version so it needs neither an undeclared package nor Vite+'s version. + source = source.replaceAll(webContainerVersionLookup, () => { + webContainerVersionRewrites++; + return JSON.stringify(rolldownVersion); + }); const rules: ReplacementRule[] = [...createRolldownRewriteRules(pkgJson.name)]; if (vitePlusPlatformSuffixes) { const result = rewriteRolldownBindingRequires(source, { @@ -404,6 +416,13 @@ async function bundleRolldown() { } } + if (webContainerVersionRewrites !== 1) { + throw new Error( + `bundleRolldown: expected one WebContainer version lookup, found ${webContainerVersionRewrites}; ` + + 'review the upstream fallback and update or remove its version rewrite', + ); + } + // Every published platform suffix must find its loader branch, and each // redirected branch requires the platform package twice (the binding itself // and its package.json version guard) with one guard. A napi-rs upgrade From 9a3c8e763abcfeca1c3a081b2bb59604613b6959 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 19 Oct 2026 14:02:09 +0800 Subject: [PATCH 7/7] test(core): snapshot WebContainer binding fallback --- .../package.json | 8 + .../snapshots.toml | 12 ++ .../rolldown_webcontainer_fallback.md | 28 ++++ .../rolldown_webcontainer_fallback/verify.mjs | 148 ++++++++++++++++++ packages/core/BUNDLING.md | 2 + 5 files changed, 198 insertions(+) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/snapshots/rolldown_webcontainer_fallback.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/verify.mjs diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/package.json new file mode 100644 index 0000000000..bb82239909 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/package.json @@ -0,0 +1,8 @@ +{ + "name": "rolldown-webcontainer-fallback-test", + "private": true, + "type": "module", + "dependencies": { + "@voidzero-dev/vite-plus-core": "latest" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/snapshots.toml new file mode 100644 index 0000000000..b9295204b3 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/snapshots.toml @@ -0,0 +1,12 @@ +# Execute the complete packed binding loader in a simulated WebContainer. +# Native bindings are unavailable; the WASM installer and cache are local fakes. +# A linked checkout would hide the missing rolldown/package.json dependency. +[[case]] +name = "rolldown_webcontainer_fallback" +vp = "global" +local-registry = true +steps = [ + { argv = ["npm", "install", "--ignore-scripts", "--legacy-peer-deps", "--no-audit", "--no-fund"], snapshot = false }, + { argv = ["node", "--experimental-vm-modules", "--disable-warning=ExperimentalWarning", "verify.mjs", "missing"], comment = "Without a standalone rolldown package, download the WASM binding at the bundled Rolldown version." }, + { argv = ["node", "--experimental-vm-modules", "--disable-warning=ExperimentalWarning", "verify.mjs", "cached"], comment = "Load an existing WASM binding without a package lookup or another download." }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/snapshots/rolldown_webcontainer_fallback.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/snapshots/rolldown_webcontainer_fallback.md new file mode 100644 index 0000000000..a777e6f700 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/snapshots/rolldown_webcontainer_fallback.md @@ -0,0 +1,28 @@ +# rolldown_webcontainer_fallback + +## `npm install --ignore-scripts --legacy-peer-deps --no-audit --no-fund` + + +## `node --experimental-vm-modules --disable-warning=ExperimentalWarning verify.mjs missing` + +Without a standalone rolldown package, download the WASM binding at the bundled Rolldown version. + +``` +Standalone rolldown: missing +WASM cache: missing +[rolldown] Downloading @rolldown/binding-wasm32-wasi@ on WebContainer... +Installer selected the bundled Rolldown version +Loaded the WebContainer WASM binding +Downloads: 1 +``` + +## `node --experimental-vm-modules --disable-warning=ExperimentalWarning verify.mjs cached` + +Load an existing WASM binding without a package lookup or another download. + +``` +Standalone rolldown: missing +WASM cache: cached +Loaded the WebContainer WASM binding +Downloads: 0 +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/verify.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/verify.mjs new file mode 100644 index 0000000000..a4beaf509b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/rolldown_webcontainer_fallback/verify.mjs @@ -0,0 +1,148 @@ +import assert from 'node:assert/strict'; +import { readFileSync, readdirSync, realpathSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { dirname, join, sep } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { createContext, SourceTextModule, SyntheticModule } from 'node:vm'; + +const mode = process.argv[2]; +assert.ok(mode === 'missing' || mode === 'cached'); +const projectRequire = createRequire(import.meta.url); +const corePackage = projectRequire.resolve('@voidzero-dev/vite-plus-core/package.json'); +assert.ok(realpathSync(corePackage).startsWith(realpathSync('node_modules') + sep)); +const coreRequire = createRequire(corePackage); +assert.throws(() => coreRequire.resolve('rolldown/package.json'), { code: 'MODULE_NOT_FOUND' }); +const core = coreRequire('./package.json'); +const version = core.bundledVersions.rolldown; +assert.notEqual(core.version, version, 'the fixture must distinguish core and Rolldown versions'); + +const sharedDir = join(dirname(corePackage), 'dist/rolldown/shared'); +const files = readdirSync(sharedDir).filter((file) => /^binding-[\w-]+\.mjs$/.test(file)); +assert.equal(files.length, 1); +const bindingUrl = pathToFileURL(join(sharedDir, files[0])).href; +const bindingRequire = createRequire(bindingUrl); +const source = readFileSync(new URL(bindingUrl), 'utf8'); +const bindingExport = source.match(/\brequire_binding as (\w+)/)?.[1]; +assert.ok(bindingExport, 'the emitted loader must export its binding initializer'); + +// Use WebContainer's POSIX paths even when the snapshot runs on Windows. +// No real /tmp files or package-manager processes are used by the loader. +const baseDir = `/tmp/rolldown-${version}`; +const bindingEntry = `${baseDir}/node_modules/@rolldown/binding-wasm32-wasi/rolldown-binding.wasi.cjs`; +const wasmBinding = {}; +let cached = mode === 'cached'; +let downloads = 0; +let cacheChecks = 0; +const fakeFs = { + readFileSync(file, encoding) { + assert.equal(file, corePackage, 'only core metadata may be read'); + return readFileSync(file, encoding); + }, + existsSync(file) { + assert.equal(file, bindingEntry); + cacheChecks++; + return cached; + }, + rmSync(dir, options) { + assert.equal(dir, baseDir); + assert.deepEqual({ ...options }, { recursive: true, force: true }); + assert.equal(cached, false); + }, + mkdirSync(dir, options) { + assert.equal(dir, baseDir); + assert.deepEqual({ ...options }, { recursive: true }); + }, +}; +const fakeChildProcess = { + execFileSync(command, args, options) { + assert.equal(command, 'pnpm'); + assert.deepEqual(Array.from(args), ['i', `@rolldown/binding-wasm32-wasi@${version}`]); + assert.equal(options.cwd, baseDir); + assert.equal(options.stdio, 'inherit'); + assert.equal(cached, false); + downloads++; + cached = true; + console.log('Installer selected the bundled Rolldown version'); + }, +}; + +function missingModule(specifier) { + return Object.assign(new Error(`Cannot find module '${specifier}'`), { + code: 'MODULE_NOT_FOUND', + }); +} + +function unavailableBinding(specifier) { + return ( + specifier.endsWith('.node') || + specifier.includes('binding-wasm32-wasi') || + specifier.endsWith('.wasi.cjs') + ); +} + +const isolatedRequire = Object.assign( + (specifier) => { + if (specifier === 'fs' || specifier === 'node:fs') return fakeFs; + if (specifier === 'child_process' || specifier === 'node:child_process') return fakeChildProcess; + if (specifier === bindingEntry) { + assert.equal(cached, true, 'the WASM binding must be cached or installed first'); + return wasmBinding; + } + if (unavailableBinding(specifier)) throw missingModule(specifier); + return bindingRequire(specifier); + }, + { + resolve(specifier) { + if (unavailableBinding(specifier)) throw missingModule(specifier); + // Keep real Node resolution for metadata: the old rolldown/package.json + // lookup must fail in this packed installation, not in a mocked resolver. + return bindingRequire.resolve(specifier); + }, + }, +); + +const context = createContext({ + console: { + log(message) { + // The installer and cache assertions above check the exact version. + console.log(message.replaceAll(`@${version}`, '@')); + }, + }, + process: { + env: { NAPI_RS_NATIVE_LIBRARY_PATH: '/unavailable-native-binding.node' }, + versions: { webcontainer: '1' }, + platform: 'linux', + arch: 'x64', + }, +}); +const loader = new SourceTextModule(source, { + context, + identifier: bindingUrl, + initializeImportMeta(meta) { + meta.url = bindingUrl; + }, +}); +await loader.link((specifier) => { + assert.equal(specifier, 'node:module'); + return new SyntheticModule( + ['createRequire'], + function () { + this.setExport('createRequire', (url) => { + assert.equal(url, bindingUrl); + return isolatedRequire; + }); + }, + { context }, + ); +}); +await loader.evaluate(); + +console.log('Standalone rolldown: missing'); +console.log(`WASM cache: ${mode}`); +const binding = loader.namespace[bindingExport](); +assert.equal(binding, wasmBinding); +assert.equal(binding.__napiBindingTarget, 'wasm32-wasi'); +assert.equal(cacheChecks, 1); +assert.equal(downloads, mode === 'missing' ? 1 : 0); +console.log('Loaded the WebContainer WASM binding'); +console.log(`Downloads: ${downloads}`); diff --git a/packages/core/BUNDLING.md b/packages/core/BUNDLING.md index 44defda08d..6a024b268b 100644 --- a/packages/core/BUNDLING.md +++ b/packages/core/BUNDLING.md @@ -138,6 +138,8 @@ The snapshot allows CLI branding, embedded package metadata, and the browser run Native `@rolldown/binding-*` references have separate rewrite checks, described below. +The `rolldown_webcontainer_fallback` CLI snapshot executes the packed binding loader in a simulated WebContainer. It checks fresh and cached WASM bindings without a standalone `rolldown` package. The fixture uses real Node metadata resolution and a fake installer, and asserts that both paths use the bundled Rolldown version. + ### Release Build: Native Binding Rewriting During release builds (`RELEASE_BUILD=1`), `bundleRolldown()` rewrites Rolldown's native binding requires through `build-support/rewrite-rolldown-binding.ts`. For every platform in the CLI's `napi.targets` (mapped to napi platform suffixes with `parseTriple`), the loader's `@rolldown/binding-` requires become `@voidzero-dev/vite-plus-`, and that branch's version guard switches from the Rolldown version to core's version, which is what the Vite+ platform packages are published as.