From e83beff596072f9c7a42f6e2410f154668981d71 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 25 Sep 2024 15:24:08 +0200 Subject: [PATCH] fix(vite): refactor "module cache" to "evaluated modules", pass down module to "runInlinedModule" (#18092) --- docs/guide/api-environment.md | 4 +- .../src/module-runner/evaluatedModules.ts | 147 ++++++++++++++++ packages/vite/src/module-runner/hmrHandler.ts | 33 ++-- packages/vite/src/module-runner/index.ts | 3 +- .../vite/src/module-runner/moduleCache.ts | 165 ------------------ packages/vite/src/module-runner/runner.ts | 143 +++++++-------- .../module-runner/sourcemap/interceptor.ts | 14 +- packages/vite/src/module-runner/types.ts | 31 ++-- packages/vite/src/node/ssr/fetchModule.ts | 3 +- .../__tests__/server-source-maps.spec.ts | 2 +- packages/vite/src/node/ssr/ssrModuleLoader.ts | 38 ++-- 11 files changed, 270 insertions(+), 313 deletions(-) create mode 100644 packages/vite/src/module-runner/evaluatedModules.ts delete mode 100644 packages/vite/src/module-runner/moduleCache.ts diff --git a/docs/guide/api-environment.md b/docs/guide/api-environment.md index 470ce35b55cf35..70b9585eaa9aab 100644 --- a/docs/guide/api-environment.md +++ b/docs/guide/api-environment.md @@ -644,7 +644,7 @@ export class ModuleRunner { The module evaluator in `ModuleRunner` is responsible for executing the code. Vite exports `ESModulesEvaluator` out of the box, it uses `new AsyncFunction` to evaluate the code. You can provide your own implementation if your JavaScript runtime doesn't support unsafe evaluation. -Module runner exposes `import` method. When Vite server triggers `full-reload` HMR event, all affected modules will be re-executed. Be aware that Module Runner doesn't update `exports` object when this happens (it overrides it), you would need to run `import` or get the module from `moduleCache` again if you rely on having the latest `exports` object. +Module runner exposes `import` method. When Vite server triggers `full-reload` HMR event, all affected modules will be re-executed. Be aware that Module Runner doesn't update `exports` object when this happens (it overrides it), you would need to run `import` or get the module from `evaluatedModules` again if you rely on having the latest `exports` object. **Example Usage:** @@ -704,7 +704,7 @@ export interface ModuleRunnerOptions { /** * Custom module cache. If not provided, it creates a separate module cache for each module runner instance. */ - moduleCache?: ModuleCacheMap + evaluatedModules?: EvaluatedModules } ``` diff --git a/packages/vite/src/module-runner/evaluatedModules.ts b/packages/vite/src/module-runner/evaluatedModules.ts new file mode 100644 index 00000000000000..51902964cd2e28 --- /dev/null +++ b/packages/vite/src/module-runner/evaluatedModules.ts @@ -0,0 +1,147 @@ +import { cleanUrl, isWindows, slash, unwrapId } from '../shared/utils' +import { SOURCEMAPPING_URL } from '../shared/constants' +import { decodeBase64 } from './utils' +import { DecodedMap } from './sourcemap/decoder' +import type { ResolvedResult } from './types' + +const MODULE_RUNNER_SOURCEMAPPING_REGEXP = new RegExp( + `//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`, +) + +export class EvaluatedModuleNode { + public importers = new Set() + public imports = new Set() + public evaluated = false + public meta: ResolvedResult | undefined + public promise: Promise | undefined + public exports: any | undefined + public file: string + public map: DecodedMap | undefined + + constructor( + public id: string, + public url: string, + ) { + this.file = cleanUrl(id) + } +} + +export class EvaluatedModules { + public readonly idToModuleMap = new Map() + public readonly fileToModulesMap = new Map>() + public readonly urlToIdModuleMap = new Map() + + /** + * Returns the module node by the resolved module ID. Usually, module ID is + * the file system path with query and/or hash. It can also be a virtual module. + * + * Module runner graph will have 1 to 1 mapping with the server module graph. + * @param id Resolved module ID + */ + public getModuleById(id: string): EvaluatedModuleNode | undefined { + return this.idToModuleMap.get(id) + } + + /** + * Returns all modules related to the file system path. Different modules + * might have different query parameters or hash, so it's possible to have + * multiple modules for the same file. + * @param file The file system path of the module + */ + public getModulesByFile(file: string): Set | undefined { + return this.fileToModulesMap.get(file) + } + + /** + * Returns the module node by the URL that was used in the import statement. + * Unlike module graph on the server, the URL is not resolved and is used as is. + * @param url Server URL that was used in the import statement + */ + public getModuleByUrl(url: string): EvaluatedModuleNode | undefined { + return this.urlToIdModuleMap.get(unwrapId(url)) + } + + /** + * Ensure that module is in the graph. If the module is already in the graph, + * it will return the existing module node. Otherwise, it will create a new + * module node and add it to the graph. + * @param id Resolved module ID + * @param url URL that was used in the import statement + */ + public ensureModule(id: string, url: string): EvaluatedModuleNode { + id = normalizeModuleId(id) + if (this.idToModuleMap.has(id)) { + const moduleNode = this.idToModuleMap.get(id)! + this.urlToIdModuleMap.set(url, moduleNode) + return moduleNode + } + const moduleNode = new EvaluatedModuleNode(id, url) + this.idToModuleMap.set(id, moduleNode) + this.urlToIdModuleMap.set(url, moduleNode) + + const fileModules = this.fileToModulesMap.get(moduleNode.file) || new Set() + fileModules.add(moduleNode) + this.fileToModulesMap.set(moduleNode.file, fileModules) + return moduleNode + } + + public invalidateModule(node: EvaluatedModuleNode): void { + node.evaluated = false + node.meta = undefined + node.map = undefined + node.promise = undefined + node.exports = undefined + // remove imports in case they are changed, + // don't remove the importers because otherwise it will be empty after evaluation + // this can create a bug when file was removed but it still triggers full-reload + // we are fine with the bug for now because it's not a common case + node.imports.clear() + } + + /** + * Extracts the inlined source map from the module code and returns the decoded + * source map. If the source map is not inlined, it will return null. + * @param id Resolved module ID + */ + getModuleSourceMapById(id: string): DecodedMap | null { + const mod = this.getModuleById(id) + if (!mod) return null + if (mod.map) return mod.map + if (!mod.meta || !('code' in mod.meta)) return null + const mapString = MODULE_RUNNER_SOURCEMAPPING_REGEXP.exec( + mod.meta.code, + )?.[1] + if (!mapString) return null + mod.map = new DecodedMap(JSON.parse(decodeBase64(mapString)), mod.file) + return mod.map + } + + public clear(): void { + this.idToModuleMap.clear() + this.fileToModulesMap.clear() + this.urlToIdModuleMap.clear() + } +} + +// unique id that is not available as "$bare_import" like "test" +const prefixedBuiltins = new Set(['node:test', 'node:sqlite']) + +// transform file url to id +// virtual:custom -> virtual:custom +// \0custom -> \0custom +// /root/id -> /id +// /root/id.js -> /id.js +// C:/root/id.js -> /id.js +// C:\root\id.js -> /id.js +function normalizeModuleId(file: string): string { + if (prefixedBuiltins.has(file)) return file + + // unix style, but Windows path still starts with the drive letter to check the root + const unixFile = slash(file) + .replace(/^\/@fs\//, isWindows ? '' : '/') + .replace(/^node:/, '') + .replace(/^\/+/, '/') + + // if it's not in the root, keep it as a path, not a URL + return unixFile.replace(/^file:\//, '/') +} diff --git a/packages/vite/src/module-runner/hmrHandler.ts b/packages/vite/src/module-runner/hmrHandler.ts index eeed2f521cd55a..c75407b6ec2e61 100644 --- a/packages/vite/src/module-runner/hmrHandler.ts +++ b/packages/vite/src/module-runner/hmrHandler.ts @@ -43,21 +43,21 @@ export async function handleHotPayload( } case 'full-reload': { const { triggeredBy } = payload - const clearEntrypoints = triggeredBy + const clearEntrypointUrls = triggeredBy ? getModulesEntrypoints( runner, getModulesByFile(runner, slash(triggeredBy)), ) : findAllEntrypoints(runner) - if (!clearEntrypoints.size) break + if (!clearEntrypointUrls.size) break hmrClient.logger.debug(`program reload`) await hmrClient.notifyListeners('vite:beforeFullReload', payload) - runner.moduleCache.clear() + runner.evaluatedModules.clear() - for (const id of clearEntrypoints) { - await runner.import(id) + for (const url of clearEntrypointUrls) { + await runner.import(url) } break } @@ -120,14 +120,12 @@ class Queue { } } -function getModulesByFile(runner: ModuleRunner, file: string) { - const modules: string[] = [] - for (const [id, mod] of runner.moduleCache.entries()) { - if (mod.meta && 'file' in mod.meta && mod.meta.file === file) { - modules.push(id) - } +function getModulesByFile(runner: ModuleRunner, file: string): string[] { + const nodes = runner.evaluatedModules.getModulesByFile(file) + if (!nodes) { + return [] } - return modules + return [...nodes].map((node) => node.id) } function getModulesEntrypoints( @@ -139,9 +137,12 @@ function getModulesEntrypoints( for (const moduleId of modules) { if (visited.has(moduleId)) continue visited.add(moduleId) - const module = runner.moduleCache.getByModuleId(moduleId) + const module = runner.evaluatedModules.getModuleById(moduleId) + if (!module) { + continue + } if (module.importers && !module.importers.size) { - entrypoints.add(moduleId) + entrypoints.add(module.url) continue } for (const importer of module.importers || []) { @@ -155,9 +156,9 @@ function findAllEntrypoints( runner: ModuleRunner, entrypoints = new Set(), ): Set { - for (const [id, mod] of runner.moduleCache.entries()) { + for (const mod of runner.evaluatedModules.idToModuleMap.values()) { if (mod.importers && !mod.importers.size) { - entrypoints.add(id) + entrypoints.add(mod.url) } } return entrypoints diff --git a/packages/vite/src/module-runner/index.ts b/packages/vite/src/module-runner/index.ts index 836b261a8b8687..7130795f862767 100644 --- a/packages/vite/src/module-runner/index.ts +++ b/packages/vite/src/module-runner/index.ts @@ -1,6 +1,6 @@ // this file should re-export only things that don't rely on Node.js or other runner features -export { ModuleCacheMap } from './moduleCache' +export { EvaluatedModules, type EvaluatedModuleNode } from './evaluatedModules' export { ModuleRunner } from './runner' export { ESModulesEvaluator } from './esmEvaluator' export { RemoteRunnerTransport } from './runnerTransport' @@ -10,7 +10,6 @@ export type { HMRLogger, HMRConnection } from '../shared/hmr' export type { ModuleEvaluator, ModuleRunnerContext, - ModuleCache, FetchResult, FetchFunction, FetchFunctionOptions, diff --git a/packages/vite/src/module-runner/moduleCache.ts b/packages/vite/src/module-runner/moduleCache.ts deleted file mode 100644 index 9c5c188e3da8cc..00000000000000 --- a/packages/vite/src/module-runner/moduleCache.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { isWindows, slash, withTrailingSlash } from '../shared/utils' -import { SOURCEMAPPING_URL } from '../shared/constants' -import { decodeBase64 } from './utils' -import { DecodedMap } from './sourcemap/decoder' -import type { ModuleCache } from './types' - -const MODULE_RUNNER_SOURCEMAPPING_REGEXP = new RegExp( - `//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`, -) - -export class ModuleCacheMap extends Map { - private root: string - - constructor(root: string, entries?: [string, ModuleCache][]) { - super(entries) - this.root = withTrailingSlash(root) - } - - normalize(fsPath: string): string { - return normalizeModuleId(fsPath, this.root) - } - - /** - * Assign partial data to the map - */ - update(fsPath: string, mod: ModuleCache): this { - fsPath = this.normalize(fsPath) - if (!super.has(fsPath)) this.setByModuleId(fsPath, mod) - else Object.assign(super.get(fsPath)!, mod) - return this - } - - setByModuleId(modulePath: string, mod: ModuleCache): this { - return super.set(modulePath, mod) - } - - override set(fsPath: string, mod: ModuleCache): this { - return this.setByModuleId(this.normalize(fsPath), mod) - } - - getByModuleId(modulePath: string): ModuleCache { - if (!super.has(modulePath)) this.setByModuleId(modulePath, {}) - - const mod = super.get(modulePath)! - if (!mod.imports) { - Object.assign(mod, { - imports: new Set(), - importers: new Set(), - timestamp: 0, - }) - } - return mod - } - - override get(fsPath: string): ModuleCache { - return this.getByModuleId(this.normalize(fsPath)) - } - - deleteByModuleId(modulePath: string): boolean { - return super.delete(modulePath) - } - - override delete(fsPath: string): boolean { - return this.deleteByModuleId(this.normalize(fsPath)) - } - - invalidateUrl(id: string): void { - const module = this.get(id) - this.invalidateModule(module) - } - - invalidateModule(module: ModuleCache): void { - module.evaluated = false - module.meta = undefined - module.map = undefined - module.promise = undefined - module.exports = undefined - // remove imports in case they are changed, - // don't remove the importers because otherwise it will be empty after evaluation - // this can create a bug when file was removed but it still triggers full-reload - // we are fine with the bug for now because it's not a common case - module.imports?.clear() - } - - /** - * Invalidate modules that dependent on the given modules, up to the main entry - */ - invalidateDepTree( - ids: string[] | Set, - invalidated = new Set(), - ): Set { - for (const _id of ids) { - const id = this.normalize(_id) - if (invalidated.has(id)) continue - invalidated.add(id) - const mod = super.get(id) - if (mod?.importers) this.invalidateDepTree(mod.importers, invalidated) - this.invalidateUrl(id) - } - return invalidated - } - - /** - * Invalidate dependency modules of the given modules, down to the bottom-level dependencies - */ - invalidateSubDepTree( - ids: string[] | Set, - invalidated = new Set(), - ): Set { - for (const _id of ids) { - const id = this.normalize(_id) - if (invalidated.has(id)) continue - invalidated.add(id) - const subIds = Array.from(super.entries()) - .filter(([, mod]) => mod.importers?.has(id)) - .map(([key]) => key) - if (subIds.length) { - this.invalidateSubDepTree(subIds, invalidated) - } - super.delete(id) - } - return invalidated - } - - getSourceMap(moduleId: string): null | DecodedMap { - const mod = this.get(moduleId) - if (mod.map) return mod.map - if (!mod.meta || !('code' in mod.meta)) return null - const mapString = MODULE_RUNNER_SOURCEMAPPING_REGEXP.exec( - mod.meta.code, - )?.[1] - if (!mapString) return null - const baseFile = mod.meta.file || moduleId.split('?')[0] - mod.map = new DecodedMap(JSON.parse(decodeBase64(mapString)), baseFile) - return mod.map - } -} - -// unique id that is not available as "$bare_import" like "test" -const prefixedBuiltins = new Set(['node:test', 'node:sqlite']) - -// transform file url to id -// virtual:custom -> virtual:custom -// \0custom -> \0custom -// /root/id -> /id -// /root/id.js -> /id.js -// C:/root/id.js -> /id.js -// C:\root\id.js -> /id.js -function normalizeModuleId(file: string, root: string): string { - if (prefixedBuiltins.has(file)) return file - - // unix style, but Windows path still starts with the drive letter to check the root - let unixFile = slash(file) - .replace(/^\/@fs\//, isWindows ? '' : '/') - .replace(/^node:/, '') - .replace(/^\/+/, '/') - - if (unixFile.startsWith(root)) { - // keep slash - unixFile = unixFile.slice(root.length - 1) - } - - // if it's not in the root, keep it as a path, not a URL - return unixFile.replace(/^file:\//, '/') -} diff --git a/packages/vite/src/module-runner/runner.ts b/packages/vite/src/module-runner/runner.ts index 1df16d3ffd094e..4157a031924633 100644 --- a/packages/vite/src/module-runner/runner.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -1,10 +1,10 @@ import type { ViteHotContext } from 'types/hot' import { HMRClient, HMRContext } from '../shared/hmr' -import { cleanUrl, isPrimitive, isWindows, unwrapId } from '../shared/utils' +import { cleanUrl, isPrimitive, isWindows } from '../shared/utils' import { analyzeImportedModDifference } from '../shared/ssrTransform' -import { ModuleCacheMap } from './moduleCache' +import type { EvaluatedModuleNode } from './evaluatedModules' +import { EvaluatedModules } from './evaluatedModules' import type { - ModuleCache, ModuleEvaluator, ModuleRunnerContext, ModuleRunnerImportMeta, @@ -36,15 +36,9 @@ interface ModuleRunnerDebugger { } export class ModuleRunner { - /** - * Holds the cache of modules - * Keys of the map are ids - */ - public moduleCache: ModuleCacheMap + public evaluatedModules: EvaluatedModules public hmrClient?: HMRClient - private readonly urlToIdMap = new Map() - private readonly fileToIdMap = new Map() private readonly envProxy = new Proxy({} as any, { get(_, p) { throw new Error( @@ -55,7 +49,10 @@ export class ModuleRunner { private readonly transport: RunnerTransport private readonly resetSourceMapSupport?: () => void private readonly root: string - private readonly moduleInfoCache = new Map>() + private readonly concurrentModuleNodePromises = new Map< + string, + Promise + >() private destroyed = false @@ -66,7 +63,7 @@ export class ModuleRunner { ) { const root = this.options.root this.root = root[root.length - 1] === '/' ? root : `${root}/` - this.moduleCache = options.moduleCache ?? new ModuleCacheMap(options.root) + this.evaluatedModules = options.evaluatedModules ?? new EvaluatedModules() this.transport = options.transport if (typeof options.hmr === 'object') { this.hmrClient = new HMRClient( @@ -95,8 +92,7 @@ export class ModuleRunner { * Clear all caches including HMR listeners. */ public clearCache(): void { - this.moduleCache.clear() - this.urlToIdMap.clear() + this.evaluatedModules.clear() this.hmrClient?.clear() } @@ -126,13 +122,13 @@ export class ModuleRunner { if (!('externalize' in fetchResult)) { return exports } - const { url: id, type } = fetchResult + const { url, type } = fetchResult if (type !== 'module' && type !== 'commonjs') return exports - analyzeImportedModDifference(exports, id, type, metadata) + analyzeImportedModDifference(exports, url, type, metadata) return exports } - private isCircularModule(mod: Required) { + private isCircularModule(mod: EvaluatedModuleNode) { for (const importedFile of mod.imports) { if (mod.importers.has(importedFile)) { return true @@ -154,10 +150,9 @@ export class ModuleRunner { if (importer === moduleUrl) { return true } - const mod = this.moduleCache.getByModuleId( - importer, - ) as Required + const mod = this.evaluatedModules.getModuleById(importer) if ( + mod && mod.importers.size && this.isCircularImport(mod.importers, moduleUrl, visited) ) { @@ -168,14 +163,13 @@ export class ModuleRunner { } private async cachedRequest( - id: string, - mod_: ModuleCache, + url: string, + mod: EvaluatedModuleNode, callstack: string[] = [], metadata?: SSRImportMetadata, ): Promise { - const mod = mod_ as Required const meta = mod.meta! - const moduleUrl = meta.url + const moduleId = meta.id const { importers } = mod @@ -185,9 +179,9 @@ export class ModuleRunner { // check circular dependency if ( - callstack.includes(moduleUrl) || + callstack.includes(moduleId) || this.isCircularModule(mod) || - this.isCircularImport(importers, moduleUrl) + this.isCircularImport(importers, moduleId) ) { if (mod.exports) return this.processImport(mod.exports, meta, metadata) } @@ -196,13 +190,13 @@ export class ModuleRunner { if (this.debug) { debugTimer = setTimeout(() => { const getStack = () => - `stack:\n${[...callstack, moduleUrl] + `stack:\n${[...callstack, moduleId] .reverse() .map((p) => ` - ${p}`) .join('\n')}` this.debug!( - `[module runner] module ${moduleUrl} takes over 2s to load.\n${getStack()}`, + `[module runner] module ${moduleId} takes over 2s to load.\n${getStack()}`, ) }, 2000) } @@ -212,7 +206,7 @@ export class ModuleRunner { if (mod.promise) return this.processImport(await mod.promise, meta, metadata) - const promise = this.directRequest(id, mod, callstack) + const promise = this.directRequest(url, mod, callstack) mod.promise = promise mod.evaluated = false return this.processImport(await promise, meta, metadata) @@ -222,23 +216,21 @@ export class ModuleRunner { } } - private async cachedModule(url: string, importer?: string) { + private async cachedModule( + url: string, + importer?: string, + ): Promise { url = normalizeAbsoluteUrl(url, this.root) - const normalized = this.urlToIdMap.get(url) - let cachedModule = normalized && this.moduleCache.getByModuleId(normalized) - if (!cachedModule) { - cachedModule = this.moduleCache.getByModuleId(url) - } - - let cached = this.moduleInfoCache.get(url) + let cached = this.concurrentModuleNodePromises.get(url) if (!cached) { + const cachedModule = this.evaluatedModules.getModuleByUrl(url) cached = this.getModuleInformation(url, importer, cachedModule).finally( () => { - this.moduleInfoCache.delete(url) + this.concurrentModuleNodePromises.delete(url) }, ) - this.moduleInfoCache.set(url, cached) + this.concurrentModuleNodePromises.set(url, cached) } else { this.debug?.('[module runner] using cached module info for', url) } @@ -249,8 +241,8 @@ export class ModuleRunner { private async getModuleInformation( url: string, importer: string | undefined, - cachedModule: ModuleCache | undefined, - ): Promise { + cachedModule: EvaluatedModuleNode | undefined, + ): Promise { if (this.destroyed) { throw new Error(`Vite module runner has been destroyed.`) } @@ -278,60 +270,48 @@ export class ModuleRunner { return cachedModule } - // base moduleId on "file" and not on id - // if `import(variable)` is called it's possible that it doesn't have an extension for example - // if we used id for that, then a module will be duplicated - const idQuery = url.split('?')[1] - const query = idQuery ? `?${idQuery}` : '' - const file = 'file' in fetchedModule ? fetchedModule.file : undefined - const fileId = file ? `${file}${query}` : url - const moduleUrl = this.moduleCache.normalize(fileId) - const mod = this.moduleCache.getByModuleId(moduleUrl) + const moduleId = + 'externalize' in fetchedModule + ? fetchedModule.externalize + : fetchedModule.id + const moduleUrl = 'url' in fetchedModule ? fetchedModule.url : url + const module = this.evaluatedModules.ensureModule(moduleId, moduleUrl) if ('invalidate' in fetchedModule && fetchedModule.invalidate) { - this.moduleCache.invalidateModule(mod) + this.evaluatedModules.invalidateModule(module) } fetchedModule.url = moduleUrl - mod.meta = fetchedModule + fetchedModule.id = moduleId + module.meta = fetchedModule - if (file) { - const fileModules = this.fileToIdMap.get(file) || [] - fileModules.push(moduleUrl) - this.fileToIdMap.set(file, fileModules) - } - - this.urlToIdMap.set(url, moduleUrl) - this.urlToIdMap.set(unwrapId(url), moduleUrl) - return mod + return module } // override is allowed, consider this a public API protected async directRequest( - id: string, - mod: ModuleCache, + url: string, + mod: EvaluatedModuleNode, _callstack: string[], ): Promise { const fetchResult = mod.meta! - const moduleUrl = fetchResult.url - const callstack = [..._callstack, moduleUrl] + const moduleId = fetchResult.id + const callstack = [..._callstack, moduleId] const request = async (dep: string, metadata?: SSRImportMetadata) => { - const importer = ('file' in fetchResult && fetchResult.file) || moduleUrl - const fetchedModule = await this.cachedModule(dep, importer) - const resolvedId = fetchedModule.meta!.url - const depMod = this.moduleCache.getByModuleId(resolvedId) - depMod.importers!.add(moduleUrl) - mod.imports!.add(resolvedId) - - return this.cachedRequest(dep, fetchedModule, callstack, metadata) + const importer = ('file' in fetchResult && fetchResult.file) || moduleId + const depMod = await this.cachedModule(dep, importer) + depMod.importers.add(moduleId) + mod.imports.add(depMod.id) + + return this.cachedRequest(dep, depMod, callstack, metadata) } const dynamicRequest = async (dep: string) => { // it's possible to provide an object with toString() method inside import() dep = String(dep) if (dep[0] === '.') { - dep = posixResolve(posixDirname(id), dep) + dep = posixResolve(posixDirname(url), dep) } return request(dep, { isDynamicImport: true }) } @@ -349,13 +329,13 @@ export class ModuleRunner { if (code == null) { const importer = callstack[callstack.length - 2] throw new Error( - `[module runner] Failed to load "${id}"${ + `[module runner] Failed to load "${url}"${ importer ? ` imported from ${importer}` : '' }`, ) } - const modulePath = cleanUrl(file || moduleUrl) + const modulePath = cleanUrl(file || moduleId) // disambiguate the `:/` on windows: see nodejs/node#31710 const href = posixPathToFileHref(modulePath) const filename = modulePath @@ -372,7 +352,10 @@ export class ModuleRunner { }, // should be replaced during transformation glob() { - throw new Error('[module runner] "import.meta.glob" is not supported.') + throw new Error( + `[module runner] "import.meta.glob" is statically replaced during ` + + `file transformation. Make sure to reference it by the full name.`, + ) }, } const exports = Object.create(null) @@ -392,8 +375,8 @@ export class ModuleRunner { if (!this.hmrClient) { throw new Error(`[module runner] HMR client was destroyed.`) } - this.debug?.('[module runner] creating hmr context for', moduleUrl) - hotContext ||= new HMRContext(this.hmrClient, moduleUrl) + this.debug?.('[module runner] creating hmr context for', mod.url) + hotContext ||= new HMRContext(this.hmrClient, mod.url) return hotContext }, set: (value) => { @@ -412,7 +395,7 @@ export class ModuleRunner { this.debug?.('[module runner] executing', href) - await this.evaluator.runInlinedModule(context, code, id) + await this.evaluator.runInlinedModule(context, code, mod) return exports } diff --git a/packages/vite/src/module-runner/sourcemap/interceptor.ts b/packages/vite/src/module-runner/sourcemap/interceptor.ts index 1041e6b7276459..eb4f41f83e68a2 100644 --- a/packages/vite/src/module-runner/sourcemap/interceptor.ts +++ b/packages/vite/src/module-runner/sourcemap/interceptor.ts @@ -1,7 +1,7 @@ import type { OriginalMapping } from '@jridgewell/trace-mapping' import type { ModuleRunner } from '../runner' import { posixDirname, posixResolve } from '../utils' -import type { ModuleCacheMap } from '../moduleCache' +import type { EvaluatedModules } from '../evaluatedModules' import { slash } from '../../shared/utils' import { DecodedMap, getOriginalPosition } from './decoder' @@ -21,7 +21,7 @@ export interface InterceptorOptions { const sourceMapCache: Record = {} const fileContentsCache: Record = {} -const moduleGraphs = new Set() +const evaluatedModulesCache = new Set() const retrieveFileHandlers = new Set() const retrieveSourceMapHandlers = new Set() @@ -46,11 +46,11 @@ let overridden = false const originalPrepare = Error.prepareStackTrace function resetInterceptor(runner: ModuleRunner, options: InterceptorOptions) { - moduleGraphs.delete(runner.moduleCache) + evaluatedModulesCache.delete(runner.evaluatedModules) if (options.retrieveFile) retrieveFileHandlers.delete(options.retrieveFile) if (options.retrieveSourceMap) retrieveSourceMapHandlers.delete(options.retrieveSourceMap) - if (moduleGraphs.size === 0) { + if (evaluatedModulesCache.size === 0) { Error.prepareStackTrace = originalPrepare overridden = false } @@ -64,7 +64,7 @@ export function interceptStackTrace( Error.prepareStackTrace = prepareStackTrace overridden = true } - moduleGraphs.add(runner.moduleCache) + evaluatedModulesCache.add(runner.evaluatedModules) if (options.retrieveFile) retrieveFileHandlers.add(options.retrieveFile) if (options.retrieveSourceMap) retrieveSourceMapHandlers.add(options.retrieveSourceMap) @@ -102,8 +102,8 @@ function supportRelativeURL(file: string, url: string) { } function getRunnerSourceMap(position: OriginalMapping): CachedMapEntry | null { - for (const moduleCache of moduleGraphs) { - const sourceMap = moduleCache.getSourceMap(position.source!) + for (const moduleGraph of evaluatedModulesCache) { + const sourceMap = moduleGraph.getModuleSourceMapById(position.source!) if (sourceMap) { return { url: position.source, diff --git a/packages/vite/src/module-runner/types.ts b/packages/vite/src/module-runner/types.ts index 18ac314cbf1e95..70bda14adc96de 100644 --- a/packages/vite/src/module-runner/types.ts +++ b/packages/vite/src/module-runner/types.ts @@ -5,7 +5,7 @@ import type { DefineImportMetadata, SSRImportMetadata, } from '../shared/ssrTransform' -import type { ModuleCacheMap } from './moduleCache' +import type { EvaluatedModuleNode, EvaluatedModules } from './evaluatedModules' import type { ssrDynamicImportKey, ssrExportAllKey, @@ -13,7 +13,6 @@ import type { ssrImportMetaKey, ssrModuleExportsKey, } from './constants' -import type { DecodedMap } from './sourcemap/decoder' import type { InterceptorOptions } from './sourcemap/interceptor' import type { RunnerTransport } from './runnerTransport' @@ -54,12 +53,12 @@ export interface ModuleEvaluator { * Run code that was transformed by Vite. * @param context Function context * @param code Transformed code - * @param id ID that was used to fetch the module + * @param module The module node */ runInlinedModule( context: ModuleRunnerContext, code: string, - id: string, + module: Readonly, ): Promise /** * Run externalized module. @@ -68,19 +67,6 @@ export interface ModuleEvaluator { runExternalModule(file: string): Promise } -export interface ModuleCache { - promise?: Promise - exports?: any - evaluated?: boolean - map?: DecodedMap - meta?: ResolvedResult - /** - * Module ids that imports this module - */ - importers?: Set - imports?: Set -} - export type FetchResult = | CachedFetchResult | ExternalFetchResult @@ -105,7 +91,7 @@ export interface ExternalFetchResult { * Type of the module. Will be used to determine if import statement is correct. * For example, if Vite needs to throw an error if variable is not actually exported */ - type?: 'module' | 'commonjs' | 'builtin' | 'network' + type: 'module' | 'commonjs' | 'builtin' | 'network' } export interface ViteFetchResult { @@ -123,7 +109,11 @@ export interface ViteFetchResult { /** * Module ID in the server module graph. */ - serverId: string + id: string + /** + * Module URL used in the import. + */ + url: string /** * Invalidate module on the client side. */ @@ -132,6 +122,7 @@ export interface ViteFetchResult { export type ResolvedResult = (ExternalFetchResult | ViteFetchResult) & { url: string + id: string } export type FetchFunction = ( @@ -182,7 +173,7 @@ export interface ModuleRunnerOptions { /** * Custom module cache. If not provided, creates a separate module cache for each ModuleRunner instance. */ - moduleCache?: ModuleCacheMap + evaluatedModules?: EvaluatedModules } export interface ImportMetaEnv { diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index 8cf533906419b4..4e04718ecbe903 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -137,7 +137,8 @@ export async function fetchModule( return { code: result.code, file: mod.file, - serverId: mod.id!, + id: mod.id!, + url: mod.url, invalidate: !cached, } } diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts index ece2fc12242753..758072f25a83be 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts @@ -52,7 +52,7 @@ describe('module runner initialization', async () => { resolvePath(import.meta.url, './fixtures/throws-error-method.ts'), (code) => '\n\n\n\n\n' + code + '\n', ) - runner.moduleCache.clear() + runner.evaluatedModules.clear() server.environments.ssr.moduleGraph.invalidateAll() const methodErrorNew = await getError(async () => { diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index aa155ad35d29bb..c60c03d4f8819a 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -1,8 +1,9 @@ import colors from 'picocolors' -import type { ModuleCache } from 'vite/module-runner' +import type { EvaluatedModuleNode } from 'vite/module-runner' import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' import type { ViteDevServer } from '../server' import { unwrapId } from '../../shared/utils' +import type { DevEnvironment } from '../server/environment' import { ssrFixStacktrace } from './ssrStacktrace' type SSRModule = Record @@ -12,13 +13,14 @@ export async function ssrLoadModule( server: ViteDevServer, fixStacktrace?: boolean, ): Promise { - server._ssrCompatModuleRunner ||= new SSRCompatModuleRunner(server) + const environment = server.environments.ssr + server._ssrCompatModuleRunner ||= new SSRCompatModuleRunner(environment) url = unwrapId(url) return instantiateModule( url, server._ssrCompatModuleRunner, - server, + environment, fixStacktrace, ) } @@ -26,10 +28,9 @@ export async function ssrLoadModule( async function instantiateModule( url: string, runner: ModuleRunner, - server: ViteDevServer, + environment: DevEnvironment, fixStacktrace?: boolean, ): Promise { - const environment = server.environments.ssr const mod = await environment.moduleGraph.ensureEntryFromUrl(url) if (mod.ssrError) { @@ -47,7 +48,7 @@ async function instantiateModule( colors.red(`Error when evaluating SSR module ${url}:\n|- ${e.stack}\n`), { timestamp: true, - clear: server.config.clearScreen, + clear: environment.config.clearScreen, error: e, }, ) @@ -57,13 +58,13 @@ async function instantiateModule( } class SSRCompatModuleRunner extends ModuleRunner { - constructor(private server: ViteDevServer) { + constructor(private environment: DevEnvironment) { super( { - root: server.environments.ssr.config.root, + root: environment.config.root, transport: { fetchModule: (id, importer, options) => - server.environments.ssr.fetchModule(id, importer, options), + environment.fetchModule(id, importer, options), }, sourcemapInterceptor: false, hmr: false, @@ -73,25 +74,24 @@ class SSRCompatModuleRunner extends ModuleRunner { } protected override async directRequest( - id: string, - mod: ModuleCache, - _callstack: string[], + url: string, + mod: EvaluatedModuleNode, + callstack: string[], ): Promise { - const serverId = mod.meta && 'serverId' in mod.meta && mod.meta.serverId + const id = mod.meta && 'id' in mod.meta && mod.meta.id // serverId doesn't exist for external modules - if (!serverId) { - return super.directRequest(id, mod, _callstack) + if (!id) { + return super.directRequest(url, mod, callstack) } - const viteMod = - this.server.environments.ssr.moduleGraph.getModuleById(serverId) + const viteMod = this.environment.moduleGraph.getModuleById(id) if (!viteMod) { - return super.directRequest(id, mod, _callstack) + return super.directRequest(id, mod, callstack) } try { - const exports = await super.directRequest(id, mod, _callstack) + const exports = await super.directRequest(id, mod, callstack) viteMod.ssrModule = exports return exports } catch (err) {