From c0c1c0359e022680a543f84780276a5227e926e5 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 2 Oct 2026 11:48:03 +0100 Subject: [PATCH 1/9] test(rsc): cover client entry facade with strictExecutionOrder --- .../e2e/strict-execution-order.test.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 packages/plugin-rsc/e2e/strict-execution-order.test.ts diff --git a/packages/plugin-rsc/e2e/strict-execution-order.test.ts b/packages/plugin-rsc/e2e/strict-execution-order.test.ts new file mode 100644 index 000000000..2cd28e9e0 --- /dev/null +++ b/packages/plugin-rsc/e2e/strict-execution-order.test.ts @@ -0,0 +1,99 @@ +import fs from 'node:fs' +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { setupInlineFixture, useFixture } from './fixture' +import { defineStarterTest } from './starter' + +// With `output.strictExecutionOrder`, rolldown keeps the client entry's +// modules out of the `index` chunk when they are shared with client reference +// chunks, leaving `index` as an empty facade that only imports and runs them. +// The facade has no `moduleIds`, so the client entry must not be resolved via +// module id -> chunk lookups. +test.describe('strict-execution-order', () => { + const root = 'examples/e2e/temp/strict-execution-order' + + test.beforeAll(async () => { + await setupInlineFixture({ + src: 'examples/starter', + dest: root, + files: { + 'vite.config.base.ts': { cp: 'vite.config.ts' }, + 'vite.config.ts': /* js */ ` + import { defineConfig, mergeConfig } from 'vite' + import baseConfig from './vite.config.base.ts' + + const overrideConfig = defineConfig({ + plugins: [ + { + name: 'test:record-client-entry-shape', + applyToEnvironment: (environment) => environment.name === 'client', + generateBundle(_options, bundle) { + const entry = Object.values(bundle).find( + (output) => output.type === 'chunk' && output.isEntry && output.name === 'index', + ) + this.emitFile({ + type: 'asset', + fileName: 'test-client-entry-shape.json', + source: JSON.stringify( + entry && { + fileName: entry.fileName, + moduleIds: entry.moduleIds, + imports: entry.imports, + }, + ), + }) + }, + }, + ], + environments: { + client: { + build: { + rollupOptions: { + output: { + strictExecutionOrder: true, + }, + }, + }, + }, + }, + }) + + export default mergeConfig(baseConfig, overrideConfig) + `, + }, + }) + }) + + test.describe('build', () => { + const f = useFixture({ root, mode: 'build' }) + defineStarterTest(f) + + test('client entry facade is the bootstrap entry', () => { + const entry = JSON.parse( + fs.readFileSync( + path.join(f.root, 'dist/client/test-client-entry-shape.json'), + 'utf-8', + ), + ) + // guard: the fixture must actually produce an empty facade entry + expect(entry.moduleIds).toEqual([]) + expect(entry.imports.length).toBeGreaterThan(0) + + const manifest = JSON.parse( + fs + .readFileSync( + path.join(f.root, 'dist/ssr/__vite_rsc_assets_manifest.js'), + 'utf-8', + ) + .slice('export default '.length), + ) + expect(manifest.clientEntryUrl).toBe(`/${entry.fileName}`) + expect(manifest.clientEntryDeps.js).toEqual( + expect.arrayContaining([ + `/${entry.fileName}`, + ...entry.imports.map((fileName: string) => `/${fileName}`), + ]), + ) + }) + }) +}) From 1d9ad27b8d29bb73828fad2cbb2a5d9d5627a705 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 2 Oct 2026 11:48:04 +0100 Subject: [PATCH 2/9] fix(rsc): resolve facade client entry with strictExecutionOrder --- packages/plugin-rsc/src/plugin.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 8710a66db..5a05f93b4 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1227,9 +1227,20 @@ export function createRpcClient(params) { // When customClientEntry is enabled, don't require "index" entry // and don't merge entry deps into client references if (!rscPluginOptions.customClientEntry) { - const entry = Object.values(assetDeps).find( - (v) => v.chunk.name === 'index' && v.chunk.isEntry, + // Look up the entry chunk directly rather than through `assetDeps`, + // which is keyed by module id. With `strictExecutionOrder`, + // rolldown can emit the entry as an empty facade (no `moduleIds`) + // that imports a shared chunk holding the entry's modules. + const entryChunk = Object.values(bundle).find( + (output): output is Rollup.OutputChunk => + output.type === 'chunk' && + output.name === 'index' && + output.isEntry, ) + const entry = entryChunk && { + chunk: entryChunk, + deps: collectAssetDepsInner(entryChunk.fileName, bundle), + } if (!entry) { throw new Error( `[vite-rsc] Client build must have an entry chunk named "index". Use 'customClientEntry' option to disable this requirement.`, From 97ba487c800d05029599de22f508199bd2d2d05e Mon Sep 17 00:00:00 2001 From: James Date: Fri, 2 Oct 2026 12:11:45 +0100 Subject: [PATCH 3/9] test(rsc): run strict execution order e2e on rolldown only --- packages/plugin-rsc/e2e/strict-execution-order.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/plugin-rsc/e2e/strict-execution-order.test.ts b/packages/plugin-rsc/e2e/strict-execution-order.test.ts index 2cd28e9e0..a35001101 100644 --- a/packages/plugin-rsc/e2e/strict-execution-order.test.ts +++ b/packages/plugin-rsc/e2e/strict-execution-order.test.ts @@ -1,6 +1,7 @@ import fs from 'node:fs' import path from 'node:path' import { expect, test } from '@playwright/test' +import * as vite from 'vite' import { setupInlineFixture, useFixture } from './fixture' import { defineStarterTest } from './starter' @@ -10,6 +11,8 @@ import { defineStarterTest } from './starter' // The facade has no `moduleIds`, so the client entry must not be resolved via // module id -> chunk lookups. test.describe('strict-execution-order', () => { + test.skip(!('rolldownVersion' in vite), 'rolldown only') + const root = 'examples/e2e/temp/strict-execution-order' test.beforeAll(async () => { From 419f905e8e5abecf3228bde1de965193639b11cb Mon Sep 17 00:00:00 2001 From: James Date: Fri, 2 Oct 2026 14:40:30 +0100 Subject: [PATCH 4/9] test(rsc): keep strict execution order e2e with starter edits Copying `examples/starter` from a separate spec file could race with the `starter.test.ts` HMR tests editing it on the other worker, building a fixture with `Client [edit] Counter`. Colocate it so the copy runs serially after those edits are reset. --- packages/plugin-rsc/e2e/starter.test.ts | 101 +++++++++++++++++ .../e2e/strict-execution-order.test.ts | 102 ------------------ 2 files changed, 101 insertions(+), 102 deletions(-) delete mode 100644 packages/plugin-rsc/e2e/strict-execution-order.test.ts diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index 3a75475e1..24da16462 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -1,5 +1,8 @@ +import fs from 'node:fs' +import path from 'node:path' import { expect, test } from '@playwright/test' import { x } from 'tinyexec' +import * as vite from 'vite' import { setupInlineFixture, useFixture, type Fixture } from './fixture' import { expectNoPageError, waitForHydration } from './helper' import { defineStarterTest } from './starter' @@ -188,3 +191,101 @@ test.describe('isolated build', () => { expect(result.exitCode).toBe(0) }) }) + +// With `output.strictExecutionOrder`, rolldown keeps the client entry's +// modules out of the `index` chunk when they are shared with client reference +// chunks, leaving `index` as an empty facade that only imports and runs them. +// The facade has no `moduleIds`, so the client entry must not be resolved via +// module id -> chunk lookups. +// Kept in this file so copying `examples/starter` never overlaps the HMR +// edits made by the dev tests above, which run serially in the same worker. +test.describe('strict-execution-order', () => { + test.skip(!('rolldownVersion' in vite), 'rolldown only') + + const root = 'examples/e2e/temp/strict-execution-order' + + test.beforeAll(async () => { + await setupInlineFixture({ + src: 'examples/starter', + dest: root, + files: { + 'vite.config.base.ts': { cp: 'vite.config.ts' }, + 'vite.config.ts': /* js */ ` + import { defineConfig, mergeConfig } from 'vite' + import baseConfig from './vite.config.base.ts' + + const overrideConfig = defineConfig({ + plugins: [ + { + name: 'test:record-client-entry-shape', + applyToEnvironment: (environment) => environment.name === 'client', + generateBundle(_options, bundle) { + const entry = Object.values(bundle).find( + (output) => output.type === 'chunk' && output.isEntry && output.name === 'index', + ) + this.emitFile({ + type: 'asset', + fileName: 'test-client-entry-shape.json', + source: JSON.stringify( + entry && { + fileName: entry.fileName, + moduleIds: entry.moduleIds, + imports: entry.imports, + }, + ), + }) + }, + }, + ], + environments: { + client: { + build: { + rollupOptions: { + output: { + strictExecutionOrder: true, + }, + }, + }, + }, + }, + }) + + export default mergeConfig(baseConfig, overrideConfig) + `, + }, + }) + }) + + test.describe('build', () => { + const f = useFixture({ root, mode: 'build' }) + defineStarterTest(f) + + test('client entry facade is the bootstrap entry', () => { + const entry = JSON.parse( + fs.readFileSync( + path.join(f.root, 'dist/client/test-client-entry-shape.json'), + 'utf-8', + ), + ) + // guard: the fixture must actually produce an empty facade entry + expect(entry.moduleIds).toEqual([]) + expect(entry.imports.length).toBeGreaterThan(0) + + const manifest = JSON.parse( + fs + .readFileSync( + path.join(f.root, 'dist/ssr/__vite_rsc_assets_manifest.js'), + 'utf-8', + ) + .slice('export default '.length), + ) + expect(manifest.clientEntryUrl).toBe(`/${entry.fileName}`) + expect(manifest.clientEntryDeps.js).toEqual( + expect.arrayContaining([ + `/${entry.fileName}`, + ...entry.imports.map((fileName: string) => `/${fileName}`), + ]), + ) + }) + }) +}) diff --git a/packages/plugin-rsc/e2e/strict-execution-order.test.ts b/packages/plugin-rsc/e2e/strict-execution-order.test.ts deleted file mode 100644 index a35001101..000000000 --- a/packages/plugin-rsc/e2e/strict-execution-order.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { expect, test } from '@playwright/test' -import * as vite from 'vite' -import { setupInlineFixture, useFixture } from './fixture' -import { defineStarterTest } from './starter' - -// With `output.strictExecutionOrder`, rolldown keeps the client entry's -// modules out of the `index` chunk when they are shared with client reference -// chunks, leaving `index` as an empty facade that only imports and runs them. -// The facade has no `moduleIds`, so the client entry must not be resolved via -// module id -> chunk lookups. -test.describe('strict-execution-order', () => { - test.skip(!('rolldownVersion' in vite), 'rolldown only') - - const root = 'examples/e2e/temp/strict-execution-order' - - test.beforeAll(async () => { - await setupInlineFixture({ - src: 'examples/starter', - dest: root, - files: { - 'vite.config.base.ts': { cp: 'vite.config.ts' }, - 'vite.config.ts': /* js */ ` - import { defineConfig, mergeConfig } from 'vite' - import baseConfig from './vite.config.base.ts' - - const overrideConfig = defineConfig({ - plugins: [ - { - name: 'test:record-client-entry-shape', - applyToEnvironment: (environment) => environment.name === 'client', - generateBundle(_options, bundle) { - const entry = Object.values(bundle).find( - (output) => output.type === 'chunk' && output.isEntry && output.name === 'index', - ) - this.emitFile({ - type: 'asset', - fileName: 'test-client-entry-shape.json', - source: JSON.stringify( - entry && { - fileName: entry.fileName, - moduleIds: entry.moduleIds, - imports: entry.imports, - }, - ), - }) - }, - }, - ], - environments: { - client: { - build: { - rollupOptions: { - output: { - strictExecutionOrder: true, - }, - }, - }, - }, - }, - }) - - export default mergeConfig(baseConfig, overrideConfig) - `, - }, - }) - }) - - test.describe('build', () => { - const f = useFixture({ root, mode: 'build' }) - defineStarterTest(f) - - test('client entry facade is the bootstrap entry', () => { - const entry = JSON.parse( - fs.readFileSync( - path.join(f.root, 'dist/client/test-client-entry-shape.json'), - 'utf-8', - ), - ) - // guard: the fixture must actually produce an empty facade entry - expect(entry.moduleIds).toEqual([]) - expect(entry.imports.length).toBeGreaterThan(0) - - const manifest = JSON.parse( - fs - .readFileSync( - path.join(f.root, 'dist/ssr/__vite_rsc_assets_manifest.js'), - 'utf-8', - ) - .slice('export default '.length), - ) - expect(manifest.clientEntryUrl).toBe(`/${entry.fileName}`) - expect(manifest.clientEntryDeps.js).toEqual( - expect.arrayContaining([ - `/${entry.fileName}`, - ...entry.imports.map((fileName: string) => `/${fileName}`), - ]), - ) - }) - }) -}) From 62fb48a0b5ab90ac4c8032a4a4d7ea3da7a34fe2 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 2 Oct 2026 15:50:44 +0100 Subject: [PATCH 5/9] test(rsc): fix HMR edit races in e2e - Space out editor writes to the same file. Vite's chokidar watcher drops a `change` event within 50ms of the previous one, so a fast HMR round trip could swallow `editor.reset()` (e.g. `dev-no-ssr > client hmr`). - Wait for the `rsc:update` refetch before reloading in `use-cache-persistent`, so the reload neither aborts it (unhandled `Load failed` in WebKit) nor receives the late update before hydration (`setPayload is not a function`). --- packages/plugin-rsc/e2e/fixture.ts | 25 ++++++++++++++++--- .../e2e/use-cache-persistent.test.ts | 25 ++++++++++++++++--- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/plugin-rsc/e2e/fixture.ts b/packages/plugin-rsc/e2e/fixture.ts index 5835e57d9..9e90d888a 100644 --- a/packages/plugin-rsc/e2e/fixture.ts +++ b/packages/plugin-rsc/e2e/fixture.ts @@ -159,12 +159,29 @@ export function useFixture(options: { } } +// Vite's file watcher (chokidar) drops a `change` event that arrives within +// 50ms of the previous one for the same path. A fast HMR round trip can make +// an edit followed by a reset land inside that window, so Vite never sees the +// reset. Space out writes to the same file to keep every change observable. +const WRITE_INTERVAL_MS = 100 +const lastWriteTimes = new Map() + +function writeWatchedFile(filepath: string, content: string) { + const wait = + (lastWriteTimes.get(filepath) ?? 0) + WRITE_INTERVAL_MS - Date.now() + if (wait > 0) { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, wait) + } + fs.writeFileSync(filepath, content) + lastWriteTimes.set(filepath, Date.now()) +} + export function useCreateEditor(cwd: string) { const originalFiles: Record = {} test.afterAll(async () => { for (const [filepath, content] of Object.entries(originalFiles)) { - fs.writeFileSync(filepath, content) + writeWatchedFile(filepath, content) } }) @@ -179,13 +196,13 @@ export function useCreateEditor(cwd: string) { const next = editFn(current) assert(next !== current, 'Edit function did not change the content') current = next - fs.writeFileSync(filepath, next) + writeWatchedFile(filepath, next) }, reset(): void { - fs.writeFileSync(filepath, originalFiles[filepath]!) + writeWatchedFile(filepath, originalFiles[filepath]!) }, resave(): void { - fs.writeFileSync(filepath, current) + writeWatchedFile(filepath, current) }, } } diff --git a/packages/plugin-rsc/e2e/use-cache-persistent.test.ts b/packages/plugin-rsc/e2e/use-cache-persistent.test.ts index 186b4f72e..e30f400ad 100644 --- a/packages/plugin-rsc/e2e/use-cache-persistent.test.ts +++ b/packages/plugin-rsc/e2e/use-cache-persistent.test.ts @@ -112,7 +112,9 @@ function defineDevTests(f: Fixture) { await expect(executionCount).toHaveText('1') // Editing the cached function advances its module generation. - action.edit((code) => code.replace('body-v1', 'body-v2')) + await editServerModule(page, () => + action.edit((code) => code.replace('body-v1', 'body-v2')), + ) await page.reload() await waitForHydration(page) await expectResultAfterUpdate( @@ -123,7 +125,9 @@ function defineDevTests(f: Fixture) { ) // Editing a direct dependency invalidates the importing cache module. - direct.edit((code) => code.replace('direct-v1', 'direct-v2')) + await editServerModule(page, () => + direct.edit((code) => code.replace('direct-v1', 'direct-v2')), + ) await page.reload() await waitForHydration(page) await expectResultAfterUpdate( @@ -134,7 +138,9 @@ function defineDevTests(f: Fixture) { ) // Reverse-importer traversal also reaches transitive dependencies. - transitive.edit((code) => code.replace('transitive-v1', 'transitive-v2')) + await editServerModule(page, () => + transitive.edit((code) => code.replace('transitive-v1', 'transitive-v2')), + ) await page.reload() await waitForHydration(page) await expectResultAfterUpdate( @@ -159,6 +165,19 @@ function defineDevTests(f: Fixture) { }) } +// Editing a server module makes the page refetch its RSC payload through +// `rsc:update`. Wait for that refetch before reloading. Otherwise the reload +// can abort it (an unhandled `TypeError: Load failed` in WebKit), or the late +// update can reach the reloaded page before it has hydrated. +async function editServerModule(page: Page, edit: () => void) { + const refetch = page.waitForResponse( + (response) => + response.request().method() === 'GET' && response.url().includes('_.rsc'), + ) + edit() + await (await refetch).finished() +} + async function expectResultAfterUpdate( page: Page, form: Locator, From 81c5bdcf84c3f4faa7f1819116ab4a33d90465da Mon Sep 17 00:00:00 2001 From: James Date: Fri, 2 Oct 2026 16:39:17 +0100 Subject: [PATCH 6/9] test(rsc): allow 60s for isolated fixture installs `setupIsolatedFixture` runs `pnpm i` over the network inside a `beforeAll` hook. On macOS runners installs take 16-34s, and the `react-server-dom-webpack` fixture installs twice, so the hook kept hitting the default 30s timeout. --- packages/plugin-rsc/e2e/fixture.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/plugin-rsc/e2e/fixture.ts b/packages/plugin-rsc/e2e/fixture.ts index 9e90d888a..9e013e6b7 100644 --- a/packages/plugin-rsc/e2e/fixture.ts +++ b/packages/plugin-rsc/e2e/fixture.ts @@ -215,6 +215,10 @@ export async function setupIsolatedFixture(options: { dest: string overrides?: Record }) { + // `pnpm i` hits the network and can take over 30s on macOS runners, which + // would exceed the default timeout of the `beforeAll` hook calling this. + test.setTimeout(60_000) + // copy fixture fs.rmSync(options.dest, { recursive: true, force: true }) fs.cpSync(options.src, options.dest, { From 998de8646fd8e6f5d844d7aa730a2792197799a3 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:39:24 +0900 Subject: [PATCH 7/9] refactor(rsc): look up client entry via chunk-level asset deps Return the chunk-level map from `collectAssetDeps` alongside the module-id map, and find the `index` entry there. This reuses the deps already computed for every chunk instead of walking the entry again. Co-Authored-By: Claude Opus 5.5 --- packages/plugin-rsc/src/plugin.ts | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 5a05f93b4..848993609 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1195,7 +1195,7 @@ export function createRpcClient(params) { } const serverResources: Record = {} - const rscAssetDeps = collectAssetDeps(rscBundle) + const rscAssetDeps = collectAssetDeps(rscBundle).idToDeps for (const [id, meta] of Object.entries( manager.serverResourcesMetaMap, )) { @@ -1208,7 +1208,7 @@ export function createRpcClient(params) { ) } - const assetDeps = collectAssetDeps(bundle) + const { idToDeps: assetDeps, chunkToDeps } = collectAssetDeps(bundle) let clientEntryUrl: string | RuntimeAsset | undefined let clientEntryDeps: AssetDeps | undefined @@ -1227,30 +1227,24 @@ export function createRpcClient(params) { // When customClientEntry is enabled, don't require "index" entry // and don't merge entry deps into client references if (!rscPluginOptions.customClientEntry) { - // Look up the entry chunk directly rather than through `assetDeps`, - // which is keyed by module id. With `strictExecutionOrder`, - // rolldown can emit the entry as an empty facade (no `moduleIds`) - // that imports a shared chunk holding the entry's modules. - const entryChunk = Object.values(bundle).find( - (output): output is Rollup.OutputChunk => - output.type === 'chunk' && - output.name === 'index' && - output.isEntry, + // Look up the entry chunk at chunk level rather than through + // `assetDeps`, which is keyed by module id. The entry can be an + // empty facade (no `moduleIds`) that imports a shared chunk holding + // the entry's modules, e.g. with `strictExecutionOrder`. + const entry = [...chunkToDeps].find( + ([chunk]) => chunk.name === 'index' && chunk.isEntry, ) - const entry = entryChunk && { - chunk: entryChunk, - deps: collectAssetDepsInner(entryChunk.fileName, bundle), - } if (!entry) { throw new Error( `[vite-rsc] Client build must have an entry chunk named "index". Use 'customClientEntry' option to disable this requirement.`, ) } - clientEntryDeps = assetsURLOfDeps(entry.deps, manager) + const [entryChunk, entryDeps] = entry + clientEntryDeps = assetsURLOfDeps(entryDeps, manager) for (const [key, deps] of Object.entries(clientReferenceDeps)) { clientReferenceDeps[key] = mergeAssetDeps(deps, clientEntryDeps) } - clientEntryUrl = assetsURL(entry.chunk.fileName, manager) + clientEntryUrl = assetsURL(entryChunk.fileName, manager) } manager.buildAssetsManifest = { @@ -2319,7 +2313,7 @@ function collectAssetDeps(bundle: Rollup.OutputBundle) { idToDeps[id] = { chunk, deps } } } - return idToDeps + return { idToDeps, chunkToDeps } } function collectAssetDepsInner( From c7f316e762b5de1c08b48fb93dad54764b68ee40 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:47:11 +0900 Subject: [PATCH 8/9] test(rsc): drop bundle-shape assertions from strict execution order e2e The starter suite already fails on the client build without the fix, so it covers the regression. The removed test asserted rolldown's chunk shape and the internal assets manifest, which would break on bundler changes unrelated to the plugin. Co-Authored-By: Claude Opus 5.5 --- packages/plugin-rsc/e2e/starter.test.ts | 59 +------------------------ 1 file changed, 2 insertions(+), 57 deletions(-) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index 24da16462..de0ca6d9a 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -1,5 +1,3 @@ -import fs from 'node:fs' -import path from 'node:path' import { expect, test } from '@playwright/test' import { x } from 'tinyexec' import * as vite from 'vite' @@ -192,11 +190,8 @@ test.describe('isolated build', () => { }) }) -// With `output.strictExecutionOrder`, rolldown keeps the client entry's -// modules out of the `index` chunk when they are shared with client reference -// chunks, leaving `index` as an empty facade that only imports and runs them. -// The facade has no `moduleIds`, so the client entry must not be resolved via -// module id -> chunk lookups. +// With `output.strictExecutionOrder`, rolldown can emit the client `index` +// entry as an empty facade (no `moduleIds`) that imports a shared chunk. // Kept in this file so copying `examples/starter` never overlaps the HMR // edits made by the dev tests above, which run serially in the same worker. test.describe('strict-execution-order', () => { @@ -215,28 +210,6 @@ test.describe('strict-execution-order', () => { import baseConfig from './vite.config.base.ts' const overrideConfig = defineConfig({ - plugins: [ - { - name: 'test:record-client-entry-shape', - applyToEnvironment: (environment) => environment.name === 'client', - generateBundle(_options, bundle) { - const entry = Object.values(bundle).find( - (output) => output.type === 'chunk' && output.isEntry && output.name === 'index', - ) - this.emitFile({ - type: 'asset', - fileName: 'test-client-entry-shape.json', - source: JSON.stringify( - entry && { - fileName: entry.fileName, - moduleIds: entry.moduleIds, - imports: entry.imports, - }, - ), - }) - }, - }, - ], environments: { client: { build: { @@ -259,33 +232,5 @@ test.describe('strict-execution-order', () => { test.describe('build', () => { const f = useFixture({ root, mode: 'build' }) defineStarterTest(f) - - test('client entry facade is the bootstrap entry', () => { - const entry = JSON.parse( - fs.readFileSync( - path.join(f.root, 'dist/client/test-client-entry-shape.json'), - 'utf-8', - ), - ) - // guard: the fixture must actually produce an empty facade entry - expect(entry.moduleIds).toEqual([]) - expect(entry.imports.length).toBeGreaterThan(0) - - const manifest = JSON.parse( - fs - .readFileSync( - path.join(f.root, 'dist/ssr/__vite_rsc_assets_manifest.js'), - 'utf-8', - ) - .slice('export default '.length), - ) - expect(manifest.clientEntryUrl).toBe(`/${entry.fileName}`) - expect(manifest.clientEntryDeps.js).toEqual( - expect.arrayContaining([ - `/${entry.fileName}`, - ...entry.imports.map((fileName: string) => `/${fileName}`), - ]), - ) - }) }) }) From 808107d461349a708b210f659dab2dbefbe9eae8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:49:24 +0900 Subject: [PATCH 9/9] test(rsc): move strict execution order e2e to rolldown.test.ts Base the fixture on `examples/starter-extra` like other inline fixtures, so it no longer copies `examples/starter` while starter's dev tests edit it in place. `rolldown.test.ts` already has the same rolldown-only skip. Co-Authored-By: Claude Opus 5.5 --- packages/plugin-rsc/e2e/rolldown.test.ts | 41 +++++++++++++++++++++ packages/plugin-rsc/e2e/starter.test.ts | 46 ------------------------ 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/packages/plugin-rsc/e2e/rolldown.test.ts b/packages/plugin-rsc/e2e/rolldown.test.ts index d5b630d8a..8f5eb0066 100644 --- a/packages/plugin-rsc/e2e/rolldown.test.ts +++ b/packages/plugin-rsc/e2e/rolldown.test.ts @@ -29,3 +29,44 @@ test.describe('rolldownOptions', () => { defineStarterTest(f) }) }) + +// With `output.strictExecutionOrder`, rolldown can emit the client `index` +// entry as an empty facade (no `moduleIds`) that imports a shared chunk. +test.describe('strict-execution-order', () => { + test.skip(!('rolldownVersion' in vite), 'rolldown only') + + const root = 'examples/e2e/temp/strict-execution-order' + test.beforeAll(async () => { + await setupInlineFixture({ + src: 'examples/starter-extra', + dest: root, + files: { + 'vite.config.base.ts': { cp: 'vite.config.ts' }, + 'vite.config.ts': /* js */ ` + import { defineConfig, mergeConfig } from 'vite' + import baseConfig from './vite.config.base.ts' + + export default mergeConfig( + baseConfig, + defineConfig({ + environments: { + client: { + build: { + rollupOptions: { + output: { strictExecutionOrder: true }, + }, + }, + }, + }, + }), + ) + `, + }, + }) + }) + + test.describe('build', () => { + const f = useFixture({ root, mode: 'build' }) + defineStarterTest(f) + }) +}) diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index de0ca6d9a..3a75475e1 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -1,6 +1,5 @@ import { expect, test } from '@playwright/test' import { x } from 'tinyexec' -import * as vite from 'vite' import { setupInlineFixture, useFixture, type Fixture } from './fixture' import { expectNoPageError, waitForHydration } from './helper' import { defineStarterTest } from './starter' @@ -189,48 +188,3 @@ test.describe('isolated build', () => { expect(result.exitCode).toBe(0) }) }) - -// With `output.strictExecutionOrder`, rolldown can emit the client `index` -// entry as an empty facade (no `moduleIds`) that imports a shared chunk. -// Kept in this file so copying `examples/starter` never overlaps the HMR -// edits made by the dev tests above, which run serially in the same worker. -test.describe('strict-execution-order', () => { - test.skip(!('rolldownVersion' in vite), 'rolldown only') - - const root = 'examples/e2e/temp/strict-execution-order' - - test.beforeAll(async () => { - await setupInlineFixture({ - src: 'examples/starter', - dest: root, - files: { - 'vite.config.base.ts': { cp: 'vite.config.ts' }, - 'vite.config.ts': /* js */ ` - import { defineConfig, mergeConfig } from 'vite' - import baseConfig from './vite.config.base.ts' - - const overrideConfig = defineConfig({ - environments: { - client: { - build: { - rollupOptions: { - output: { - strictExecutionOrder: true, - }, - }, - }, - }, - }, - }) - - export default mergeConfig(baseConfig, overrideConfig) - `, - }, - }) - }) - - test.describe('build', () => { - const f = useFixture({ root, mode: 'build' }) - defineStarterTest(f) - }) -})