From ecbcf5f44a716fcb1b1524b219296f4f5fd65c2d Mon Sep 17 00:00:00 2001 From: shYkiSto Date: Tue, 19 Dec 2023 21:15:41 -0800 Subject: [PATCH] fix(ssr): resolve interlocking circular dependency issues --- packages/vite/src/node/server/index.ts | 8 +- packages/vite/src/node/ssr/ssrModuleLoader.ts | 103 +++++++++++------- 2 files changed, 67 insertions(+), 44 deletions(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index c7c5270067adbf..aec12d75620de6 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -485,13 +485,7 @@ export async function _createServer( return devHtmlTransformFn(server, url, html, originalUrl) }, async ssrLoadModule(url, opts?: { fixStacktrace?: boolean }) { - return ssrLoadModule( - url, - server, - undefined, - undefined, - opts?.fixStacktrace, - ) + return ssrLoadModule(url, server, undefined, opts?.fixStacktrace) }, ssrFixStacktrace(e) { ssrFixStacktrace(e, moduleGraph) diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index e836d1e5f5f788..01da7612e2d229 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -54,14 +54,13 @@ let fnDeclarationLineCount = 0 } const pendingModules = new Map>() -const pendingImports = new Map() +const pendingModuleDependencyGraph = new Map>() const importErrors = new WeakMap() export async function ssrLoadModule( url: string, server: ViteDevServer, context: SSRContext = { global }, - urlStack: string[] = [], fixStacktrace?: boolean, ): Promise { url = unwrapId(url) @@ -75,17 +74,11 @@ export async function ssrLoadModule( return pending } - const modulePromise = instantiateModule( - url, - server, - context, - urlStack, - fixStacktrace, - ) + const modulePromise = instantiateModule(url, server, context, fixStacktrace) pendingModules.set(url, modulePromise) modulePromise .catch(() => { - pendingImports.delete(url) + /* prevent unhandled promise rejection error from bubbling up */ }) .finally(() => { pendingModules.delete(url) @@ -97,7 +90,6 @@ async function instantiateModule( url: string, server: ViteDevServer, context: SSRContext = { global }, - urlStack: string[] = [], fixStacktrace?: boolean, ): Promise { const { moduleGraph } = server @@ -132,9 +124,6 @@ async function instantiateModule( url: pathToFileURL(mod.file!).toString(), } - urlStack = urlStack.concat(url) - const isCircular = (url: string) => urlStack.includes(url) - const { isProduction, resolve: { dedupe, preserveSymlinks }, @@ -160,10 +149,6 @@ async function instantiateModule( packageCache: server.config.packageCache, } - // 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, metadata?: SSRImportMetadata) => { try { if (dep[0] !== '.' && dep[0] !== '/') { @@ -171,27 +156,30 @@ async function instantiateModule( } // convert to rollup URL because `pendingImports`, `moduleGraph.urlToModuleMap` requires that dep = unwrapId(dep) - if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) { - pendingDeps.push(dep) - if (pendingDeps.length === 1) { - pendingImports.set(url, pendingDeps) - } - const mod = await ssrLoadModule( - dep, - server, - context, - urlStack, - fixStacktrace, - ) - if (pendingDeps.length === 1) { - pendingImports.delete(url) - } else { - pendingDeps.splice(pendingDeps.indexOf(dep), 1) + + // Handle any potential circular dependencies for static imports, preventing + // deadlock scenarios when two modules are indirectly waiting on one another + // to finish initializing. Dynamic imports are resolved at runtime, hence do + // not contribute to the static module dependency graph in the same way + if (!metadata?.isDynamicImport) { + addPendingModuleDependency(url, dep) + + // If there's a circular dependency formed as a result of the dep import, + // return the current state of the dependent module being initialized, in + // order to avoid interlocking circular dependencies hanging indefinitely + if (checkModuleDependencyExists(dep, url)) { + const depSsrModule = moduleGraph.urlToModuleMap.get(dep)?.ssrModule + if (!depSsrModule) { + // Technically, this should never happen under normal circumstances + throw new Error( + '[vite] The dependency module is not yet fully initialized due to circular dependency. This is a bug in Vite SSR', + ) + } + return depSsrModule } - // return local module to avoid race condition #5470 - return mod } - return moduleGraph.urlToModuleMap.get(dep)?.ssrModule + + return ssrLoadModule(dep, server, context, fixStacktrace) } catch (err) { // tell external error handler which mod was imported with error importErrors.set(err, { importee: dep }) @@ -278,11 +266,52 @@ async function instantiateModule( ) throw e + } finally { + pendingModuleDependencyGraph.delete(url) } return Object.freeze(ssrModule) } +function addPendingModuleDependency(originUrl: string, depUrl: string): void { + if (pendingModuleDependencyGraph.has(originUrl)) { + pendingModuleDependencyGraph.get(originUrl)!.add(depUrl) + } else { + pendingModuleDependencyGraph.set(originUrl, new Set([depUrl])) + } +} + +function checkModuleDependencyExists( + originUrl: string, + targetUrl: string, +): boolean { + const visited = new Set() + const stack = [originUrl] + + while (stack.length) { + const currentUrl = stack.pop()! + + if (currentUrl === targetUrl) { + return true + } + + if (!visited.has(currentUrl)) { + visited.add(currentUrl) + + const dependencies = pendingModuleDependencyGraph.get(currentUrl) + if (dependencies) { + for (const depUrl of dependencies) { + if (!visited.has(depUrl)) { + stack.push(depUrl) + } + } + } + } + } + + return false +} + // In node@12+ we can use dynamic import to load CJS and ESM async function nodeImport( id: string,