Skip to content

Commit

Permalink
fix(vite-node): self circular reference (#1609)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Jul 7, 2022
1 parent 57b877f commit 49c67d2
Show file tree
Hide file tree
Showing 15 changed files with 143 additions and 55 deletions.
2 changes: 1 addition & 1 deletion examples/mocks/src/example.ts
Expand Up @@ -5,7 +5,7 @@ export async function asyncSquare(a: number, b: number) {
const result = (await a) * b
return result
}
export const someClasss = new (class Bar {
export const someClasses = new (class Bar {
public array: number[]
constructor() {
this.array = [1, 2, 3]
Expand Down
8 changes: 4 additions & 4 deletions examples/mocks/test/automocking.spec.ts
Expand Up @@ -17,10 +17,10 @@ test('all mocked are valid', async () => {
expect(example.asyncSquare.length).toEqual(0)

// creates a new class with the same interface, member functions and properties are mocked.
expect(example.someClasss.constructor.name).toEqual('Bar')
expect(example.someClasss.foo.name).toEqual('foo')
expect(vi.isMockFunction(example.someClasss.foo)).toBe(true)
expect(example.someClasss.array.length).toEqual(0)
expect(example.someClasses.constructor.name).toEqual('Bar')
expect(example.someClasses.foo.name).toEqual('foo')
expect(vi.isMockFunction(example.someClasses.foo)).toBe(true)
expect(example.someClasses.array.length).toEqual(0)

// creates a deeply cloned version of the original object.
expect(example.object).toEqual({
Expand Down
18 changes: 12 additions & 6 deletions packages/vite-node/src/client.ts
Expand Up @@ -82,6 +82,11 @@ export class ViteNodeRunner {
const id = normalizeRequestId(rawId, this.options.base)
const fsPath = toFilePath(id, this.root)

// the callstack reference itself circularly
if (callstack.includes(fsPath) && this.moduleCache.get(fsPath)?.exports)
return this.moduleCache.get(fsPath)?.exports

// cached module
if (this.moduleCache.get(fsPath)?.promise)
return this.moduleCache.get(fsPath)?.promise

Expand All @@ -93,20 +98,21 @@ export class ViteNodeRunner {

/** @internal */
async directRequest(id: string, fsPath: string, _callstack: string[]) {
const callstack = [..._callstack, normalizeModuleId(id)]
const callstack = [..._callstack, fsPath]
const request = async (dep: string) => {
const fsPath = toFilePath(normalizeRequestId(dep, this.options.base), this.root)
const getStack = () => {
return `stack:\n${[...callstack, dep].reverse().map(p => `- ${p}`).join('\n')}`
return `stack:\n${[...callstack, fsPath].reverse().map(p => `- ${p}`).join('\n')}`
}

let debugTimer: any
if (this.debug)
debugTimer = setTimeout(() => this.debugLog(() => `module ${dep} takes over 2s to load.\n${getStack()}`), 2000)
debugTimer = setTimeout(() => this.debugLog(() => `module ${fsPath} takes over 2s to load.\n${getStack()}`), 2000)

try {
if (callstack.includes(normalizeModuleId(dep))) {
if (callstack.includes(fsPath)) {
this.debugLog(() => `circular dependency, ${getStack()}`)
const depExports = this.moduleCache.get(dep)?.exports
const depExports = this.moduleCache.get(fsPath)?.exports
if (depExports)
return depExports
throw new Error(`[vite-node] Failed to resolve circular dependency, ${getStack()}`)
Expand Down Expand Up @@ -165,7 +171,7 @@ export class ViteNodeRunner {
const exports: any = Object.create(null)
exports[Symbol.toStringTag] = 'Module'

this.moduleCache.set(id, { code: transformed, exports })
this.moduleCache.set(fsPath, { code: transformed, exports })

const __filename = fileURLToPath(url)
const moduleProxy = {
Expand Down
6 changes: 3 additions & 3 deletions packages/vite-node/src/hmr/hmr.ts
Expand Up @@ -74,9 +74,9 @@ export function sendMessageBuffer(runner: ViteNodeRunner, emitter: HMREmitter) {
export async function reload(runner: ViteNodeRunner, files: string[]) {
// invalidate module cache but not node_modules
Array.from(runner.moduleCache.keys())
.forEach((i) => {
if (!i.includes('node_modules'))
runner.moduleCache.delete(i)
.forEach((fsPath) => {
if (!fsPath.includes('node_modules'))
runner.moduleCache.delete(fsPath)
})

return Promise.all(files.map(file => runner.executeId(file)))
Expand Down
6 changes: 3 additions & 3 deletions packages/vite-node/src/utils.ts
Expand Up @@ -46,20 +46,20 @@ export function isPrimitive(v: any) {
}

export function toFilePath(id: string, root: string): string {
let absolute = slash(id).startsWith('/@fs/')
let absolute = id.startsWith('/@fs/')
? id.slice(4)
: id.startsWith(root)
? id
: id.startsWith('/')
? slash(resolve(root, id.slice(1)))
? resolve(root, id.slice(1))
: id

if (absolute.startsWith('//'))
absolute = absolute.slice(1)

// disambiguate the `<UNIT>:/` on windows: see nodejs/node#31710
return isWindows && absolute.startsWith('/')
? fileURLToPath(pathToFileURL(absolute.slice(1)).href)
? slash(fileURLToPath(pathToFileURL(absolute.slice(1)).href))
: absolute
}

Expand Down
17 changes: 9 additions & 8 deletions packages/vitest/src/runtime/mocker.ts
@@ -1,6 +1,6 @@
import { existsSync, readdirSync } from 'fs'
import { isNodeBuiltin } from 'mlly'
import { basename, dirname, resolve } from 'pathe'
import { basename, dirname, join, resolve } from 'pathe'
import { normalizeRequestId, toFilePath } from 'vite-node/utils'
import type { ModuleCacheMap } from 'vite-node/client'
import { getAllProperties, getType, getWorkerState, isWindows, mergeSlashes, slash } from '../utils'
Expand Down Expand Up @@ -106,7 +106,7 @@ export class VitestMocker {
// all mocks should be inside <root>/__mocks__
if (external || isNodeBuiltin(mockPath) || !existsSync(mockPath)) {
const mockDirname = dirname(path) // for nested mocks: @vueuse/integration/useJwt
const mockFolder = resolve(this.root, '__mocks__', mockDirname)
const mockFolder = join(this.root, '__mocks__', mockDirname)

if (!existsSync(mockFolder))
return null
Expand All @@ -117,7 +117,7 @@ export class VitestMocker {
for (const file of files) {
const [basename] = file.split('.')
if (basename === baseFilename)
return resolve(mockFolder, file).replace(this.root, '')
return resolve(mockFolder, file)
}

return null
Expand All @@ -126,7 +126,7 @@ export class VitestMocker {
const dir = dirname(path)
const baseId = basename(path)
const fullPath = resolve(dir, '__mocks__', baseId)
return existsSync(fullPath) ? fullPath.replace(this.root, '') : null
return existsSync(fullPath) ? fullPath : null
}

public mockValue(value: any) {
Expand Down Expand Up @@ -196,19 +196,20 @@ export class VitestMocker {
public async importMock(id: string, importer: string): Promise<any> {
const { path, external } = await this.resolvePath(id, importer)

let mock = this.getDependencyMock(path)
const fsPath = this.getFsPath(path, external)
let mock = this.getDependencyMock(fsPath)

if (mock === undefined)
mock = this.resolveMockPath(path, external)
mock = this.resolveMockPath(fsPath, external)

if (mock === null) {
await this.ensureSpy()
const fsPath = this.getFsPath(path, external)
const mod = await this.request(fsPath)
return this.mockValue(mod)
}

if (typeof mock === 'function')
return this.callFunctionMock(path, mock)
return this.callFunctionMock(fsPath, mock)
return this.requestWithMock(mock)
}

Expand Down
6 changes: 3 additions & 3 deletions packages/vitest/src/runtime/setup.ts
Expand Up @@ -165,9 +165,9 @@ export async function withEnv(
export async function runSetupFiles(config: ResolvedConfig) {
const files = toArray(config.setupFiles)
await Promise.all(
files.map(async (file) => {
getWorkerState().moduleCache.delete(file)
await import(file)
files.map(async (fsPath) => {
getWorkerState().moduleCache.delete(fsPath)
await import(fsPath)
}),
)
}
6 changes: 3 additions & 3 deletions packages/vitest/src/runtime/worker.ts
Expand Up @@ -86,9 +86,9 @@ function init(ctx: WorkerContext) {
}

if (ctx.invalidates) {
ctx.invalidates.forEach((i) => {
moduleCache.delete(i)
moduleCache.delete(`${i}__mock`)
ctx.invalidates.forEach((fsPath) => {
moduleCache.delete(fsPath)
moduleCache.delete(`${fsPath}__mock`)
})
}
ctx.files.forEach(i => moduleCache.delete(i))
Expand Down
6 changes: 3 additions & 3 deletions packages/web-worker/src/pure.ts
Expand Up @@ -133,10 +133,10 @@ export function defineWebWorker() {

runner.executeFile(fsPath)
.then(() => {
invalidates.forEach((path) => {
invalidates.forEach((fsPath) => {
// worker should be new every time
moduleCache.delete(path)
moduleCache.delete(`${path}__mock`)
moduleCache.delete(fsPath)
moduleCache.delete(`${fsPath}__mock`)
})
const q = this.messageQueue
this.messageQueue = null
Expand Down

0 comments on commit 49c67d2

Please sign in to comment.