From 8cd846cdbf5cc3214e6a32accf31a187726a23cd Mon Sep 17 00:00:00 2001 From: Dunqing Date: Wed, 13 Mar 2024 15:03:55 +0800 Subject: [PATCH] fix(ssr): crash on circular import (#14441) --- .../node/ssr/__tests__/ssrTransform.spec.ts | 10 +++---- packages/vite/src/node/ssr/ssrTransform.ts | 30 +++++++++++-------- playground/ssr/__tests__/ssr.spec.ts | 8 +++++ playground/ssr/src/app.js | 6 ++++ playground/ssr/src/circular-import/a.js | 5 ++++ playground/ssr/src/circular-import/b.js | 5 ++++ playground/ssr/src/circular-import/index.js | 5 ++++ 7 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 playground/ssr/src/circular-import/a.js create mode 100644 playground/ssr/src/circular-import/b.js create mode 100644 playground/ssr/src/circular-import/index.js diff --git a/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts b/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts index 4935c13c9d6b2f..5c99e5cab3ecdb 100644 --- a/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts +++ b/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts @@ -104,9 +104,9 @@ test('export * from', async () => { ).toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue"); __vite_ssr_exportAll__(__vite_ssr_import_0__); + const __vite_ssr_import_1__ = await __vite_ssr_import__("react"); __vite_ssr_exportAll__(__vite_ssr_import_1__); - " `) }) @@ -964,14 +964,14 @@ console.log(foo + 2) `), ).toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("./foo", {"importedNames":["foo"]}); - const __vite_ssr_import_1__ = await __vite_ssr_import__("./a"); - __vite_ssr_exportAll__(__vite_ssr_import_1__); - const __vite_ssr_import_2__ = await __vite_ssr_import__("./b"); - __vite_ssr_exportAll__(__vite_ssr_import_2__); console.log(__vite_ssr_import_0__.foo + 1) + const __vite_ssr_import_1__ = await __vite_ssr_import__("./a"); + __vite_ssr_exportAll__(__vite_ssr_import_1__); + const __vite_ssr_import_2__ = await __vite_ssr_import__("./b"); + __vite_ssr_exportAll__(__vite_ssr_import_2__); console.log(__vite_ssr_import_0__.foo + 2) " diff --git a/packages/vite/src/node/ssr/ssrTransform.ts b/packages/vite/src/node/ssr/ssrTransform.ts index 0c22577c888155..c3800a48a8d138 100644 --- a/packages/vite/src/node/ssr/ssrTransform.ts +++ b/packages/vite/src/node/ssr/ssrTransform.ts @@ -94,7 +94,11 @@ async function ssrTransformScript( // hoist at the start of the file, after the hashbang const hoistIndex = code.match(hashbangRE)?.[0].length ?? 0 - function defineImport(source: string, metadata?: DefineImportMetadata) { + function defineImport( + index: number, + source: string, + metadata?: DefineImportMetadata, + ) { deps.add(source) const importId = `__vite_ssr_import_${uid++}__` @@ -110,7 +114,7 @@ async function ssrTransformScript( // There will be an error if the module is called before it is imported, // so the module import statement is hoisted to the top s.appendLeft( - hoistIndex, + index, `const ${importId} = await ${ssrImportKey}(${JSON.stringify( source, )}${metadataStr});\n`, @@ -132,7 +136,7 @@ async function ssrTransformScript( // import { baz } from 'foo' --> baz -> __import_foo__.baz // import * as ok from 'foo' --> ok -> __import_foo__ if (node.type === 'ImportDeclaration') { - const importId = defineImport(node.source.value as string, { + const importId = defineImport(hoistIndex, node.source.value as string, { importedNames: node.specifiers .map((s) => { if (s.type === 'ImportSpecifier') return s.imported.name @@ -182,13 +186,16 @@ async function ssrTransformScript( s.remove(node.start, node.end) if (node.source) { // export { foo, bar } from './foo' - const importId = defineImport(node.source.value as string, { - importedNames: node.specifiers.map((s) => s.local.name), - }) - // hoist re-exports near the defined import so they are immediately exported + const importId = defineImport( + node.start, + node.source.value as string, + { + importedNames: node.specifiers.map((s) => s.local.name), + }, + ) for (const spec of node.specifiers) { defineExport( - hoistIndex, + node.start, spec.exported.name, `${importId}.${spec.local.name}`, ) @@ -234,12 +241,11 @@ async function ssrTransformScript( // export * from './foo' if (node.type === 'ExportAllDeclaration') { s.remove(node.start, node.end) - const importId = defineImport(node.source.value as string) - // hoist re-exports near the defined import so they are immediately exported + const importId = defineImport(node.start, node.source.value as string) if (node.exported) { - defineExport(hoistIndex, node.exported.name, `${importId}`) + defineExport(node.start, node.exported.name, `${importId}`) } else { - s.appendLeft(hoistIndex, `${ssrExportAllKey}(${importId});\n`) + s.appendLeft(node.start, `${ssrExportAllKey}(${importId});\n`) } } } diff --git a/playground/ssr/__tests__/ssr.spec.ts b/playground/ssr/__tests__/ssr.spec.ts index 812e9323aad289..6e5d227070db29 100644 --- a/playground/ssr/__tests__/ssr.spec.ts +++ b/playground/ssr/__tests__/ssr.spec.ts @@ -12,6 +12,14 @@ test(`circular dependencies modules doesn't throw`, async () => { ) }) +test(`circular import doesn't throw`, async () => { + await page.goto(`${url}/circular-import`) + + expect(await page.textContent('.circ-import')).toMatchInlineSnapshot( + '"A is: __A__"', + ) +}) + test(`deadlock doesn't happen`, async () => { await page.goto(`${url}/forked-deadlock`) diff --git a/playground/ssr/src/app.js b/playground/ssr/src/app.js index 5e10dfe45937e3..b151504d973401 100644 --- a/playground/ssr/src/app.js +++ b/playground/ssr/src/app.js @@ -3,6 +3,7 @@ import { escapeHtml } from './utils' const pathRenderers = { '/': renderRoot, '/circular-dep': renderCircularDep, + '/circular-import': renderCircularImport, '/forked-deadlock': renderForkedDeadlock, } @@ -34,6 +35,11 @@ async function renderCircularDep(rootDir) { return `
${escapeHtml(getValueAB())}
` } +async function renderCircularImport(rootDir) { + const { logA } = await import('./circular-import/index.js') + return `
${escapeHtml(logA())}
` +} + async function renderForkedDeadlock(rootDir) { const { commonModuleExport } = await import('./forked-deadlock/common-module') commonModuleExport() diff --git a/playground/ssr/src/circular-import/a.js b/playground/ssr/src/circular-import/a.js new file mode 100644 index 00000000000000..00c8645fd78b12 --- /dev/null +++ b/playground/ssr/src/circular-import/a.js @@ -0,0 +1,5 @@ +import { getB } from './b' + +export const A = '__A__' + +export const B = getB() diff --git a/playground/ssr/src/circular-import/b.js b/playground/ssr/src/circular-import/b.js new file mode 100644 index 00000000000000..c0e732b29e2035 --- /dev/null +++ b/playground/ssr/src/circular-import/b.js @@ -0,0 +1,5 @@ +export function getB() { + return '__B__' +} + +export { A } from './a' diff --git a/playground/ssr/src/circular-import/index.js b/playground/ssr/src/circular-import/index.js new file mode 100644 index 00000000000000..c9ec0fc50238b4 --- /dev/null +++ b/playground/ssr/src/circular-import/index.js @@ -0,0 +1,5 @@ +import { A } from './b' + +export function logA() { + return `A is: ${A}` +}