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] - } - }) }