Skip to content

Commit e83beff

Browse files
authored
fix(vite): refactor "module cache" to "evaluated modules", pass down module to "runInlinedModule" (#18092)
1 parent 5e56614 commit e83beff

File tree

11 files changed

+270
-313
lines changed

11 files changed

+270
-313
lines changed

docs/guide/api-environment.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ export class ModuleRunner {
644644
645645
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.
646646
647-
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.
647+
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.
648648
649649
**Example Usage:**
650650
@@ -704,7 +704,7 @@ export interface ModuleRunnerOptions {
704704
/**
705705
* Custom module cache. If not provided, it creates a separate module cache for each module runner instance.
706706
*/
707-
moduleCache?: ModuleCacheMap
707+
evaluatedModules?: EvaluatedModules
708708
}
709709
```
710710
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { cleanUrl, isWindows, slash, unwrapId } from '../shared/utils'
2+
import { SOURCEMAPPING_URL } from '../shared/constants'
3+
import { decodeBase64 } from './utils'
4+
import { DecodedMap } from './sourcemap/decoder'
5+
import type { ResolvedResult } from './types'
6+
7+
const MODULE_RUNNER_SOURCEMAPPING_REGEXP = new RegExp(
8+
`//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`,
9+
)
10+
11+
export class EvaluatedModuleNode {
12+
public importers = new Set<string>()
13+
public imports = new Set<string>()
14+
public evaluated = false
15+
public meta: ResolvedResult | undefined
16+
public promise: Promise<any> | undefined
17+
public exports: any | undefined
18+
public file: string
19+
public map: DecodedMap | undefined
20+
21+
constructor(
22+
public id: string,
23+
public url: string,
24+
) {
25+
this.file = cleanUrl(id)
26+
}
27+
}
28+
29+
export class EvaluatedModules {
30+
public readonly idToModuleMap = new Map<string, EvaluatedModuleNode>()
31+
public readonly fileToModulesMap = new Map<string, Set<EvaluatedModuleNode>>()
32+
public readonly urlToIdModuleMap = new Map<string, EvaluatedModuleNode>()
33+
34+
/**
35+
* Returns the module node by the resolved module ID. Usually, module ID is
36+
* the file system path with query and/or hash. It can also be a virtual module.
37+
*
38+
* Module runner graph will have 1 to 1 mapping with the server module graph.
39+
* @param id Resolved module ID
40+
*/
41+
public getModuleById(id: string): EvaluatedModuleNode | undefined {
42+
return this.idToModuleMap.get(id)
43+
}
44+
45+
/**
46+
* Returns all modules related to the file system path. Different modules
47+
* might have different query parameters or hash, so it's possible to have
48+
* multiple modules for the same file.
49+
* @param file The file system path of the module
50+
*/
51+
public getModulesByFile(file: string): Set<EvaluatedModuleNode> | undefined {
52+
return this.fileToModulesMap.get(file)
53+
}
54+
55+
/**
56+
* Returns the module node by the URL that was used in the import statement.
57+
* Unlike module graph on the server, the URL is not resolved and is used as is.
58+
* @param url Server URL that was used in the import statement
59+
*/
60+
public getModuleByUrl(url: string): EvaluatedModuleNode | undefined {
61+
return this.urlToIdModuleMap.get(unwrapId(url))
62+
}
63+
64+
/**
65+
* Ensure that module is in the graph. If the module is already in the graph,
66+
* it will return the existing module node. Otherwise, it will create a new
67+
* module node and add it to the graph.
68+
* @param id Resolved module ID
69+
* @param url URL that was used in the import statement
70+
*/
71+
public ensureModule(id: string, url: string): EvaluatedModuleNode {
72+
id = normalizeModuleId(id)
73+
if (this.idToModuleMap.has(id)) {
74+
const moduleNode = this.idToModuleMap.get(id)!
75+
this.urlToIdModuleMap.set(url, moduleNode)
76+
return moduleNode
77+
}
78+
const moduleNode = new EvaluatedModuleNode(id, url)
79+
this.idToModuleMap.set(id, moduleNode)
80+
this.urlToIdModuleMap.set(url, moduleNode)
81+
82+
const fileModules = this.fileToModulesMap.get(moduleNode.file) || new Set()
83+
fileModules.add(moduleNode)
84+
this.fileToModulesMap.set(moduleNode.file, fileModules)
85+
return moduleNode
86+
}
87+
88+
public invalidateModule(node: EvaluatedModuleNode): void {
89+
node.evaluated = false
90+
node.meta = undefined
91+
node.map = undefined
92+
node.promise = undefined
93+
node.exports = undefined
94+
// remove imports in case they are changed,
95+
// don't remove the importers because otherwise it will be empty after evaluation
96+
// this can create a bug when file was removed but it still triggers full-reload
97+
// we are fine with the bug for now because it's not a common case
98+
node.imports.clear()
99+
}
100+
101+
/**
102+
* Extracts the inlined source map from the module code and returns the decoded
103+
* source map. If the source map is not inlined, it will return null.
104+
* @param id Resolved module ID
105+
*/
106+
getModuleSourceMapById(id: string): DecodedMap | null {
107+
const mod = this.getModuleById(id)
108+
if (!mod) return null
109+
if (mod.map) return mod.map
110+
if (!mod.meta || !('code' in mod.meta)) return null
111+
const mapString = MODULE_RUNNER_SOURCEMAPPING_REGEXP.exec(
112+
mod.meta.code,
113+
)?.[1]
114+
if (!mapString) return null
115+
mod.map = new DecodedMap(JSON.parse(decodeBase64(mapString)), mod.file)
116+
return mod.map
117+
}
118+
119+
public clear(): void {
120+
this.idToModuleMap.clear()
121+
this.fileToModulesMap.clear()
122+
this.urlToIdModuleMap.clear()
123+
}
124+
}
125+
126+
// unique id that is not available as "$bare_import" like "test"
127+
const prefixedBuiltins = new Set(['node:test', 'node:sqlite'])
128+
129+
// transform file url to id
130+
// virtual:custom -> virtual:custom
131+
// \0custom -> \0custom
132+
// /root/id -> /id
133+
// /root/id.js -> /id.js
134+
// C:/root/id.js -> /id.js
135+
// C:\root\id.js -> /id.js
136+
function normalizeModuleId(file: string): string {
137+
if (prefixedBuiltins.has(file)) return file
138+
139+
// unix style, but Windows path still starts with the drive letter to check the root
140+
const unixFile = slash(file)
141+
.replace(/^\/@fs\//, isWindows ? '' : '/')
142+
.replace(/^node:/, '')
143+
.replace(/^\/+/, '/')
144+
145+
// if it's not in the root, keep it as a path, not a URL
146+
return unixFile.replace(/^file:\//, '/')
147+
}

packages/vite/src/module-runner/hmrHandler.ts

+17-16
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,21 @@ export async function handleHotPayload(
4343
}
4444
case 'full-reload': {
4545
const { triggeredBy } = payload
46-
const clearEntrypoints = triggeredBy
46+
const clearEntrypointUrls = triggeredBy
4747
? getModulesEntrypoints(
4848
runner,
4949
getModulesByFile(runner, slash(triggeredBy)),
5050
)
5151
: findAllEntrypoints(runner)
5252

53-
if (!clearEntrypoints.size) break
53+
if (!clearEntrypointUrls.size) break
5454

5555
hmrClient.logger.debug(`program reload`)
5656
await hmrClient.notifyListeners('vite:beforeFullReload', payload)
57-
runner.moduleCache.clear()
57+
runner.evaluatedModules.clear()
5858

59-
for (const id of clearEntrypoints) {
60-
await runner.import(id)
59+
for (const url of clearEntrypointUrls) {
60+
await runner.import(url)
6161
}
6262
break
6363
}
@@ -120,14 +120,12 @@ class Queue {
120120
}
121121
}
122122

123-
function getModulesByFile(runner: ModuleRunner, file: string) {
124-
const modules: string[] = []
125-
for (const [id, mod] of runner.moduleCache.entries()) {
126-
if (mod.meta && 'file' in mod.meta && mod.meta.file === file) {
127-
modules.push(id)
128-
}
123+
function getModulesByFile(runner: ModuleRunner, file: string): string[] {
124+
const nodes = runner.evaluatedModules.getModulesByFile(file)
125+
if (!nodes) {
126+
return []
129127
}
130-
return modules
128+
return [...nodes].map((node) => node.id)
131129
}
132130

133131
function getModulesEntrypoints(
@@ -139,9 +137,12 @@ function getModulesEntrypoints(
139137
for (const moduleId of modules) {
140138
if (visited.has(moduleId)) continue
141139
visited.add(moduleId)
142-
const module = runner.moduleCache.getByModuleId(moduleId)
140+
const module = runner.evaluatedModules.getModuleById(moduleId)
141+
if (!module) {
142+
continue
143+
}
143144
if (module.importers && !module.importers.size) {
144-
entrypoints.add(moduleId)
145+
entrypoints.add(module.url)
145146
continue
146147
}
147148
for (const importer of module.importers || []) {
@@ -155,9 +156,9 @@ function findAllEntrypoints(
155156
runner: ModuleRunner,
156157
entrypoints = new Set<string>(),
157158
): Set<string> {
158-
for (const [id, mod] of runner.moduleCache.entries()) {
159+
for (const mod of runner.evaluatedModules.idToModuleMap.values()) {
159160
if (mod.importers && !mod.importers.size) {
160-
entrypoints.add(id)
161+
entrypoints.add(mod.url)
161162
}
162163
}
163164
return entrypoints

packages/vite/src/module-runner/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// this file should re-export only things that don't rely on Node.js or other runner features
22

3-
export { ModuleCacheMap } from './moduleCache'
3+
export { EvaluatedModules, type EvaluatedModuleNode } from './evaluatedModules'
44
export { ModuleRunner } from './runner'
55
export { ESModulesEvaluator } from './esmEvaluator'
66
export { RemoteRunnerTransport } from './runnerTransport'
@@ -10,7 +10,6 @@ export type { HMRLogger, HMRConnection } from '../shared/hmr'
1010
export type {
1111
ModuleEvaluator,
1212
ModuleRunnerContext,
13-
ModuleCache,
1413
FetchResult,
1514
FetchFunction,
1615
FetchFunctionOptions,

0 commit comments

Comments
 (0)