Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ssr): hoist export to handle cyclic import better #18983

Merged
merged 25 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5f8c6fd
fix(ssr): hoist export to handle cyclic import better
hi-ogawa Dec 17, 2024
b537976
chore: remove override temporarily
hi-ogawa Dec 17, 2024
3cc86da
wip: forget this one too for now
hi-ogawa Dec 17, 2024
ed79e4d
wip: workaround cyclic import
hi-ogawa Dec 17, 2024
95f47eb
chore: unused
hi-ogawa Dec 17, 2024
2245aaa
test: fix hmr-ssr
hi-ogawa Dec 18, 2024
a8c1d12
Merge branch 'main' into fix-ssr-transform-hoist-export
hi-ogawa Dec 18, 2024
c2f7d75
test: fix importing broken entries
hi-ogawa Dec 18, 2024
96f123a
test: tweak
hi-ogawa Dec 18, 2024
d5b2333
Merge branch 'main' into fix-ssr-transform-hoist-export
hi-ogawa Dec 25, 2024
d252b5c
fix: remove new line
hi-ogawa Dec 25, 2024
7904808
test: update
hi-ogawa Dec 25, 2024
45e74ac
chore: comment
hi-ogawa Dec 25, 2024
83a7a1f
test: test invalid cyclic
hi-ogawa Dec 25, 2024
af7de84
chore: comment
hi-ogawa Dec 25, 2024
b0b75eb
chore: comment
hi-ogawa Dec 25, 2024
958467a
test: simplify
hi-ogawa Dec 25, 2024
19f7533
test: add test in ssr playground
sapphi-red Jan 7, 2025
059d132
Merge branch 'main' into fix-ssr-transform-hoist-export
hi-ogawa Jan 8, 2025
677949c
test: hacky setupFiles
hi-ogawa Jan 10, 2025
a844104
chore: remove unused
hi-ogawa Jan 10, 2025
94a2d1f
Merge branch 'main' into fix-ssr-transform-hoist-export
hi-ogawa Jan 13, 2025
7d75d1f
Merge branch 'main' into fix-ssr-transform-hoist-export
hi-ogawa Mar 7, 2025
1d5110e
fix: give up preserving lines
hi-ogawa Mar 7, 2025
7e7b660
fix: tweak new lines
hi-ogawa Mar 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 103 additions & 77 deletions packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,59 +51,74 @@ test('namespace import', async () => {
})

test('export function declaration', async () => {
expect(await ssrTransformSimpleCode(`export function foo() {}`))
.toMatchInlineSnapshot(`
"function foo() {}
Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return foo }});"
`)
expect(
await ssrTransformSimpleCode(`export function foo() {}`),
).toMatchInlineSnapshot(
`
"Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return foo }});
function foo() {}"
`,
)
})

test('export class declaration', async () => {
expect(await ssrTransformSimpleCode(`export class foo {}`))
.toMatchInlineSnapshot(`
"class foo {}
Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return foo }});"
`)
expect(
await ssrTransformSimpleCode(`export class foo {}`),
).toMatchInlineSnapshot(
`
"Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return foo }});
class foo {}"
`,
)
})

test('export var declaration', async () => {
expect(await ssrTransformSimpleCode(`export const a = 1, b = 2`))
.toMatchInlineSnapshot(`
"const a = 1, b = 2
Object.defineProperty(__vite_ssr_exports__, "a", { enumerable: true, configurable: true, get(){ return a }});
Object.defineProperty(__vite_ssr_exports__, "b", { enumerable: true, configurable: true, get(){ return b }});"
`)
expect(
await ssrTransformSimpleCode(`export const a = 1, b = 2`),
).toMatchInlineSnapshot(
`
"Object.defineProperty(__vite_ssr_exports__, "a", { enumerable: true, configurable: true, get(){ return a }});
Object.defineProperty(__vite_ssr_exports__, "b", { enumerable: true, configurable: true, get(){ return b }});
const a = 1, b = 2"
`,
)
})

test('export named', async () => {
expect(
await ssrTransformSimpleCode(`const a = 1, b = 2; export { a, b as c }`),
).toMatchInlineSnapshot(`
"const a = 1, b = 2;
Object.defineProperty(__vite_ssr_exports__, "a", { enumerable: true, configurable: true, get(){ return a }});
Object.defineProperty(__vite_ssr_exports__, "c", { enumerable: true, configurable: true, get(){ return b }});"
`)
).toMatchInlineSnapshot(
`
"Object.defineProperty(__vite_ssr_exports__, "a", { enumerable: true, configurable: true, get(){ return a }});
Object.defineProperty(__vite_ssr_exports__, "c", { enumerable: true, configurable: true, get(){ return b }});
const a = 1, b = 2; "
`,
)
})

test('export named from', async () => {
expect(
await ssrTransformSimpleCode(`export { ref, computed as c } from 'vue'`),
).toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["ref","computed"]});
Object.defineProperty(__vite_ssr_exports__, "ref", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.ref }});
Object.defineProperty(__vite_ssr_exports__, "c", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.computed }});"
`)
).toMatchInlineSnapshot(
`
"Object.defineProperty(__vite_ssr_exports__, "ref", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.ref }});
Object.defineProperty(__vite_ssr_exports__, "c", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.computed }});
const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["ref","computed"]});"
`,
)
})

test('named exports of imported binding', async () => {
expect(
await ssrTransformSimpleCode(
`import {createApp} from 'vue';export {createApp}`,
),
).toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["createApp"]});
Object.defineProperty(__vite_ssr_exports__, "createApp", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.createApp }});"
`)
).toMatchInlineSnapshot(
`
"Object.defineProperty(__vite_ssr_exports__, "createApp", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.createApp }});
const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["createApp"]});"
`,
)
})

test('export * from', async () => {
Expand All @@ -120,11 +135,14 @@ test('export * from', async () => {
})

test('export * as from', async () => {
expect(await ssrTransformSimpleCode(`export * as foo from 'vue'`))
.toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue");
Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ }});"
`)
expect(
await ssrTransformSimpleCode(`export * as foo from 'vue'`),
).toMatchInlineSnapshot(
`
"Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ }});
const __vite_ssr_import_0__ = await __vite_ssr_import__("vue");"
`,
)
})

test('re-export by imported name', async () => {
Expand All @@ -134,9 +152,9 @@ import * as foo from 'foo'
export * as foo from 'foo'
`),
).toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("foo");
"Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_1__ }});
const __vite_ssr_import_0__ = await __vite_ssr_import__("foo");
const __vite_ssr_import_1__ = await __vite_ssr_import__("foo");
Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_1__ }});
"
`)

Expand All @@ -146,54 +164,60 @@ import { foo } from 'foo'
export { foo } from 'foo'
`),
).toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["foo"]});
const __vite_ssr_import_1__ = await __vite_ssr_import__("foo", {"importedNames":["foo"]});
Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_1__.foo }});
"
`)
"Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_1__.foo }});
const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["foo"]});
const __vite_ssr_import_1__ = await __vite_ssr_import__("foo", {"importedNames":["foo"]});
"
`)

expect(
await ssrTransformSimpleCode(`\
import { foo } from 'foo'
export { foo as foo } from 'foo'
`),
).toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["foo"]});
const __vite_ssr_import_1__ = await __vite_ssr_import__("foo", {"importedNames":["foo"]});
Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_1__.foo }});
"
`)
"Object.defineProperty(__vite_ssr_exports__, "foo", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_1__.foo }});
const __vite_ssr_import_0__ = await __vite_ssr_import__("foo", {"importedNames":["foo"]});
const __vite_ssr_import_1__ = await __vite_ssr_import__("foo", {"importedNames":["foo"]});
"
`)
})

test('export * as from arbitrary module namespace identifier', async () => {
expect(
await ssrTransformSimpleCode(`export * as "arbitrary string" from 'vue'`),
).toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue");
Object.defineProperty(__vite_ssr_exports__, "arbitrary string", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ }});"
`)
).toMatchInlineSnapshot(
`
"Object.defineProperty(__vite_ssr_exports__, "arbitrary string", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ }});
const __vite_ssr_import_0__ = await __vite_ssr_import__("vue");"
`,
)
})

test('export as arbitrary module namespace identifier', async () => {
expect(
await ssrTransformSimpleCode(
`const something = "Something";export { something as "arbitrary string" };`,
),
).toMatchInlineSnapshot(`
"const something = "Something";
Object.defineProperty(__vite_ssr_exports__, "arbitrary string", { enumerable: true, configurable: true, get(){ return something }});"
`)
).toMatchInlineSnapshot(
`
"Object.defineProperty(__vite_ssr_exports__, "arbitrary string", { enumerable: true, configurable: true, get(){ return something }});
const something = "Something";"
`,
)
})

test('export as from arbitrary module namespace identifier', async () => {
expect(
await ssrTransformSimpleCode(
`export { "arbitrary string2" as "arbitrary string" } from 'vue';`,
),
).toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["arbitrary string2"]});
Object.defineProperty(__vite_ssr_exports__, "arbitrary string", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__["arbitrary string2"] }});"
`)
).toMatchInlineSnapshot(
`
"Object.defineProperty(__vite_ssr_exports__, "arbitrary string", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__["arbitrary string2"] }});
const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["arbitrary string2"]});"
`,
)
})

test('export default', async () => {
Expand Down Expand Up @@ -319,10 +343,12 @@ test('dynamic import', async () => {
const result = await ssrTransformSimple(
`export const i = () => import('./foo')`,
)
expect(result?.code).toMatchInlineSnapshot(`
"const i = () => __vite_ssr_dynamic_import__('./foo')
Object.defineProperty(__vite_ssr_exports__, "i", { enumerable: true, configurable: true, get(){ return i }});"
`)
expect(result?.code).toMatchInlineSnapshot(
`
"Object.defineProperty(__vite_ssr_exports__, "i", { enumerable: true, configurable: true, get(){ return i }});
const i = () => __vite_ssr_dynamic_import__('./foo')"
`,
)
expect(result?.deps).toEqual([])
expect(result?.dynamicDeps).toEqual(['./foo'])
})
Expand Down Expand Up @@ -479,10 +505,10 @@ test('should declare variable for imported super class', async () => {
`export class B extends Foo {}`,
),
).toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("./dependency", {"importedNames":["Foo"]});const Foo = __vite_ssr_import_0__.Foo;
"Object.defineProperty(__vite_ssr_exports__, "B", { enumerable: true, configurable: true, get(){ return B }});
const __vite_ssr_import_0__ = await __vite_ssr_import__("./dependency", {"importedNames":["Foo"]});const Foo = __vite_ssr_import_0__.Foo;
class A extends Foo {};
class B extends Foo {}
Object.defineProperty(__vite_ssr_exports__, "B", { enumerable: true, configurable: true, get(){ return B }});
Object.defineProperty(__vite_ssr_exports__, "default", { enumerable: true, configurable: true, value: A });"
`)
})
Expand Down Expand Up @@ -518,9 +544,9 @@ test('should handle default export variants', async () => {
`export default class A {}\n` + `export class B extends A {}`,
),
).toMatchInlineSnapshot(`
"class A {};
"Object.defineProperty(__vite_ssr_exports__, "B", { enumerable: true, configurable: true, get(){ return B }});
class A {};
class B extends A {}
Object.defineProperty(__vite_ssr_exports__, "B", { enumerable: true, configurable: true, get(){ return B }});
Object.defineProperty(__vite_ssr_exports__, "default", { enumerable: true, configurable: true, value: A });"
`)
})
Expand Down Expand Up @@ -958,12 +984,12 @@ export function fn1() {
`,
),
).toMatchInlineSnapshot(`
"
"Object.defineProperty(__vite_ssr_exports__, "fn1", { enumerable: true, configurable: true, get(){ return fn1 }});
Object.defineProperty(__vite_ssr_exports__, "fn2", { enumerable: true, configurable: true, get(){ return fn2 }});

function fn1() {
};function fn2() {
}
Object.defineProperty(__vite_ssr_exports__, "fn1", { enumerable: true, configurable: true, get(){ return fn1 }});;function fn2() {
}
Object.defineProperty(__vite_ssr_exports__, "fn2", { enumerable: true, configurable: true, get(){ return fn2 }});
"
`)
})
Expand Down Expand Up @@ -1062,7 +1088,8 @@ export class Test {
};`.trim()

expect(await ssrTransformSimpleCode(code)).toMatchInlineSnapshot(`
"const __vite_ssr_import_0__ = await __vite_ssr_import__("foobar", {"importedNames":["foo","bar"]});
"Object.defineProperty(__vite_ssr_exports__, "Test", { enumerable: true, configurable: true, get(){ return Test }});
const __vite_ssr_import_0__ = await __vite_ssr_import__("foobar", {"importedNames":["foo","bar"]});
if (false) {
const foo = 'foo';
console.log(foo)
Expand All @@ -1086,8 +1113,7 @@ export class Test {
console.log((0,__vite_ssr_import_0__.bar))
}
}
}
Object.defineProperty(__vite_ssr_exports__, "Test", { enumerable: true, configurable: true, get(){ return Test }});;;"
};;"
`)
})

Expand Down Expand Up @@ -1252,11 +1278,11 @@ export * as bar from './bar'
console.log(bar)
`),
).toMatchInlineSnapshot(`
"
"Object.defineProperty(__vite_ssr_exports__, "bar", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_1__ }});

const __vite_ssr_import_0__ = await __vite_ssr_import__("./foo", {"importedNames":["foo"]});
__vite_ssr_exports__.default = (0,__vite_ssr_import_0__.foo)();
const __vite_ssr_import_1__ = await __vite_ssr_import__("./bar");
Object.defineProperty(__vite_ssr_exports__, "bar", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_1__ }});;
const __vite_ssr_import_1__ = await __vite_ssr_import__("./bar");;
console.log(bar)
"
`)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
- `test1` - `test5`

Cyclic import example based on https://github.com/vitejs/vite/issues/14048#issuecomment-2354774156

```mermaid
flowchart TD
B(dep1.js) -->|dep1| A(index.js)
A -->|dep1| C(dep2.js)
C -->|dep2| A
A -->|dep1, dep2| entry.js
```

---

- `test6`

```mermaid
flowchart TD
A(dep1.js) -->|dep1| B
B(dep2.js) -->|dep2| A
A -->|dep1| C(index.js)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const dep1 = { ok: true };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { dep1 } from "./index.js"
export const dep2 = { ok: dep1.ok }
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { dep1 } from './dep1.js'
export { dep1 }

import { dep2 } from './dep2.js'
export { dep2 }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const dep1 = { ok: true };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { dep1 } from "./index.js"
export const dep2 = { ok: dep1.ok }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { dep1 } from './dep1.js'
export { dep2 } from "./dep2.js"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const dep1 = { ok: true };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { dep1 } from "./index.js"
export const dep2 = { ok: dep1.ok }
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { dep1 } from './dep1.js'
import { dep2 } from './dep2.js'
export { dep1 }
export { dep2 }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const dep1 = { ok: true };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { dep1 } from "./index.js"
export const dep2 = { ok: dep1.ok }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './dep1.js'
export * from './dep2.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const dep1 = { ok: true };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { dep1 } from "./index.js"
export const dep2 = { ok: dep1.ok }
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { dep2 } from './dep2.js'
export { dep2 }

import { dep1 } from './dep1.js'
export { dep1 }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { dep2 } from "./dep2.js"
export const dep1 = "dep1: " + dep2
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { dep1 } from "./dep1.js"
export const dep2 = "dep2: " + dep1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { dep1 } from './dep1.js'
export { dep1 }
Loading