Skip to content

Commit 7ef25e7

Browse files
committed
feat(ssr): tolerate circular imports (vitejs#3950)
1 parent 024a2de commit 7ef25e7

18 files changed

+269
-91
lines changed

packages/playground/ssr-react/__tests__/ssr-react.spec.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { editFile, getColor, isBuild, untilUpdated } from '../../testUtils'
1+
import { editFile, untilUpdated } from '../../testUtils'
22
import { port } from './serve'
33
import fetch from 'node-fetch'
44

@@ -46,3 +46,10 @@ test('client navigation', async () => {
4646
)
4747
await untilUpdated(() => page.textContent('h1'), 'changed')
4848
})
49+
50+
test(`circular dependecies modules doesn't throw`, async () => {
51+
await page.goto(url)
52+
expect(await page.textContent('.circ-dep-init')).toMatch(
53+
'circ-dep-init-a circ-dep-init-b'
54+
)
55+
})
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { multiply } from './multiply'
2+
3+
export function add(a, b) {
4+
return a + b
5+
}
6+
7+
export function addAndMultiply(a, b, c) {
8+
return multiply(add(a, b), c)
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This test aim to find out wherever the modules with circular dependencies are correctly initialized
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './module-a'
2+
export { getValueAB } from './module-b'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const valueA = 'circ-dep-init-a'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { valueA } from './circular-dep-init'
2+
3+
export const valueB = 'circ-dep-init-b'
4+
export const valueAB = valueA.concat(` ${valueB}`)
5+
6+
export function getValueAB() {
7+
return valueAB
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
This test aim to check for a particular type of circular dependency that causes tricky deadlocks, **deadlocks with forked imports stack**
2+
3+
```
4+
A -> B means: B is imported by A and B has A in its stack
5+
A ... B means: A is waiting for B to ssrLoadModule()
6+
7+
H -> X ... Y
8+
H -> X -> Y ... B
9+
H -> A ... B
10+
H -> A -> B ... X
11+
```
12+
13+
### Forked deadlock description:
14+
15+
```
16+
[X] is waiting for [Y] to resolve
17+
↑ ↳ is waiting for [A] to resolve
18+
│ ↳ is waiting for [B] to resolve
19+
│ ↳ is waiting for [X] to resolve
20+
└────────────────────────────────────────────────────────────────────────┘
21+
```
22+
23+
This may seems a traditional deadlock, but the thing that makes this special is the import stack of each module:
24+
25+
```
26+
[X] stack:
27+
[H]
28+
```
29+
30+
```
31+
[Y] stack:
32+
[X]
33+
[H]
34+
```
35+
36+
```
37+
[A] stack:
38+
[H]
39+
```
40+
41+
```
42+
[B] stack:
43+
[A]
44+
[H]
45+
```
46+
47+
Even if `[X]` is imported by `[B]`, `[B]` is not in `[X]`'s stack because it's imported by `[H]` in first place then it's stack is only composed by `[H]`. `[H]` **forks** the imports **stack** and this make hard to be found.
48+
49+
### Fix description
50+
51+
Vite, when imports `[X]`, should check whether `[X]` is already pending and if it is, it must check that, when it was imported in first place, the stack of `[X]` doesn't have any module in common with the current module; in this case `[B]` has the module `[H]` is common with `[X]` and i can assume that a deadlock is going to happen.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { stuckModuleExport } from './stuck-module'
2+
import { deadlockfuseModuleExport } from './deadlock-fuse-module'
3+
4+
/**
5+
* module H
6+
*/
7+
export function commonModuleExport() {
8+
stuckModuleExport()
9+
deadlockfuseModuleExport()
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { fuseStuckBridgeModuleExport } from './fuse-stuck-bridge-module'
2+
3+
/**
4+
* module A
5+
*/
6+
export function deadlockfuseModuleExport() {
7+
fuseStuckBridgeModuleExport()
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { stuckModuleExport } from './stuck-module'
2+
3+
/**
4+
* module C
5+
*/
6+
export function fuseStuckBridgeModuleExport() {
7+
stuckModuleExport()
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { deadlockfuseModuleExport } from './deadlock-fuse-module'
2+
3+
/**
4+
* module Y
5+
*/
6+
export function middleModuleExport() {
7+
void deadlockfuseModuleExport
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { middleModuleExport } from './middle-module'
2+
3+
/**
4+
* module X
5+
*/
6+
export function stuckModuleExport() {
7+
middleModuleExport()
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { add } from './add'
2+
3+
export function multiply(a, b) {
4+
return a * b
5+
}
6+
7+
export function multiplyAndAdd(a, b, c) {
8+
return add(multiply(a, b), c)
9+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import { addAndMultiply } from '../add'
2+
import { multiplyAndAdd } from '../multiply'
3+
14
export default function About() {
2-
return <h1>About</h1>
5+
return (
6+
<>
7+
<h1>About</h1>
8+
<div>{addAndMultiply(1, 2, 3)}</div>
9+
<div>{multiplyAndAdd(1, 2, 3)}</div>
10+
</>
11+
)
312
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
import { addAndMultiply } from '../add'
2+
import { multiplyAndAdd } from '../multiply'
3+
import { commonModuleExport } from '../forked-deadlock/common-module'
4+
import { getValueAB } from '../circular-dep-init/circular-dep-init'
5+
16
export default function Home() {
2-
return <h1>Home</h1>
7+
commonModuleExport()
8+
9+
return (
10+
<>
11+
<h1>Home</h1>
12+
<div>{addAndMultiply(1, 2, 3)}</div>
13+
<div>{multiplyAndAdd(1, 2, 3)}</div>
14+
<div className="circ-dep-init">{getValueAB()}</div>
15+
</>
16+
)
317
}

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

+40-32
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ test('default import', async () => {
1111
)
1212
).code
1313
).toMatchInlineSnapshot(`
14-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
14+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
1515
console.log(__vite_ssr_import_0__.default.bar)"
1616
`)
1717
})
@@ -26,7 +26,7 @@ test('named import', async () => {
2626
)
2727
).code
2828
).toMatchInlineSnapshot(`
29-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
29+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
3030
function foo() { return __vite_ssr_import_0__.ref(0) }"
3131
`)
3232
})
@@ -41,7 +41,7 @@ test('namespace import', async () => {
4141
)
4242
).code
4343
).toMatchInlineSnapshot(`
44-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
44+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
4545
function foo() { return __vite_ssr_import_0__.ref(0) }"
4646
`)
4747
})
@@ -50,24 +50,24 @@ test('export function declaration', async () => {
5050
expect((await ssrTransform(`export function foo() {}`, null, null)).code)
5151
.toMatchInlineSnapshot(`
5252
"function foo() {}
53-
Object.defineProperty(__vite_ssr_exports__, \\"foo\\", { enumerable: true, configurable: true, get(){ return foo }})"
53+
Object.defineProperty(__vite_ssr_exports__, \\"foo\\", { enumerable: true, configurable: true, get(){ return foo }});"
5454
`)
5555
})
5656

5757
test('export class declaration', async () => {
5858
expect((await ssrTransform(`export class foo {}`, null, null)).code)
5959
.toMatchInlineSnapshot(`
6060
"class foo {}
61-
Object.defineProperty(__vite_ssr_exports__, \\"foo\\", { enumerable: true, configurable: true, get(){ return foo }})"
61+
Object.defineProperty(__vite_ssr_exports__, \\"foo\\", { enumerable: true, configurable: true, get(){ return foo }});"
6262
`)
6363
})
6464

6565
test('export var declaration', async () => {
6666
expect((await ssrTransform(`export const a = 1, b = 2`, null, null)).code)
6767
.toMatchInlineSnapshot(`
6868
"const a = 1, b = 2
69-
Object.defineProperty(__vite_ssr_exports__, \\"a\\", { enumerable: true, configurable: true, get(){ return a }})
70-
Object.defineProperty(__vite_ssr_exports__, \\"b\\", { enumerable: true, configurable: true, get(){ return b }})"
69+
Object.defineProperty(__vite_ssr_exports__, \\"a\\", { enumerable: true, configurable: true, get(){ return a }});
70+
Object.defineProperty(__vite_ssr_exports__, \\"b\\", { enumerable: true, configurable: true, get(){ return b }});"
7171
`)
7272
})
7373

@@ -77,8 +77,8 @@ test('export named', async () => {
7777
.code
7878
).toMatchInlineSnapshot(`
7979
"const a = 1, b = 2;
80-
Object.defineProperty(__vite_ssr_exports__, \\"a\\", { enumerable: true, configurable: true, get(){ return a }})
81-
Object.defineProperty(__vite_ssr_exports__, \\"c\\", { enumerable: true, configurable: true, get(){ return b }})"
80+
Object.defineProperty(__vite_ssr_exports__, \\"a\\", { enumerable: true, configurable: true, get(){ return a }});
81+
Object.defineProperty(__vite_ssr_exports__, \\"c\\", { enumerable: true, configurable: true, get(){ return b }});"
8282
`)
8383
})
8484

@@ -87,10 +87,10 @@ test('export named from', async () => {
8787
(await ssrTransform(`export { ref, computed as c } from 'vue'`, null, null))
8888
.code
8989
).toMatchInlineSnapshot(`
90-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
90+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
9191
92-
Object.defineProperty(__vite_ssr_exports__, \\"ref\\", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.ref }})
93-
Object.defineProperty(__vite_ssr_exports__, \\"c\\", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.computed }})"
92+
Object.defineProperty(__vite_ssr_exports__, \\"ref\\", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.ref }});
93+
Object.defineProperty(__vite_ssr_exports__, \\"c\\", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.computed }});"
9494
`)
9595
})
9696

@@ -104,27 +104,35 @@ test('named exports of imported binding', async () => {
104104
)
105105
).code
106106
).toMatchInlineSnapshot(`
107-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
107+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
108108
109-
Object.defineProperty(__vite_ssr_exports__, \\"createApp\\", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.createApp }})"
109+
Object.defineProperty(__vite_ssr_exports__, \\"createApp\\", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.createApp }});"
110110
`)
111111
})
112112

113113
test('export * from', async () => {
114-
expect((await ssrTransform(`export * from 'vue'`, null, null)).code)
115-
.toMatchInlineSnapshot(`
116-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
117-
118-
__vite_ssr_exportAll__(__vite_ssr_import_0__)"
114+
expect(
115+
(
116+
await ssrTransform(
117+
`export * from 'vue'\n` + `export * from 'react'`,
118+
null,
119+
null
120+
)
121+
).code
122+
).toMatchInlineSnapshot(`
123+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
124+
__vite_ssr_exportAll__(__vite_ssr_import_0__);
125+
const __vite_ssr_import_1__ = await __vite_ssr_import__(\\"react\\");
126+
__vite_ssr_exportAll__(__vite_ssr_import_1__);"
119127
`)
120128
})
121129

122130
test('export * as from', async () => {
123131
expect((await ssrTransform(`export * as foo from 'vue'`, null, null)).code)
124132
.toMatchInlineSnapshot(`
125-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
133+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
126134
127-
Object.defineProperty(__vite_ssr_exports__, \\"foo\\", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ }})"
135+
Object.defineProperty(__vite_ssr_exports__, \\"foo\\", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ }});"
128136
`)
129137
})
130138

@@ -146,7 +154,7 @@ test('dynamic import', async () => {
146154
.code
147155
).toMatchInlineSnapshot(`
148156
"const i = () => __vite_ssr_dynamic_import__('./foo')
149-
Object.defineProperty(__vite_ssr_exports__, \\"i\\", { enumerable: true, configurable: true, get(){ return i }})"
157+
Object.defineProperty(__vite_ssr_exports__, \\"i\\", { enumerable: true, configurable: true, get(){ return i }});"
150158
`)
151159
})
152160

@@ -160,7 +168,7 @@ test('do not rewrite method definition', async () => {
160168
)
161169
).code
162170
).toMatchInlineSnapshot(`
163-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
171+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
164172
class A { fn() { __vite_ssr_import_0__.fn() } }"
165173
`)
166174
})
@@ -175,7 +183,7 @@ test('do not rewrite catch clause', async () => {
175183
)
176184
).code
177185
).toMatchInlineSnapshot(`
178-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"./dependency\\")
186+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"./dependency\\");
179187
try {} catch(error) {}"
180188
`)
181189
})
@@ -191,7 +199,7 @@ test('should declare variable for imported super class', async () => {
191199
)
192200
).code
193201
).toMatchInlineSnapshot(`
194-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"./dependency\\")
202+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"./dependency\\");
195203
const Foo = __vite_ssr_import_0__.Foo;
196204
class A extends Foo {}"
197205
`)
@@ -209,12 +217,12 @@ test('should declare variable for imported super class', async () => {
209217
)
210218
).code
211219
).toMatchInlineSnapshot(`
212-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"./dependency\\")
220+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"./dependency\\");
213221
const Foo = __vite_ssr_import_0__.Foo;
214222
class A extends Foo {}
215223
class B extends Foo {}
216-
Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: A })
217-
Object.defineProperty(__vite_ssr_exports__, \\"B\\", { enumerable: true, configurable: true, get(){ return B }})"
224+
Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: A });
225+
Object.defineProperty(__vite_ssr_exports__, \\"B\\", { enumerable: true, configurable: true, get(){ return B }});"
218226
`)
219227
})
220228

@@ -246,7 +254,7 @@ test('should handle default export variants', async () => {
246254
).toMatchInlineSnapshot(`
247255
"function foo() {}
248256
foo.prototype = Object.prototype;
249-
Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: foo })"
257+
Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: foo });"
250258
`)
251259
// default named classes
252260
expect(
@@ -260,8 +268,8 @@ test('should handle default export variants', async () => {
260268
).toMatchInlineSnapshot(`
261269
"class A {}
262270
class B extends A {}
263-
Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: A })
264-
Object.defineProperty(__vite_ssr_exports__, \\"B\\", { enumerable: true, configurable: true, get(){ return B }})"
271+
Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: A });
272+
Object.defineProperty(__vite_ssr_exports__, \\"B\\", { enumerable: true, configurable: true, get(){ return B }});"
265273
`)
266274
})
267275

@@ -288,7 +296,7 @@ test('overwrite bindings', async () => {
288296
)
289297
).code
290298
).toMatchInlineSnapshot(`
291-
"const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
299+
"const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
292300
const a = { inject: __vite_ssr_import_0__.inject }
293301
const b = { test: __vite_ssr_import_0__.inject }
294302
function c() { const { test: inject } = { test: true }; console.log(inject) }

0 commit comments

Comments
 (0)