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/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/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/__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/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 b68b104461c0b3..789ca8f56e9a8b 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -1,7 +1,9 @@ +import vm from 'vm' 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 +13,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 @@ -19,6 +23,7 @@ interface SSRContext { type SSRModule = Record const pendingModules = new Map>() +const pendingImports = new Map() export async function ssrLoadModule( url: string, @@ -28,13 +33,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 +44,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 +80,61 @@ async function instantiateModule( } Object.defineProperty(ssrModule, '__esModule', { value: true }) - const isExternal = (dep: string) => dep[0] !== '.' && dep[0] !== '/' + // Tolerate circular imports by ensuring the module can be + // referenced before it's been instantiated. + mod.ssrModule = ssrModule + + 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, + mainFields: ['main', 'module'], + root + } - await Promise.all( - result.deps!.map((dep) => { - if (!isExternal(dep)) { - return ssrLoadModule(dep, server, context, urlStack.concat(url)) - } - }) - ) + urlStack = urlStack.concat(url) + const isCircular = (url: string) => urlStack.includes(url) - const ssrImportMeta = { url } + // Since dynamic imports can happen in parallel, we need to + // account for multiple pending deps and duplicate imports. + const pendingDeps: string[] = [] - const ssrImport = (dep: string) => { - if (isExternal(dep)) { - return nodeRequire(dep, mod.file, server.config.root) - } else { - return moduleGraph.urlToModuleMap.get(unwrapId(dep))?.ssrModule + async function ssrImport(dep: string) { + if (dep[0] !== '.' && dep[0] !== '/') { + return nodeRequire(dep, mod.file, resolveOptions) } + 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)) + function ssrDynamicImport(dep: string) { + // #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) { @@ -123,23 +151,26 @@ async function instantiateModule( } } + const ssrImportMeta = { url } + const ssrArguments = { + global: context.global, + [ssrModuleExportsKey]: ssrModule, + [ssrImportMetaKey]: ssrImportMeta, + [ssrImportKey]: ssrImport, + [ssrDynamicImportKey]: ssrDynamicImport, + [ssrExportAllKey]: ssrExportAll + } + try { - new Function( - `global`, - ssrModuleExportsKey, - ssrImportMetaKey, - ssrImportKey, - ssrDynamicImportKey, - ssrExportAllKey, - result.code + `\n//# sourceURL=${mod.url}` - )( - 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) @@ -153,35 +184,43 @@ 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) { - const mod = require(resolve(id, importer, root)) - const defaultExport = mod.__esModule ? mod.default : mod - // rollup-style default import interop for cjs - return new Proxy(mod, { - get(mod, prop) { - if (prop === 'default') return defaultExport - return mod[prop] +async function nodeRequire( + id: string, + importer: string | null, + resolveOptions: InternalResolveOptions +) { + 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}'`) + } + return resolved.id } - }) -} - -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 + try { + // 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() } - const resolveDir = - importer && fs.existsSync(cleanUrl(importer)) - ? path.dirname(importer) - : root - const resolved = resolveFrom(id, resolveDir, true) - resolveCache.set(key, resolved) - return resolved } 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});`) } } }