Skip to content

Commit

Permalink
fix: correct soft invalidation
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Apr 15, 2024
1 parent 5b635e6 commit e97ac1e
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 141 deletions.
9 changes: 7 additions & 2 deletions packages/vite/src/module-runner/moduleCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class ModuleCacheMap extends Map<string, ModuleCache> {
Object.assign(mod, {
imports: new Set(),
importers: new Set(),
timestamp: 0,
})
}
return mod
Expand All @@ -63,8 +64,12 @@ export class ModuleCacheMap extends Map<string, ModuleCache> {
return this.deleteByModuleId(this.normalize(fsPath))
}

invalidate(id: string): void {
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
Expand All @@ -90,7 +95,7 @@ export class ModuleCacheMap extends Map<string, ModuleCache> {
invalidated.add(id)
const mod = super.get(id)
if (mod?.importers) this.invalidateDepTree(mod.importers, invalidated)
this.invalidate(id)
this.invalidateUrl(id)
}
return invalidated
}
Expand Down
121 changes: 61 additions & 60 deletions packages/vite/src/module-runner/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
} from '../shared/ssrTransform'
import { ModuleCacheMap } from './moduleCache'
import type {
FetchResult,
ModuleCache,
ModuleEvaluator,
ModuleRunnerContext,
Expand All @@ -24,6 +23,7 @@ import type {
SSRImportMetadata,
} from './types'
import {
parseUrl,
posixDirname,
posixPathToFileHref,
posixResolve,
Expand Down Expand Up @@ -53,19 +53,19 @@ export class ModuleRunner {
public moduleCache: ModuleCacheMap
public hmrClient?: HMRClient

private idToUrlMap = new Map<string, string>()
private fileToIdMap = new Map<string, string[]>()
private envProxy = new Proxy({} as any, {
private readonly urlToIdMap = new Map<string, string>()
private readonly fileToIdMap = new Map<string, string[]>()
private readonly envProxy = new Proxy({} as any, {
get(_, p) {
throw new Error(
`[module runner] Dynamic access of "import.meta.env" is not supported. Please, use "import.meta.env.${String(p)}" instead.`,
)
},
})
private transport: RunnerTransport
private readonly transport: RunnerTransport
private readonly resetSourceMapSupport?: () => void

private _destroyed = false
private _resetSourceMapSupport?: () => void
private destroyed = false

constructor(
public options: ModuleRunnerOptions,
Expand All @@ -80,18 +80,20 @@ export class ModuleRunner {
? silentConsole
: options.hmr.logger || console,
options.hmr.connection,
({ acceptedPath, invalidates }) => {
this.moduleCache.invalidate(acceptedPath)
if (invalidates) {
this.invalidateFiles(invalidates)
}
return this.import(acceptedPath)
({ acceptedPath, explicitImportRequired, timestamp }) => {
const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`)
const url =
acceptedPathWithoutQuery +
`?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${
query ? `&${query}` : ''
}`
return this.import(url)
},
)
options.hmr.connection.onUpdate(createHMRHandler(this))
}
if (options.sourcemapInterceptor !== false) {
this._resetSourceMapSupport = enableSourceMapSupport(this)
this.resetSourceMapSupport = enableSourceMapSupport(this)
}
}

Expand All @@ -109,7 +111,7 @@ export class ModuleRunner {
*/
public clearCache(): void {
this.moduleCache.clear()
this.idToUrlMap.clear()
this.urlToIdMap.clear()
this.hmrClient?.clear()
}

Expand All @@ -118,27 +120,17 @@ export class ModuleRunner {
* This method doesn't stop the HMR connection.
*/
public async destroy(): Promise<void> {
this._resetSourceMapSupport?.()
this.resetSourceMapSupport?.()
this.clearCache()
this.hmrClient = undefined
this._destroyed = true
this.destroyed = true
}

/**
* Returns `true` if the runtime has been destroyed by calling `destroy()` method.
*/
public isDestroyed(): boolean {
return this._destroyed
}

// map files to modules and invalidate them
private invalidateFiles(files: string[]) {
files.forEach((file) => {
const ids = this.fileToIdMap.get(file)
if (ids) {
ids.forEach((id) => this.moduleCache.invalidate(id))
}
})
return this.destroyed
}

// we don't use moduleCache.normalize because this URL doesn't have to follow the same rules
Expand Down Expand Up @@ -187,13 +179,12 @@ export class ModuleRunner {

private async cachedRequest(
id: string,
fetchedModule: ResolvedResult,
mod: ModuleCache,
callstack: string[] = [],
metadata?: SSRImportMetadata,
): Promise<any> {
const moduleId = fetchedModule.id

const mod = this.moduleCache.getByModuleId(moduleId)
const meta = mod.meta!
const moduleId = meta.id

const { imports, importers } = mod as Required<ModuleCache>

Expand All @@ -206,8 +197,7 @@ export class ModuleRunner {
callstack.includes(moduleId) ||
Array.from(imports.values()).some((i) => importers.has(i))
) {
if (mod.exports)
return this.processImport(mod.exports, fetchedModule, metadata)
if (mod.exports) return this.processImport(mod.exports, meta, metadata)
}

let debugTimer: any
Expand All @@ -228,76 +218,87 @@ export class ModuleRunner {
try {
// cached module
if (mod.promise)
return this.processImport(await mod.promise, fetchedModule, metadata)
return this.processImport(await mod.promise, meta, metadata)

const promise = this.directRequest(id, fetchedModule, callstack)
const promise = this.directRequest(id, mod, callstack)
mod.promise = promise
mod.evaluated = false
return this.processImport(await promise, fetchedModule, metadata)
return this.processImport(await promise, meta, metadata)
} finally {
mod.evaluated = true
if (debugTimer) clearTimeout(debugTimer)
}
}

private async cachedModule(
id: string,
url: string,
importer?: string,
): Promise<ResolvedResult> {
if (this._destroyed) {
): Promise<ModuleCache> {
if (this.destroyed) {
throw new Error(`[vite] Vite runtime has been destroyed.`)
}
const normalized = this.idToUrlMap.get(id)
const normalized = this.urlToIdMap.get(url)
if (normalized) {
const mod = this.moduleCache.getByModuleId(normalized)
if (mod.meta) {
return mod.meta as ResolvedResult
return mod
}
}
this.debug?.('[module runner] fetching', id)

this.debug?.('[module runner] fetching', url)
// fast return for established externalized patterns
const fetchedModule = id.startsWith('data:')
? ({ externalize: id, type: 'builtin' } satisfies FetchResult)
: await this.transport.fetchModule(id, importer)
const fetchedModule = (
url.startsWith('data:')
? { externalize: url, type: 'builtin' }
: await this.transport.fetchModule(url, importer)
) as ResolvedResult

// 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, it's possible to have a duplicated module
const idQuery = id.split('?')[1]
const query = idQuery ? `?${idQuery}` : ''
// if we used id for that, then a module will be duplicated
const { query, timestamp } = parseUrl(url)
const file = 'file' in fetchedModule ? fetchedModule.file : undefined
const fullFile = file ? `${file}${query}` : id
const moduleId = this.moduleCache.normalize(fullFile)
const fileId = file ? `${file}${query}` : url
const moduleId = this.moduleCache.normalize(fileId)
const mod = this.moduleCache.getByModuleId(moduleId)
;(fetchedModule as ResolvedResult).id = moduleId

// if URL has a ?t= query, it might've been invalidated due to HMR
// checking if we should also invalidate the module
if (mod.timestamp != null && timestamp > 0 && mod.timestamp < timestamp) {
this.moduleCache.invalidateModule(mod)
}

fetchedModule.id = moduleId
mod.meta = fetchedModule
mod.timestamp = timestamp

if (file) {
const fileModules = this.fileToIdMap.get(file) || []
fileModules.push(moduleId)
this.fileToIdMap.set(file, fileModules)
}

this.idToUrlMap.set(id, moduleId)
this.idToUrlMap.set(unwrapId(id), moduleId)
return fetchedModule as ResolvedResult
this.urlToIdMap.set(url, moduleId)
this.urlToIdMap.set(unwrapId(url), moduleId)
return mod
}

// override is allowed, consider this a public API
protected async directRequest(
id: string,
fetchResult: ResolvedResult,
mod: ModuleCache,
_callstack: string[],
): Promise<any> {
const fetchResult = mod.meta!
const moduleId = fetchResult.id
const callstack = [..._callstack, moduleId]

const mod = this.moduleCache.getByModuleId(moduleId)

const request = async (dep: string, metadata?: SSRImportMetadata) => {
const fetchedModule = await this.cachedModule(dep, moduleId)
const depMod = this.moduleCache.getByModuleId(fetchedModule.id)
const resolvedId = fetchedModule.meta!.id
const depMod = this.moduleCache.getByModuleId(resolvedId)
depMod.importers!.add(moduleId)
mod.imports!.add(fetchedModule.id)
mod.imports!.add(resolvedId)

return this.cachedRequest(dep, fetchedModule, callstack, metadata)
}
Expand Down
3 changes: 2 additions & 1 deletion packages/vite/src/module-runner/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export interface ModuleCache {
exports?: any
evaluated?: boolean
map?: DecodedMap
meta?: FetchResult
meta?: ResolvedResult
timestamp?: number
/**
* Module ids that imports this module
*/
Expand Down
27 changes: 27 additions & 0 deletions packages/vite/src/module-runner/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,33 @@ const carriageReturnRegEx = /\r/g
const tabRegEx = /\t/g
const questionRegex = /\?/g
const hashRegex = /#/g
const timestampRegex = /[?&]t=(\d{13})(&?)/

interface ParsedPath {
query: string
timestamp: number
}

export function parseUrl(url: string): ParsedPath {
const idQuery = url.split('?')[1]
let timestamp = 0
// for performance, we avoid using URL constructor and parsing twice
// it's not really needed, but it's a micro-optimization that we can do for free
const query = idQuery
? ('?' + idQuery).replace(
timestampRegex,
(substring, tsString, nextItem) => {
timestamp = Number(tsString)
// remove the "?t=" query since it's only used for invalidation
return substring[0] === '?' && nextItem === '&' ? '?' : ''
},
)
: ''
return {
query,
timestamp,
}
}

function encodePathChars(filepath: string) {
if (filepath.indexOf('%') !== -1)
Expand Down
27 changes: 0 additions & 27 deletions packages/vite/src/node/server/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,6 @@ export function updateModules(
? isExplicitImportRequired(acceptedVia.url)
: false,
isWithinCircularImport,
invalidates: getInvalidatedImporters(acceptedVia),
}),
),
)
Expand Down Expand Up @@ -435,32 +434,6 @@ export function updateModules(
})
}

function populateImporters(
module: EnvironmentModuleNode,
timestamp: number,
seen: Set<EnvironmentModuleNode> = new Set(),
) {
module.importedModules.forEach((importer) => {
if (seen.has(importer)) {
return
}
if (
importer.lastHMRTimestamp === timestamp ||
importer.lastInvalidationTimestamp === timestamp
) {
seen.add(importer)
populateImporters(importer, timestamp, seen)
}
})
return seen
}

function getInvalidatedImporters(module: EnvironmentModuleNode) {
return [...populateImporters(module, module.lastHMRTimestamp)].map(
(m) => m.file!,
)
}

function areAllImportsAccepted(
importedBindings: Set<string>,
acceptedExports: Set<string>,
Expand Down
Loading

0 comments on commit e97ac1e

Please sign in to comment.