From e90ff686550c05a43f89c0ca36d2a689c7222bf5 Mon Sep 17 00:00:00 2001 From: xiachao Date: Tue, 25 Aug 2026 18:07:15 +0800 Subject: [PATCH] fix(ssr): throw on missing named exports in inlined modules Vite-transformed SSR modules skipped analyzeImportedModDifference, so import { missing } from a local ESM file succeeded while Node throws SyntaxError. Run the same check after the module finishes evaluating, and skip it on incomplete circular graphs. Co-authored-by: Cursor --- packages/vite/src/module-runner/runner.ts | 21 ++++++++++++++----- .../fixtures/esm-internal-existing.js | 3 +++ .../fixtures/esm-internal-non-existing.js | 3 +++ .../runtime/__tests__/server-runtime.spec.ts | 13 ++++++++++++ 4 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-internal-existing.js create mode 100644 packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-internal-non-existing.js diff --git a/packages/vite/src/module-runner/runner.ts b/packages/vite/src/module-runner/runner.ts index b975b0a436c0f9..b63f6071ef270b 100644 --- a/packages/vite/src/module-runner/runner.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -123,13 +123,24 @@ export class ModuleRunner { exports: Record, fetchResult: ResolvedResult, metadata?: SSRImportMetadata, + isCompleteModule = true, ) { - if (!('externalize' in fetchResult)) { + // Cyclic inlined graphs may return a partial exports object while the + // exporter is still evaluating. Named-export checks must wait until the + // module finished, matching Node (and avoiding false SyntaxErrors). + if (!isCompleteModule) { return exports } - const { url, type } = fetchResult - if (type !== 'module' && type !== 'commonjs') return exports - analyzeImportedModDifference(exports, url, type, metadata) + if ('externalize' in fetchResult) { + const { url, type } = fetchResult + if (type !== 'module' && type !== 'commonjs') return exports + analyzeImportedModDifference(exports, url, type, metadata) + return exports + } + // Vite-transformed modules are ESM. Previously this path skipped + // analyzeImportedModDifference entirely, so `import { missing }` from a + // local file did not throw (Node throws SyntaxError). + analyzeImportedModDifference(exports, fetchResult.url, 'module', metadata) return exports } @@ -185,7 +196,7 @@ export class ModuleRunner { mod.exports && (callstack.includes(moduleId) || this.isCircularRequest(mod, callstack)) ) { - return this.processImport(mod.exports, meta, metadata) + return this.processImport(mod.exports, meta, metadata, false) } return this.processImport(await mod.promise, meta, metadata) } diff --git a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-internal-existing.js b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-internal-existing.js new file mode 100644 index 00000000000000..69402f59187c65 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-internal-existing.js @@ -0,0 +1,3 @@ +import { test } from './simple.js' + +export const result = test diff --git a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-internal-non-existing.js b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-internal-non-existing.js new file mode 100644 index 00000000000000..a952325c57c814 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-internal-non-existing.js @@ -0,0 +1,3 @@ +import { nonExisting } from './simple.js' + +export const result = nonExisting diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts index 79b474ab66a6f0..b6c346bc7f1f1d 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts @@ -200,6 +200,19 @@ describe('module runner initialization', async () => { }) }) + it('importing inlined esm module checks exports', async ({ runner }) => { + await expect(() => + runner.import('/fixtures/esm-internal-non-existing.js'), + ).rejects.toThrowError( + `[vite] The requested module '/fixtures/simple.js' does not provide an export named 'nonExisting'`, + ) + await expect( + runner.import('/fixtures/esm-internal-existing.js'), + ).resolves.toMatchObject({ + result: 'I am initialized', + }) + }) + it("dynamic import doesn't produce duplicates", async ({ runner }) => { const mod = await runner.import('/fixtures/dynamic-import.js') const modules = await mod.initialize()