Skip to content

Commit 8cd846c

Browse files
authored
fix(ssr): crash on circular import (#14441)
1 parent d82e8b1 commit 8cd846c

File tree

7 files changed

+52
-17
lines changed

7 files changed

+52
-17
lines changed

packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ test('export * from', async () => {
104104
).toMatchInlineSnapshot(`
105105
"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue");
106106
__vite_ssr_exportAll__(__vite_ssr_import_0__);
107+
107108
const __vite_ssr_import_1__ = await __vite_ssr_import__("react");
108109
__vite_ssr_exportAll__(__vite_ssr_import_1__);
109-
110110
"
111111
`)
112112
})
@@ -964,14 +964,14 @@ console.log(foo + 2)
964964
`),
965965
).toMatchInlineSnapshot(`
966966
"const __vite_ssr_import_0__ = await __vite_ssr_import__("./foo", {"importedNames":["foo"]});
967-
const __vite_ssr_import_1__ = await __vite_ssr_import__("./a");
968-
__vite_ssr_exportAll__(__vite_ssr_import_1__);
969-
const __vite_ssr_import_2__ = await __vite_ssr_import__("./b");
970-
__vite_ssr_exportAll__(__vite_ssr_import_2__);
971967
972968
console.log(__vite_ssr_import_0__.foo + 1)
969+
const __vite_ssr_import_1__ = await __vite_ssr_import__("./a");
970+
__vite_ssr_exportAll__(__vite_ssr_import_1__);
973971
974972
973+
const __vite_ssr_import_2__ = await __vite_ssr_import__("./b");
974+
__vite_ssr_exportAll__(__vite_ssr_import_2__);
975975
976976
console.log(__vite_ssr_import_0__.foo + 2)
977977
"

packages/vite/src/node/ssr/ssrTransform.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,11 @@ async function ssrTransformScript(
9494
// hoist at the start of the file, after the hashbang
9595
const hoistIndex = code.match(hashbangRE)?.[0].length ?? 0
9696

97-
function defineImport(source: string, metadata?: DefineImportMetadata) {
97+
function defineImport(
98+
index: number,
99+
source: string,
100+
metadata?: DefineImportMetadata,
101+
) {
98102
deps.add(source)
99103
const importId = `__vite_ssr_import_${uid++}__`
100104

@@ -110,7 +114,7 @@ async function ssrTransformScript(
110114
// There will be an error if the module is called before it is imported,
111115
// so the module import statement is hoisted to the top
112116
s.appendLeft(
113-
hoistIndex,
117+
index,
114118
`const ${importId} = await ${ssrImportKey}(${JSON.stringify(
115119
source,
116120
)}${metadataStr});\n`,
@@ -132,7 +136,7 @@ async function ssrTransformScript(
132136
// import { baz } from 'foo' --> baz -> __import_foo__.baz
133137
// import * as ok from 'foo' --> ok -> __import_foo__
134138
if (node.type === 'ImportDeclaration') {
135-
const importId = defineImport(node.source.value as string, {
139+
const importId = defineImport(hoistIndex, node.source.value as string, {
136140
importedNames: node.specifiers
137141
.map((s) => {
138142
if (s.type === 'ImportSpecifier') return s.imported.name
@@ -182,13 +186,16 @@ async function ssrTransformScript(
182186
s.remove(node.start, node.end)
183187
if (node.source) {
184188
// export { foo, bar } from './foo'
185-
const importId = defineImport(node.source.value as string, {
186-
importedNames: node.specifiers.map((s) => s.local.name),
187-
})
188-
// hoist re-exports near the defined import so they are immediately exported
189+
const importId = defineImport(
190+
node.start,
191+
node.source.value as string,
192+
{
193+
importedNames: node.specifiers.map((s) => s.local.name),
194+
},
195+
)
189196
for (const spec of node.specifiers) {
190197
defineExport(
191-
hoistIndex,
198+
node.start,
192199
spec.exported.name,
193200
`${importId}.${spec.local.name}`,
194201
)
@@ -234,12 +241,11 @@ async function ssrTransformScript(
234241
// export * from './foo'
235242
if (node.type === 'ExportAllDeclaration') {
236243
s.remove(node.start, node.end)
237-
const importId = defineImport(node.source.value as string)
238-
// hoist re-exports near the defined import so they are immediately exported
244+
const importId = defineImport(node.start, node.source.value as string)
239245
if (node.exported) {
240-
defineExport(hoistIndex, node.exported.name, `${importId}`)
246+
defineExport(node.start, node.exported.name, `${importId}`)
241247
} else {
242-
s.appendLeft(hoistIndex, `${ssrExportAllKey}(${importId});\n`)
248+
s.appendLeft(node.start, `${ssrExportAllKey}(${importId});\n`)
243249
}
244250
}
245251
}

playground/ssr/__tests__/ssr.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ test(`circular dependencies modules doesn't throw`, async () => {
1212
)
1313
})
1414

15+
test(`circular import doesn't throw`, async () => {
16+
await page.goto(`${url}/circular-import`)
17+
18+
expect(await page.textContent('.circ-import')).toMatchInlineSnapshot(
19+
'"A is: __A__"',
20+
)
21+
})
22+
1523
test(`deadlock doesn't happen`, async () => {
1624
await page.goto(`${url}/forked-deadlock`)
1725

playground/ssr/src/app.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { escapeHtml } from './utils'
33
const pathRenderers = {
44
'/': renderRoot,
55
'/circular-dep': renderCircularDep,
6+
'/circular-import': renderCircularImport,
67
'/forked-deadlock': renderForkedDeadlock,
78
}
89

@@ -34,6 +35,11 @@ async function renderCircularDep(rootDir) {
3435
return `<div class="circ-dep-init">${escapeHtml(getValueAB())}</div>`
3536
}
3637

38+
async function renderCircularImport(rootDir) {
39+
const { logA } = await import('./circular-import/index.js')
40+
return `<div class="circ-import">${escapeHtml(logA())}</div>`
41+
}
42+
3743
async function renderForkedDeadlock(rootDir) {
3844
const { commonModuleExport } = await import('./forked-deadlock/common-module')
3945
commonModuleExport()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { getB } from './b'
2+
3+
export const A = '__A__'
4+
5+
export const B = getB()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function getB() {
2+
return '__B__'
3+
}
4+
5+
export { A } from './a'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { A } from './b'
2+
3+
export function logA() {
4+
return `A is: ${A}`
5+
}

0 commit comments

Comments
 (0)