From 7ef25e7b3ef6815d82616b42bc7e7c7395b6931c Mon Sep 17 00:00:00 2001
From: Alec Larson <1925840+aleclarson@users.noreply.github.com>
Date: Thu, 29 Jul 2021 23:19:19 -0400
Subject: [PATCH 1/3] feat(ssr): tolerate circular imports (#3950)
---
.../ssr-react/__tests__/ssr-react.spec.ts | 9 ++-
packages/playground/ssr-react/src/add.js | 9 +++
.../ssr-react/src/circular-dep-init/README.md | 1 +
.../circular-dep-init/circular-dep-init.js | 2 +
.../src/circular-dep-init/module-a.js | 1 +
.../src/circular-dep-init/module-b.js | 8 ++
.../ssr-react/src/forked-deadlock/README.md | 51 ++++++++++++
.../src/forked-deadlock/common-module.js | 10 +++
.../forked-deadlock/deadlock-fuse-module.js | 8 ++
.../fuse-stuck-bridge-module.js | 8 ++
.../src/forked-deadlock/middle-module.js | 8 ++
.../src/forked-deadlock/stuck-module.js | 8 ++
packages/playground/ssr-react/src/multiply.js | 9 +++
.../playground/ssr-react/src/pages/About.jsx | 11 ++-
.../playground/ssr-react/src/pages/Home.jsx | 16 +++-
.../node/ssr/__tests__/ssrTransform.spec.ts | 72 +++++++++--------
packages/vite/src/node/ssr/ssrModuleLoader.ts | 79 +++++++++++--------
packages/vite/src/node/ssr/ssrTransform.ts | 50 ++++++------
18 files changed, 269 insertions(+), 91 deletions(-)
create mode 100644 packages/playground/ssr-react/src/add.js
create mode 100644 packages/playground/ssr-react/src/circular-dep-init/README.md
create mode 100644 packages/playground/ssr-react/src/circular-dep-init/circular-dep-init.js
create mode 100644 packages/playground/ssr-react/src/circular-dep-init/module-a.js
create mode 100644 packages/playground/ssr-react/src/circular-dep-init/module-b.js
create mode 100644 packages/playground/ssr-react/src/forked-deadlock/README.md
create mode 100644 packages/playground/ssr-react/src/forked-deadlock/common-module.js
create mode 100644 packages/playground/ssr-react/src/forked-deadlock/deadlock-fuse-module.js
create mode 100644 packages/playground/ssr-react/src/forked-deadlock/fuse-stuck-bridge-module.js
create mode 100644 packages/playground/ssr-react/src/forked-deadlock/middle-module.js
create mode 100644 packages/playground/ssr-react/src/forked-deadlock/stuck-module.js
create mode 100644 packages/playground/ssr-react/src/multiply.js
diff --git a/packages/playground/ssr-react/__tests__/ssr-react.spec.ts b/packages/playground/ssr-react/__tests__/ssr-react.spec.ts
index bf161e03e5143c..d7c3313b38e57a 100644
--- a/packages/playground/ssr-react/__tests__/ssr-react.spec.ts
+++ b/packages/playground/ssr-react/__tests__/ssr-react.spec.ts
@@ -1,4 +1,4 @@
-import { editFile, getColor, isBuild, untilUpdated } from '../../testUtils'
+import { editFile, untilUpdated } from '../../testUtils'
import { port } from './serve'
import fetch from 'node-fetch'
@@ -46,3 +46,10 @@ test('client navigation', async () => {
)
await untilUpdated(() => page.textContent('h1'), 'changed')
})
+
+test(`circular dependecies modules doesn't throw`, async () => {
+ await page.goto(url)
+ expect(await page.textContent('.circ-dep-init')).toMatch(
+ 'circ-dep-init-a circ-dep-init-b'
+ )
+})
diff --git a/packages/playground/ssr-react/src/add.js b/packages/playground/ssr-react/src/add.js
new file mode 100644
index 00000000000000..a0e419e9cfcacf
--- /dev/null
+++ b/packages/playground/ssr-react/src/add.js
@@ -0,0 +1,9 @@
+import { multiply } from './multiply'
+
+export function add(a, b) {
+ return a + b
+}
+
+export function addAndMultiply(a, b, c) {
+ return multiply(add(a, b), c)
+}
diff --git a/packages/playground/ssr-react/src/circular-dep-init/README.md b/packages/playground/ssr-react/src/circular-dep-init/README.md
new file mode 100644
index 00000000000000..864d16ae8c495b
--- /dev/null
+++ b/packages/playground/ssr-react/src/circular-dep-init/README.md
@@ -0,0 +1 @@
+This test aim to find out wherever the modules with circular dependencies are correctly initialized
diff --git a/packages/playground/ssr-react/src/circular-dep-init/circular-dep-init.js b/packages/playground/ssr-react/src/circular-dep-init/circular-dep-init.js
new file mode 100644
index 00000000000000..8867d64ec45091
--- /dev/null
+++ b/packages/playground/ssr-react/src/circular-dep-init/circular-dep-init.js
@@ -0,0 +1,2 @@
+export * from './module-a'
+export { getValueAB } from './module-b'
diff --git a/packages/playground/ssr-react/src/circular-dep-init/module-a.js b/packages/playground/ssr-react/src/circular-dep-init/module-a.js
new file mode 100644
index 00000000000000..335b3ac26ab3b5
--- /dev/null
+++ b/packages/playground/ssr-react/src/circular-dep-init/module-a.js
@@ -0,0 +1 @@
+export const valueA = 'circ-dep-init-a'
diff --git a/packages/playground/ssr-react/src/circular-dep-init/module-b.js b/packages/playground/ssr-react/src/circular-dep-init/module-b.js
new file mode 100644
index 00000000000000..cb16d7e9be4a30
--- /dev/null
+++ b/packages/playground/ssr-react/src/circular-dep-init/module-b.js
@@ -0,0 +1,8 @@
+import { valueA } from './circular-dep-init'
+
+export const valueB = 'circ-dep-init-b'
+export const valueAB = valueA.concat(` ${valueB}`)
+
+export function getValueAB() {
+ return valueAB
+}
diff --git a/packages/playground/ssr-react/src/forked-deadlock/README.md b/packages/playground/ssr-react/src/forked-deadlock/README.md
new file mode 100644
index 00000000000000..798c8c429ee9e4
--- /dev/null
+++ b/packages/playground/ssr-react/src/forked-deadlock/README.md
@@ -0,0 +1,51 @@
+This test aim to check for a particular type of circular dependency that causes tricky deadlocks, **deadlocks with forked imports stack**
+
+```
+A -> B means: B is imported by A and B has A in its stack
+A ... B means: A is waiting for B to ssrLoadModule()
+
+H -> X ... Y
+H -> X -> Y ... B
+H -> A ... B
+H -> A -> B ... X
+```
+
+### Forked deadlock description:
+
+```
+[X] is waiting for [Y] to resolve
+ ↑ ↳ is waiting for [A] to resolve
+ │ ↳ is waiting for [B] to resolve
+ │ ↳ is waiting for [X] to resolve
+ └────────────────────────────────────────────────────────────────────────┘
+```
+
+This may seems a traditional deadlock, but the thing that makes this special is the import stack of each module:
+
+```
+[X] stack:
+ [H]
+```
+
+```
+[Y] stack:
+ [X]
+ [H]
+```
+
+```
+[A] stack:
+ [H]
+```
+
+```
+[B] stack:
+ [A]
+ [H]
+```
+
+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.
+
+### Fix description
+
+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.
diff --git a/packages/playground/ssr-react/src/forked-deadlock/common-module.js b/packages/playground/ssr-react/src/forked-deadlock/common-module.js
new file mode 100644
index 00000000000000..c73a3ee4b970c8
--- /dev/null
+++ b/packages/playground/ssr-react/src/forked-deadlock/common-module.js
@@ -0,0 +1,10 @@
+import { stuckModuleExport } from './stuck-module'
+import { deadlockfuseModuleExport } from './deadlock-fuse-module'
+
+/**
+ * module H
+ */
+export function commonModuleExport() {
+ stuckModuleExport()
+ deadlockfuseModuleExport()
+}
diff --git a/packages/playground/ssr-react/src/forked-deadlock/deadlock-fuse-module.js b/packages/playground/ssr-react/src/forked-deadlock/deadlock-fuse-module.js
new file mode 100644
index 00000000000000..4f31763ba2343f
--- /dev/null
+++ b/packages/playground/ssr-react/src/forked-deadlock/deadlock-fuse-module.js
@@ -0,0 +1,8 @@
+import { fuseStuckBridgeModuleExport } from './fuse-stuck-bridge-module'
+
+/**
+ * module A
+ */
+export function deadlockfuseModuleExport() {
+ fuseStuckBridgeModuleExport()
+}
diff --git a/packages/playground/ssr-react/src/forked-deadlock/fuse-stuck-bridge-module.js b/packages/playground/ssr-react/src/forked-deadlock/fuse-stuck-bridge-module.js
new file mode 100644
index 00000000000000..211ad7c3bc9f92
--- /dev/null
+++ b/packages/playground/ssr-react/src/forked-deadlock/fuse-stuck-bridge-module.js
@@ -0,0 +1,8 @@
+import { stuckModuleExport } from './stuck-module'
+
+/**
+ * module C
+ */
+export function fuseStuckBridgeModuleExport() {
+ stuckModuleExport()
+}
diff --git a/packages/playground/ssr-react/src/forked-deadlock/middle-module.js b/packages/playground/ssr-react/src/forked-deadlock/middle-module.js
new file mode 100644
index 00000000000000..0632eedeabd7a5
--- /dev/null
+++ b/packages/playground/ssr-react/src/forked-deadlock/middle-module.js
@@ -0,0 +1,8 @@
+import { deadlockfuseModuleExport } from './deadlock-fuse-module'
+
+/**
+ * module Y
+ */
+export function middleModuleExport() {
+ void deadlockfuseModuleExport
+}
diff --git a/packages/playground/ssr-react/src/forked-deadlock/stuck-module.js b/packages/playground/ssr-react/src/forked-deadlock/stuck-module.js
new file mode 100644
index 00000000000000..50b4d28063dc70
--- /dev/null
+++ b/packages/playground/ssr-react/src/forked-deadlock/stuck-module.js
@@ -0,0 +1,8 @@
+import { middleModuleExport } from './middle-module'
+
+/**
+ * module X
+ */
+export function stuckModuleExport() {
+ middleModuleExport()
+}
diff --git a/packages/playground/ssr-react/src/multiply.js b/packages/playground/ssr-react/src/multiply.js
new file mode 100644
index 00000000000000..94f43efbff58bd
--- /dev/null
+++ b/packages/playground/ssr-react/src/multiply.js
@@ -0,0 +1,9 @@
+import { add } from './add'
+
+export function multiply(a, b) {
+ return a * b
+}
+
+export function multiplyAndAdd(a, b, c) {
+ return add(multiply(a, b), c)
+}
diff --git a/packages/playground/ssr-react/src/pages/About.jsx b/packages/playground/ssr-react/src/pages/About.jsx
index 22354540091f04..0fe4de69078504 100644
--- a/packages/playground/ssr-react/src/pages/About.jsx
+++ b/packages/playground/ssr-react/src/pages/About.jsx
@@ -1,3 +1,12 @@
+import { addAndMultiply } from '../add'
+import { multiplyAndAdd } from '../multiply'
+
export default function About() {
- return
About
+ return (
+ <>
+ About
+ {addAndMultiply(1, 2, 3)}
+ {multiplyAndAdd(1, 2, 3)}
+ >
+ )
}
diff --git a/packages/playground/ssr-react/src/pages/Home.jsx b/packages/playground/ssr-react/src/pages/Home.jsx
index 3e62e6933192cd..d1f4944810cc98 100644
--- a/packages/playground/ssr-react/src/pages/Home.jsx
+++ b/packages/playground/ssr-react/src/pages/Home.jsx
@@ -1,3 +1,17 @@
+import { addAndMultiply } from '../add'
+import { multiplyAndAdd } from '../multiply'
+import { commonModuleExport } from '../forked-deadlock/common-module'
+import { getValueAB } from '../circular-dep-init/circular-dep-init'
+
export default function Home() {
- return Home
+ commonModuleExport()
+
+ return (
+ <>
+ Home
+ {addAndMultiply(1, 2, 3)}
+ {multiplyAndAdd(1, 2, 3)}
+ {getValueAB()}
+ >
+ )
}
diff --git a/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts b/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts
index b3cc856aa9ae44..d3320a06a9429e 100644
--- a/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts
+++ b/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts
@@ -11,7 +11,7 @@ test('default import', async () => {
)
).code
).toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
+ "const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
console.log(__vite_ssr_import_0__.default.bar)"
`)
})
@@ -26,7 +26,7 @@ test('named import', async () => {
)
).code
).toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
+ "const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
function foo() { return __vite_ssr_import_0__.ref(0) }"
`)
})
@@ -41,7 +41,7 @@ test('namespace import', async () => {
)
).code
).toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
+ "const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
function foo() { return __vite_ssr_import_0__.ref(0) }"
`)
})
@@ -50,7 +50,7 @@ test('export function declaration', async () => {
expect((await ssrTransform(`export function foo() {}`, null, null)).code)
.toMatchInlineSnapshot(`
"function foo() {}
- Object.defineProperty(__vite_ssr_exports__, \\"foo\\", { enumerable: true, configurable: true, get(){ return foo }})"
+ Object.defineProperty(__vite_ssr_exports__, \\"foo\\", { enumerable: true, configurable: true, get(){ return foo }});"
`)
})
@@ -58,7 +58,7 @@ test('export class declaration', async () => {
expect((await ssrTransform(`export class foo {}`, null, null)).code)
.toMatchInlineSnapshot(`
"class foo {}
- Object.defineProperty(__vite_ssr_exports__, \\"foo\\", { enumerable: true, configurable: true, get(){ return foo }})"
+ Object.defineProperty(__vite_ssr_exports__, \\"foo\\", { enumerable: true, configurable: true, get(){ return foo }});"
`)
})
@@ -66,8 +66,8 @@ test('export var declaration', async () => {
expect((await ssrTransform(`export const a = 1, b = 2`, null, null)).code)
.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 }})"
+ 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 }});"
`)
})
@@ -77,8 +77,8 @@ test('export named', async () => {
.code
).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 }})"
+ 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 }});"
`)
})
@@ -87,10 +87,10 @@ test('export named from', async () => {
(await ssrTransform(`export { ref, computed as c } from 'vue'`, null, null))
.code
).toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
+ "const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
- 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 }})"
+ 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 }});"
`)
})
@@ -104,27 +104,35 @@ test('named exports of imported binding', async () => {
)
).code
).toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
+ "const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
- Object.defineProperty(__vite_ssr_exports__, \\"createApp\\", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.createApp }})"
+ Object.defineProperty(__vite_ssr_exports__, \\"createApp\\", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__.createApp }});"
`)
})
test('export * from', async () => {
- expect((await ssrTransform(`export * from 'vue'`, null, null)).code)
- .toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
-
- __vite_ssr_exportAll__(__vite_ssr_import_0__)"
+ expect(
+ (
+ await ssrTransform(
+ `export * from 'vue'\n` + `export * from 'react'`,
+ null,
+ null
+ )
+ ).code
+ ).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__);"
`)
})
test('export * as from', async () => {
expect((await ssrTransform(`export * as foo from 'vue'`, null, null)).code)
.toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
+ "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__ }})"
+ Object.defineProperty(__vite_ssr_exports__, \\"foo\\", { enumerable: true, configurable: true, get(){ return __vite_ssr_import_0__ }});"
`)
})
@@ -146,7 +154,7 @@ test('dynamic import', async () => {
.code
).toMatchInlineSnapshot(`
"const i = () => __vite_ssr_dynamic_import__('./foo')
- Object.defineProperty(__vite_ssr_exports__, \\"i\\", { enumerable: true, configurable: true, get(){ return i }})"
+ Object.defineProperty(__vite_ssr_exports__, \\"i\\", { enumerable: true, configurable: true, get(){ return i }});"
`)
})
@@ -160,7 +168,7 @@ test('do not rewrite method definition', async () => {
)
).code
).toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
+ "const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
class A { fn() { __vite_ssr_import_0__.fn() } }"
`)
})
@@ -175,7 +183,7 @@ test('do not rewrite catch clause', async () => {
)
).code
).toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"./dependency\\")
+ "const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"./dependency\\");
try {} catch(error) {}"
`)
})
@@ -191,7 +199,7 @@ test('should declare variable for imported super class', async () => {
)
).code
).toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"./dependency\\")
+ "const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"./dependency\\");
const Foo = __vite_ssr_import_0__.Foo;
class A extends Foo {}"
`)
@@ -209,12 +217,12 @@ test('should declare variable for imported super class', async () => {
)
).code
).toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"./dependency\\")
+ "const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"./dependency\\");
const Foo = __vite_ssr_import_0__.Foo;
class A extends Foo {}
class B extends Foo {}
- Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: A })
- Object.defineProperty(__vite_ssr_exports__, \\"B\\", { enumerable: true, configurable: true, get(){ return B }})"
+ Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: A });
+ Object.defineProperty(__vite_ssr_exports__, \\"B\\", { enumerable: true, configurable: true, get(){ return B }});"
`)
})
@@ -246,7 +254,7 @@ test('should handle default export variants', async () => {
).toMatchInlineSnapshot(`
"function foo() {}
foo.prototype = Object.prototype;
- Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: foo })"
+ Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: foo });"
`)
// default named classes
expect(
@@ -260,8 +268,8 @@ test('should handle default export variants', async () => {
).toMatchInlineSnapshot(`
"class A {}
class B extends A {}
- Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: A })
- Object.defineProperty(__vite_ssr_exports__, \\"B\\", { enumerable: true, configurable: true, get(){ return B }})"
+ Object.defineProperty(__vite_ssr_exports__, \\"default\\", { enumerable: true, value: A });
+ Object.defineProperty(__vite_ssr_exports__, \\"B\\", { enumerable: true, configurable: true, get(){ return B }});"
`)
})
@@ -288,7 +296,7 @@ test('overwrite bindings', async () => {
)
).code
).toMatchInlineSnapshot(`
- "const __vite_ssr_import_0__ = __vite_ssr_import__(\\"vue\\")
+ "const __vite_ssr_import_0__ = await __vite_ssr_import__(\\"vue\\");
const a = { inject: __vite_ssr_import_0__.inject }
const b = { test: __vite_ssr_import_0__.inject }
function c() { const { test: inject } = { test: true }; console.log(inject) }
diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts
index b68b104461c0b3..2eef895c627e76 100644
--- a/packages/vite/src/node/ssr/ssrModuleLoader.ts
+++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts
@@ -19,6 +19,7 @@ interface SSRContext {
type SSRModule = Record
const pendingModules = new Map>()
+const pendingImports = new Map()
export async function ssrLoadModule(
url: string,
@@ -28,13 +29,6 @@ export async function ssrLoadModule(
): Promise {
url = unwrapId(url)
- if (urlStack.includes(url)) {
- server.config.logger.warn(
- `Circular dependency: ${urlStack.join(' -> ')} -> ${url}`
- )
- return {}
- }
-
// when we instantiate multiple dependency modules in parallel, they may
// point to shared modules. We need to avoid duplicate instantiation attempts
// by register every module as pending synchronously so that all subsequent
@@ -46,7 +40,13 @@ export async function ssrLoadModule(
const modulePromise = instantiateModule(url, server, context, urlStack)
pendingModules.set(url, modulePromise)
- modulePromise.catch(() => {}).then(() => pendingModules.delete(url))
+ modulePromise
+ .catch(() => {
+ pendingImports.delete(url)
+ })
+ .then(() => {
+ pendingModules.delete(url)
+ })
return modulePromise
}
@@ -76,37 +76,46 @@ async function instantiateModule(
}
Object.defineProperty(ssrModule, '__esModule', { value: true })
- const isExternal = (dep: string) => dep[0] !== '.' && dep[0] !== '/'
-
- await Promise.all(
- result.deps!.map((dep) => {
- if (!isExternal(dep)) {
- return ssrLoadModule(dep, server, context, urlStack.concat(url))
- }
- })
- )
+ // Tolerate circular imports by ensuring the module can be
+ // referenced before it's been instantiated.
+ mod.ssrModule = ssrModule
const ssrImportMeta = { url }
- const ssrImport = (dep: string) => {
- if (isExternal(dep)) {
+ urlStack = urlStack.concat(url)
+ const isCircular = (url: string) => urlStack.includes(url)
+
+ // Since dynamic imports can happen in parallel, we need to
+ // account for multiple pending deps and duplicate imports.
+ const pendingDeps: string[] = []
+
+ const ssrImport = async (dep: string) => {
+ if (dep[0] !== '.' && dep[0] !== '/') {
return nodeRequire(dep, mod.file, server.config.root)
- } else {
- return moduleGraph.urlToModuleMap.get(unwrapId(dep))?.ssrModule
}
+ dep = unwrapId(dep)
+ if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) {
+ pendingDeps.push(dep)
+ if (pendingDeps.length === 1) {
+ pendingImports.set(url, pendingDeps)
+ }
+ await ssrLoadModule(dep, server, context, urlStack)
+ if (pendingDeps.length === 1) {
+ pendingImports.delete(url)
+ } else {
+ pendingDeps.splice(pendingDeps.indexOf(dep), 1)
+ }
+ }
+ return moduleGraph.urlToModuleMap.get(dep)?.ssrModule
}
const ssrDynamicImport = (dep: string) => {
- if (isExternal(dep)) {
- return Promise.resolve(nodeRequire(dep, mod.file, server.config.root))
- } else {
- // #3087 dynamic import vars is ignored at rewrite import path,
- // so here need process relative path
- if (dep.startsWith('.')) {
- dep = path.posix.resolve(path.dirname(url), dep)
- }
- return ssrLoadModule(dep, server, context, urlStack.concat(url))
+ // #3087 dynamic import vars is ignored at rewrite import path,
+ // so here need process relative path
+ if (dep[0] === '.') {
+ dep = path.posix.resolve(path.dirname(url), dep)
}
+ return ssrImport(dep)
}
function ssrExportAll(sourceModule: any) {
@@ -124,7 +133,9 @@ async function instantiateModule(
}
try {
- new Function(
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ const AsyncFunction = async function () {}.constructor as typeof Function
+ const initModule = new AsyncFunction(
`global`,
ssrModuleExportsKey,
ssrImportMetaKey,
@@ -132,7 +143,8 @@ async function instantiateModule(
ssrDynamicImportKey,
ssrExportAllKey,
result.code + `\n//# sourceURL=${mod.url}`
- )(
+ )
+ await initModule(
context.global,
ssrModule,
ssrImportMeta,
@@ -153,8 +165,7 @@ async function instantiateModule(
throw e
}
- mod.ssrModule = Object.freeze(ssrModule)
- return ssrModule
+ return Object.freeze(ssrModule)
}
function nodeRequire(id: string, importer: string | null, root: string) {
diff --git a/packages/vite/src/node/ssr/ssrTransform.ts b/packages/vite/src/node/ssr/ssrTransform.ts
index 45d0a95b16d0dd..c5d87e8c9c6fc5 100644
--- a/packages/vite/src/node/ssr/ssrTransform.ts
+++ b/packages/vite/src/node/ssr/ssrTransform.ts
@@ -47,15 +47,16 @@ export async function ssrTransform(
const importId = `__vite_ssr_import_${uid++}__`
s.appendLeft(
node.start,
- `const ${importId} = ${ssrImportKey}(${JSON.stringify(source)})\n`
+ `const ${importId} = await ${ssrImportKey}(${JSON.stringify(source)});\n`
)
return importId
}
- function defineExport(name: string, local = name) {
- s.append(
+ function defineExport(position: number, name: string, local = name) {
+ s.appendRight(
+ position,
`\nObject.defineProperty(${ssrModuleExportsKey}, "${name}", ` +
- `{ enumerable: true, configurable: true, get(){ return ${local} }})`
+ `{ enumerable: true, configurable: true, get(){ return ${local} }});`
)
}
@@ -93,32 +94,37 @@ export async function ssrTransform(
node.declaration.type === 'ClassDeclaration'
) {
// export function foo() {}
- defineExport(node.declaration.id!.name)
+ defineExport(node.end, node.declaration.id!.name)
} else {
// export const foo = 1, bar = 2
for (const declaration of node.declaration.declarations) {
const names = extractNames(declaration.id as any)
for (const name of names) {
- defineExport(name)
+ defineExport(node.end, name)
}
}
}
s.remove(node.start, (node.declaration as Node).start)
- } else if (node.source) {
- // export { foo, bar } from './foo'
- const importId = defineImport(node, node.source.value as string)
- for (const spec of node.specifiers) {
- defineExport(spec.exported.name, `${importId}.${spec.local.name}`)
- }
- s.remove(node.start, node.end)
} else {
- // export { foo, bar }
- for (const spec of node.specifiers) {
- const local = spec.local.name
- const binding = idToImportMap.get(local)
- defineExport(spec.exported.name, binding || local)
- }
s.remove(node.start, node.end)
+ if (node.source) {
+ // export { foo, bar } from './foo'
+ const importId = defineImport(node, node.source.value as string)
+ for (const spec of node.specifiers) {
+ defineExport(
+ node.end,
+ spec.exported.name,
+ `${importId}.${spec.local.name}`
+ )
+ }
+ } else {
+ // export { foo, bar }
+ for (const spec of node.specifiers) {
+ const local = spec.local.name
+ const binding = idToImportMap.get(local)
+ defineExport(node.end, spec.exported.name, binding || local)
+ }
+ }
}
}
@@ -132,7 +138,7 @@ export async function ssrTransform(
s.remove(node.start, node.start + 15 /* 'export default '.length */)
s.append(
`\nObject.defineProperty(${ssrModuleExportsKey}, "default", ` +
- `{ enumerable: true, value: ${name} })`
+ `{ enumerable: true, value: ${name} });`
)
} else {
// anonymous default exports
@@ -148,12 +154,12 @@ export async function ssrTransform(
if (node.type === 'ExportAllDeclaration') {
if (node.exported) {
const importId = defineImport(node, node.source.value as string)
- defineExport(node.exported.name, `${importId}`)
s.remove(node.start, node.end)
+ defineExport(node.end, node.exported.name, `${importId}`)
} else {
const importId = defineImport(node, node.source.value as string)
s.remove(node.start, node.end)
- s.append(`\n${ssrExportAllKey}(${importId})`)
+ s.appendLeft(node.end, `${ssrExportAllKey}(${importId});`)
}
}
}
From a6e3078664a56c3fce533a17e114a49ecff26c48 Mon Sep 17 00:00:00 2001
From: Alec Larson <1925840+aleclarson@users.noreply.github.com>
Date: Thu, 29 Jul 2021 23:21:49 -0400
Subject: [PATCH 2/3] fix(ssr): use `tryNodeResolve` instead of `resolveFrom`
(#3951)
---
packages/vite/src/node/plugins/index.ts | 2 +
packages/vite/src/node/plugins/resolve.ts | 17 +++--
.../vite/src/node/plugins/ssrRequireHook.ts | 64 ++++++++++++++++
packages/vite/src/node/ssr/ssrExternal.ts | 1 +
packages/vite/src/node/ssr/ssrModuleLoader.ts | 75 +++++++++++++------
5 files changed, 129 insertions(+), 30 deletions(-)
create mode 100644 packages/vite/src/node/plugins/ssrRequireHook.ts
diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts
index a83ddae2b2bf2a..6dfa6a1437ee94 100644
--- a/packages/vite/src/node/plugins/index.ts
+++ b/packages/vite/src/node/plugins/index.ts
@@ -14,6 +14,7 @@ import { modulePreloadPolyfillPlugin } from './modulePreloadPolyfill'
import { webWorkerPlugin } from './worker'
import { preAliasPlugin } from './preAlias'
import { definePlugin } from './define'
+import { ssrRequireHookPlugin } from './ssrRequireHook'
export async function resolvePlugins(
config: ResolvedConfig,
@@ -42,6 +43,7 @@ export async function resolvePlugins(
ssrTarget: config.ssr?.target,
asSrc: true
}),
+ config.build.ssr ? ssrRequireHookPlugin(config) : null,
htmlInlineScriptProxyPlugin(),
cssPlugin(config),
config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,
diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts
index 15974ecfef7eef..9673c73b93bc09 100644
--- a/packages/vite/src/node/plugins/resolve.ts
+++ b/packages/vite/src/node/plugins/resolve.ts
@@ -20,7 +20,6 @@ import {
normalizePath,
fsPathFromId,
ensureVolumeInPath,
- resolveFrom,
isDataUrl,
cleanUrl,
slash
@@ -29,6 +28,7 @@ import { ViteDevServer, SSRTarget } from '..'
import { createFilter } from '@rollup/pluginutils'
import { PartialResolvedId } from 'rollup'
import { resolve as _resolveExports } from 'resolve.exports'
+import resolve from 'resolve'
// special id for paths marked with browser: false
// https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module
@@ -61,6 +61,7 @@ export interface InternalResolveOptions extends ResolveOptions {
tryPrefix?: string
preferRelative?: boolean
isRequire?: boolean
+ preserveSymlinks?: boolean
}
export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
@@ -361,7 +362,7 @@ export const idToPkgMap = new Map()
export function tryNodeResolve(
id: string,
- importer: string | undefined,
+ importer: string | null | undefined,
options: InternalResolveOptions,
targetWeb: boolean,
server?: ViteDevServer,
@@ -379,12 +380,12 @@ export function tryNodeResolve(
path.isAbsolute(importer) &&
fs.existsSync(cleanUrl(importer))
) {
- basedir = path.dirname(importer)
+ basedir = fs.realpathSync.native(path.dirname(importer))
} else {
basedir = root
}
- const pkg = resolvePackageData(pkgId, basedir)
+ const pkg = resolvePackageData(pkgId, basedir, options.preserveSymlinks)
if (!pkg) {
return
@@ -483,14 +484,18 @@ const packageCache = new Map()
export function resolvePackageData(
id: string,
- basedir: string
+ basedir: string,
+ preserveSymlinks = false
): PackageData | undefined {
const cacheKey = id + basedir
if (packageCache.has(cacheKey)) {
return packageCache.get(cacheKey)
}
try {
- const pkgPath = resolveFrom(`${id}/package.json`, basedir)
+ const pkgPath = resolve.sync(`${id}/package.json`, {
+ basedir,
+ preserveSymlinks
+ })
return loadPackageData(pkgPath, cacheKey)
} catch (e) {
isDebug && debug(`${chalk.red(`[failed loading package.json]`)} ${id}`)
diff --git a/packages/vite/src/node/plugins/ssrRequireHook.ts b/packages/vite/src/node/plugins/ssrRequireHook.ts
new file mode 100644
index 00000000000000..a8c263bd0cfecf
--- /dev/null
+++ b/packages/vite/src/node/plugins/ssrRequireHook.ts
@@ -0,0 +1,64 @@
+import MagicString from 'magic-string'
+import { ResolvedConfig } from '..'
+import { Plugin } from '../plugin'
+
+export function ssrRequireHookPlugin(config: ResolvedConfig): Plugin | null {
+ if (config.command !== 'build' || !config.resolve.dedupe?.length) {
+ return null
+ }
+ return {
+ name: 'vite:ssr-require-hook',
+ transform(code, id) {
+ const moduleInfo = this.getModuleInfo(id)
+ if (moduleInfo?.isEntry) {
+ const s = new MagicString(code)
+ s.prepend(
+ `;(${dedupeRequire.toString()})(${JSON.stringify(
+ config.resolve.dedupe
+ )});\n`
+ )
+ return {
+ code: s.toString(),
+ map: s.generateMap({
+ source: id
+ })
+ }
+ }
+ }
+ }
+}
+
+type NodeResolveFilename = (
+ request: string,
+ parent: NodeModule,
+ isMain: boolean,
+ options?: Record
+) => string
+
+/** Respect the `resolve.dedupe` option in production SSR. */
+function dedupeRequire(dedupe: string[]) {
+ const Module = require('module') as { _resolveFilename: NodeResolveFilename }
+ const resolveFilename = Module._resolveFilename
+ Module._resolveFilename = function (request, parent, isMain, options) {
+ if (request[0] !== '.' && request[0] !== '/') {
+ const parts = request.split('/')
+ const pkgName = parts[0][0] === '@' ? parts[0] + '/' + parts[1] : parts[0]
+ if (dedupe.includes(pkgName)) {
+ // Use this module as the parent.
+ parent = module
+ }
+ }
+ return resolveFilename!(request, parent, isMain, options)
+ }
+}
+
+export function hookNodeResolve(
+ getResolver: (resolveFilename: NodeResolveFilename) => NodeResolveFilename
+): () => void {
+ const Module = require('module') as { _resolveFilename: NodeResolveFilename }
+ const prevResolver = Module._resolveFilename
+ Module._resolveFilename = getResolver(prevResolver)
+ return () => {
+ Module._resolveFilename = prevResolver
+ }
+}
diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/ssr/ssrExternal.ts
index a3481283038789..8c8de96c01609f 100644
--- a/packages/vite/src/node/ssr/ssrExternal.ts
+++ b/packages/vite/src/node/ssr/ssrExternal.ts
@@ -35,6 +35,7 @@ export function resolveSSRExternal(
const resolveOptions: InternalResolveOptions = {
root,
+ preserveSymlinks: true,
isProduction: false,
isBuild: true
}
diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts
index 2eef895c627e76..7fe8d2207571e0 100644
--- a/packages/vite/src/node/ssr/ssrModuleLoader.ts
+++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts
@@ -1,7 +1,7 @@
-import fs from 'fs'
import path from 'path'
+import { Module } from 'module'
import { ViteDevServer } from '..'
-import { cleanUrl, resolveFrom, unwrapId } from '../utils'
+import { unwrapId } from '../utils'
import { rebindErrorStacktrace, ssrRewriteStacktrace } from './ssrStacktrace'
import {
ssrExportAllKey,
@@ -11,6 +11,8 @@ import {
ssrDynamicImportKey
} from './ssrTransform'
import { transformRequest } from '../server/transformRequest'
+import { InternalResolveOptions, tryNodeResolve } from '../plugins/resolve'
+import { hookNodeResolve } from '../plugins/ssrRequireHook'
interface SSRContext {
global: NodeJS.Global
@@ -80,7 +82,24 @@ async function instantiateModule(
// referenced before it's been instantiated.
mod.ssrModule = ssrModule
- const ssrImportMeta = { url }
+ const {
+ isProduction,
+ resolve: { dedupe },
+ root
+ } = server.config
+
+ const resolveOptions: InternalResolveOptions = {
+ conditions: ['node'],
+ dedupe,
+ // Prefer CommonJS modules.
+ extensions: ['.js', '.mjs', '.ts', '.jsx', '.tsx', '.json'],
+ isBuild: true,
+ isProduction,
+ // Disable "module" condition.
+ isRequire: true,
+ mainFields: ['main'],
+ root
+ }
urlStack = urlStack.concat(url)
const isCircular = (url: string) => urlStack.includes(url)
@@ -91,7 +110,7 @@ async function instantiateModule(
const ssrImport = async (dep: string) => {
if (dep[0] !== '.' && dep[0] !== '/') {
- return nodeRequire(dep, mod.file, server.config.root)
+ return nodeRequire(dep, mod.file, resolveOptions)
}
dep = unwrapId(dep)
if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) {
@@ -132,6 +151,7 @@ async function instantiateModule(
}
}
+ const ssrImportMeta = { url }
try {
// eslint-disable-next-line @typescript-eslint/no-empty-function
const AsyncFunction = async function () {}.constructor as typeof Function
@@ -168,10 +188,34 @@ async function instantiateModule(
return Object.freeze(ssrModule)
}
-function nodeRequire(id: string, importer: string | null, root: string) {
- const mod = require(resolve(id, importer, root))
- const defaultExport = mod.__esModule ? mod.default : mod
+function nodeRequire(
+ id: string,
+ importer: string | null,
+ resolveOptions: InternalResolveOptions
+) {
+ const loadModule = Module.createRequire(importer || resolveOptions.root + '/')
+ const unhookNodeResolve = hookNodeResolve(
+ (nodeResolve) => (id, parent, isMain, options) => {
+ if (id[0] === '.' || Module.builtinModules.includes(id)) {
+ return nodeResolve(id, parent, isMain, options)
+ }
+ const resolved = tryNodeResolve(id, parent.id, resolveOptions, false)
+ if (!resolved) {
+ throw Error(`Cannot find module '${id}' imported from '${parent.id}'`)
+ }
+ return resolved.id
+ }
+ )
+
+ let mod: any
+ try {
+ mod = loadModule(id)
+ } finally {
+ unhookNodeResolve()
+ }
+
// rollup-style default import interop for cjs
+ const defaultExport = mod.__esModule ? mod.default : mod
return new Proxy(mod, {
get(mod, prop) {
if (prop === 'default') return defaultExport
@@ -179,20 +223,3 @@ function nodeRequire(id: string, importer: string | null, root: string) {
}
})
}
-
-const resolveCache = new Map()
-
-function resolve(id: string, importer: string | null, root: string) {
- const key = id + importer + root
- const cached = resolveCache.get(key)
- if (cached) {
- return cached
- }
- const resolveDir =
- importer && fs.existsSync(cleanUrl(importer))
- ? path.dirname(importer)
- : root
- const resolved = resolveFrom(id, resolveDir, true)
- resolveCache.set(key, resolved)
- return resolved
-}
From 6a279e800a0119e8589a73fa8f69fe7ce6f6273e Mon Sep 17 00:00:00 2001
From: Alec Larson <1925840+aleclarson@users.noreply.github.com>
Date: Thu, 29 Jul 2021 23:56:01 -0400
Subject: [PATCH 3/3] wip: ssr esm support
---
.../playground/ssr-esm/app/entry-server.jsx | 4 +
.../ssr-esm/cjs-package/index.dev.js | 1 +
.../playground/ssr-esm/cjs-package/index.js | 1 +
.../ssr-esm/cjs-package/package.json | 10 +++
.../ssr-esm/esm-package/index.dev.mjs | 4 +
.../playground/ssr-esm/esm-package/index.mjs | 4 +
.../ssr-esm/esm-package/package.json | 10 +++
packages/playground/ssr-esm/package.json | 14 ++++
packages/playground/ssr-esm/server.mjs | 22 ++++++
packages/playground/ssr-esm/vite.config.js | 8 ++
packages/vite/src/node/ssr/ssrModuleLoader.ts | 75 ++++++++++---------
11 files changed, 116 insertions(+), 37 deletions(-)
create mode 100644 packages/playground/ssr-esm/app/entry-server.jsx
create mode 100644 packages/playground/ssr-esm/cjs-package/index.dev.js
create mode 100644 packages/playground/ssr-esm/cjs-package/index.js
create mode 100644 packages/playground/ssr-esm/cjs-package/package.json
create mode 100644 packages/playground/ssr-esm/esm-package/index.dev.mjs
create mode 100644 packages/playground/ssr-esm/esm-package/index.mjs
create mode 100644 packages/playground/ssr-esm/esm-package/package.json
create mode 100644 packages/playground/ssr-esm/package.json
create mode 100644 packages/playground/ssr-esm/server.mjs
create mode 100644 packages/playground/ssr-esm/vite.config.js
diff --git a/packages/playground/ssr-esm/app/entry-server.jsx b/packages/playground/ssr-esm/app/entry-server.jsx
new file mode 100644
index 00000000000000..eb21267d239846
--- /dev/null
+++ b/packages/playground/ssr-esm/app/entry-server.jsx
@@ -0,0 +1,4 @@
+import cjs from 'cjs-package'
+import { esm, cjsFromEsm } from 'esm-package'
+
+export default { cjs, esm, cjsFromEsm }
diff --git a/packages/playground/ssr-esm/cjs-package/index.dev.js b/packages/playground/ssr-esm/cjs-package/index.dev.js
new file mode 100644
index 00000000000000..0c1292c43abfe0
--- /dev/null
+++ b/packages/playground/ssr-esm/cjs-package/index.dev.js
@@ -0,0 +1 @@
+module.exports = 'cjs-dev'
diff --git a/packages/playground/ssr-esm/cjs-package/index.js b/packages/playground/ssr-esm/cjs-package/index.js
new file mode 100644
index 00000000000000..90f38dfeccf3ff
--- /dev/null
+++ b/packages/playground/ssr-esm/cjs-package/index.js
@@ -0,0 +1 @@
+module.exports = 'cjs-prod'
diff --git a/packages/playground/ssr-esm/cjs-package/package.json b/packages/playground/ssr-esm/cjs-package/package.json
new file mode 100644
index 00000000000000..64749e2fbe1b2c
--- /dev/null
+++ b/packages/playground/ssr-esm/cjs-package/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "cjs-package",
+ "version": "0.0.0",
+ "exports": {
+ ".": {
+ "development": "./index.dev.js",
+ "default": "./index.js"
+ }
+ }
+}
diff --git a/packages/playground/ssr-esm/esm-package/index.dev.mjs b/packages/playground/ssr-esm/esm-package/index.dev.mjs
new file mode 100644
index 00000000000000..3beacc4ebd14df
--- /dev/null
+++ b/packages/playground/ssr-esm/esm-package/index.dev.mjs
@@ -0,0 +1,4 @@
+import cjs from 'cjs-package'
+
+export const esm = 'esm-dev'
+export const cjsFromEsm = cjs
diff --git a/packages/playground/ssr-esm/esm-package/index.mjs b/packages/playground/ssr-esm/esm-package/index.mjs
new file mode 100644
index 00000000000000..0c1786e744538a
--- /dev/null
+++ b/packages/playground/ssr-esm/esm-package/index.mjs
@@ -0,0 +1,4 @@
+import cjs from 'cjs-package'
+
+export const esm = 'esm-prod'
+export const cjsFromEsm = cjs
diff --git a/packages/playground/ssr-esm/esm-package/package.json b/packages/playground/ssr-esm/esm-package/package.json
new file mode 100644
index 00000000000000..2f44846862ce00
--- /dev/null
+++ b/packages/playground/ssr-esm/esm-package/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "esm-package",
+ "version": "0.0.0",
+ "exports": {
+ ".": {
+ "development": "./index.dev.mjs",
+ "default": "./index.mjs"
+ }
+ }
+}
diff --git a/packages/playground/ssr-esm/package.json b/packages/playground/ssr-esm/package.json
new file mode 100644
index 00000000000000..f4c2826140dc4c
--- /dev/null
+++ b/packages/playground/ssr-esm/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "test-ssr-esm",
+ "private": true,
+ "version": "0.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "cjs-package": "link:./cjs-package",
+ "esm-package": "link:./esm-package",
+ "vite": "link:../../vite"
+ },
+ "scripts": {
+ "serve": "node server.mjs"
+ }
+}
diff --git a/packages/playground/ssr-esm/server.mjs b/packages/playground/ssr-esm/server.mjs
new file mode 100644
index 00000000000000..b06a93614f3d87
--- /dev/null
+++ b/packages/playground/ssr-esm/server.mjs
@@ -0,0 +1,22 @@
+import path from 'path'
+import { fileURLToPath } from 'url'
+
+import { install } from 'source-map-support'
+install()
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+const vite = await import('vite')
+const server = await vite.createServer({
+ root: path.join(__dirname, 'app'),
+ // mode: 'production',
+ ssr: {
+ external: ['cjs-package', 'esm-package']
+ },
+ server: {
+ middlewareMode: 'ssr'
+ }
+})
+
+const entryModule = await server.ssrLoadModule('/entry-server.jsx')
+console.log(entryModule.default)
diff --git a/packages/playground/ssr-esm/vite.config.js b/packages/playground/ssr-esm/vite.config.js
new file mode 100644
index 00000000000000..2fbf202a3f46c8
--- /dev/null
+++ b/packages/playground/ssr-esm/vite.config.js
@@ -0,0 +1,8 @@
+/**
+ * @type {import('vite').UserConfig}
+ */
+module.exports = {
+ build: {
+ minify: false
+ }
+}
diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts
index 7fe8d2207571e0..789ca8f56e9a8b 100644
--- a/packages/vite/src/node/ssr/ssrModuleLoader.ts
+++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts
@@ -1,3 +1,5 @@
+import vm from 'vm'
+import fs from 'fs'
import path from 'path'
import { Module } from 'module'
import { ViteDevServer } from '..'
@@ -95,9 +97,7 @@ async function instantiateModule(
extensions: ['.js', '.mjs', '.ts', '.jsx', '.tsx', '.json'],
isBuild: true,
isProduction,
- // Disable "module" condition.
- isRequire: true,
- mainFields: ['main'],
+ mainFields: ['main', 'module'],
root
}
@@ -108,7 +108,7 @@ async function instantiateModule(
// account for multiple pending deps and duplicate imports.
const pendingDeps: string[] = []
- const ssrImport = async (dep: string) => {
+ async function ssrImport(dep: string) {
if (dep[0] !== '.' && dep[0] !== '/') {
return nodeRequire(dep, mod.file, resolveOptions)
}
@@ -128,7 +128,7 @@ async function instantiateModule(
return moduleGraph.urlToModuleMap.get(dep)?.ssrModule
}
- const ssrDynamicImport = (dep: string) => {
+ function ssrDynamicImport(dep: string) {
// #3087 dynamic import vars is ignored at rewrite import path,
// so here need process relative path
if (dep[0] === '.') {
@@ -152,26 +152,25 @@ async function instantiateModule(
}
const ssrImportMeta = { url }
+ const ssrArguments = {
+ global: context.global,
+ [ssrModuleExportsKey]: ssrModule,
+ [ssrImportMetaKey]: ssrImportMeta,
+ [ssrImportKey]: ssrImport,
+ [ssrDynamicImportKey]: ssrDynamicImport,
+ [ssrExportAllKey]: ssrExportAll
+ }
+
try {
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- const AsyncFunction = async function () {}.constructor as typeof Function
- const initModule = new AsyncFunction(
- `global`,
- ssrModuleExportsKey,
- ssrImportMetaKey,
- ssrImportKey,
- ssrDynamicImportKey,
- ssrExportAllKey,
- result.code + `\n//# sourceURL=${mod.url}`
- )
- await initModule(
- context.global,
- ssrModule,
- ssrImportMeta,
- ssrImport,
- ssrDynamicImport,
- ssrExportAll
- )
+ const ssrModuleImpl = `(0,async function(${Object.keys(ssrArguments)}){\n${
+ result.code
+ }\n})`
+ const ssrModuleInit = vm.runInThisContext(ssrModuleImpl, {
+ filename: mod.file || mod.url,
+ columnOffset: 1,
+ displayErrors: false
+ })
+ await ssrModuleInit(...Object.values(ssrArguments))
} catch (e) {
const stacktrace = ssrRewriteStacktrace(e.stack, moduleGraph)
rebindErrorStacktrace(e, stacktrace)
@@ -188,17 +187,24 @@ async function instantiateModule(
return Object.freeze(ssrModule)
}
-function nodeRequire(
+async function nodeRequire(
id: string,
importer: string | null,
resolveOptions: InternalResolveOptions
) {
- const loadModule = Module.createRequire(importer || resolveOptions.root + '/')
+ let resolvedId: string | undefined
+
+ // Hook into `require` so that `resolveOptions` are respected.
+ // Note: ESM-only dependencies don't use this hook at all.
const unhookNodeResolve = hookNodeResolve(
(nodeResolve) => (id, parent, isMain, options) => {
if (id[0] === '.' || Module.builtinModules.includes(id)) {
return nodeResolve(id, parent, isMain, options)
}
+ // No parent exists when an ESM package imports a CJS package.
+ if (!parent) {
+ return id
+ }
const resolved = tryNodeResolve(id, parent.id, resolveOptions, false)
if (!resolved) {
throw Error(`Cannot find module '${id}' imported from '${parent.id}'`)
@@ -207,19 +213,14 @@ function nodeRequire(
}
)
- let mod: any
try {
- mod = loadModule(id)
+ // Resolve the import manually, to avoid the ESM resolver.
+ resolvedId = fs.realpathSync.native(
+ Module.createRequire(importer || resolveOptions.root + '/').resolve(id)
+ )
+ // TypeScript transforms dynamic `import` so we must use eval.
+ return await eval(`import("${resolvedId}")`)
} finally {
unhookNodeResolve()
}
-
- // rollup-style default import interop for cjs
- const defaultExport = mod.__esModule ? mod.default : mod
- return new Proxy(mod, {
- get(mod, prop) {
- if (prop === 'default') return defaultExport
- return mod[prop]
- }
- })
}