Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: resolve id before loading from vite #1204

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/mocks/package.json
Expand Up @@ -19,7 +19,8 @@
"devDependencies": {
"@vitest/ui": "latest",
"vite": "^2.9.5",
"vitest": "latest"
"vitest": "latest",
"zustand": "4.0.0-rc.1"
},
"stackblitz": {
"startCommand": "npm run test:ui"
Expand Down
14 changes: 14 additions & 0 deletions examples/mocks/src/zustand.ts
@@ -0,0 +1,14 @@
import actualCreate from 'zustand'

// a variable to hold reset functions for all stores declared in the app
const storeResetFns = new Set()

// when creating a store, we get its initial state, create a reset function and add it in the set
const create = vi.fn((createState) => {
const store = actualCreate(createState)
const initialState = store.getState()
storeResetFns.add(() => store.setState(initialState, true))
return store
})

export default create
18 changes: 18 additions & 0 deletions examples/mocks/test/factory.test.ts
@@ -1,6 +1,7 @@
import axios from 'axios'
import * as example from '../src/example'
import * as moduleA from '../src/moduleA'
import logger from '../src/log'

vi
.mock('../src/example', () => ({
Expand Down Expand Up @@ -28,6 +29,17 @@ vi.mock('axios', () => {
}
})

vi.mock('../src/log.ts', async () => {
// can import the same module inside and does not go into an infinite loop
const log = await import('../src/log')
return {
default: {
...log.default,
info: vi.fn(),
},
}
})

describe('mocking with factory', () => {
test('successfuly mocked', () => {
expect((example as any).mocked).toBe(true)
Expand All @@ -44,4 +56,10 @@ describe('mocking with factory', () => {

expect(axios.get).toHaveBeenCalledTimes(1)
})

test('logger extended', () => {
expect(logger.warn).toBeTypeOf('function')
// @ts-expect-error extending module
expect(logger.info).toBeTypeOf('function')
})
})
9 changes: 9 additions & 0 deletions examples/mocks/test/self-importing.test.ts
@@ -0,0 +1,9 @@
import zustand from 'zustand'

vi.mock('zustand')

describe('zustand didn\'t go into an infinite loop', () => {
test('zustand is mocked', () => {
expect(vi.isMockFunction(zustand)).toBe(true)
})
})
41 changes: 32 additions & 9 deletions packages/vite-node/src/client.ts
Expand Up @@ -95,13 +95,6 @@ export class ViteNodeRunner {
return `stack:\n${[...callstack, dep].reverse().map(p => `- ${p}`).join('\n')}`
}

// probably means it was passed as variable
// and wasn't transformed by Vite
if (this.options.resolveId && this.shouldResolveId(dep)) {
const resolvedDep = await this.options.resolveId(dep, id)
dep = resolvedDep?.id?.replace(this.root, '') || dep
}

let debugTimer: any
if (this.debug)
debugTimer = setTimeout(() => this.debugLog(() => `module ${dep} takes over 2s to load.\n${getStack()}`), 2000)
Expand All @@ -125,6 +118,27 @@ export class ViteNodeRunner {
}
}

Object.defineProperty(request, 'callstack', { get: () => callstack })

const resolveId = async (dep: string, callstackPosition = 1) => {
// probably means it was passed as variable
// and wasn't transformed by Vite
// or some dependency name was passed
// runner.executeFile('@scope/name')
// runner.executeFile(myDynamicName)
if (this.options.resolveId && this.shouldResolveId(dep)) {
let importer = callstack[callstack.length - callstackPosition]
if (importer && importer.startsWith('mock:'))
importer = importer.slice(5)
const { id } = await this.options.resolveId(dep, importer) || {}
dep = id && isAbsolute(id) ? `/@fs/${id}` : id || dep
}

return dep
}

id = await resolveId(id, 2)

const requestStubs = this.options.requestStubs || DEFAULT_REQUEST_STUBS
if (id in requestStubs)
return requestStubs[id]
Expand Down Expand Up @@ -157,6 +171,8 @@ export class ViteNodeRunner {
},
}

let require: NodeRequire

// Be careful when changing this
// changing context will change amount of code added on line :114 (vm.runInThisContext)
// this messes up sourcemaps for coverage
Expand All @@ -169,8 +185,15 @@ export class ViteNodeRunner {
__vite_ssr_exportAll__: (obj: any) => exportAll(exports, obj),
__vite_ssr_import_meta__: { url },

__vitest_resolve_id__: resolveId,

// cjs compact
require: createRequire(url),
require: (path: string) => {
if (!require)
require = createRequire(url)

return require(path)
},
exports,
module: moduleProxy,
__filename,
Expand All @@ -193,7 +216,7 @@ export class ViteNodeRunner {
}

shouldResolveId(dep: string) {
if (isNodeBuiltin(dep) || dep in (this.options.requestStubs || DEFAULT_REQUEST_STUBS))
if (isNodeBuiltin(dep) || dep in (this.options.requestStubs || DEFAULT_REQUEST_STUBS) || dep.startsWith('/@vite'))
return false

return !isAbsolute(dep) || !extname(dep)
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/integrations/coverage.ts
Expand Up @@ -83,7 +83,7 @@ export async function reportCoverage(ctx: Vitest) {
// This is a magic number. It corresponds to the amount of code
// that we add in packages/vite-node/src/client.ts:114 (vm.runInThisContext)
// TODO: Include our transformations in sourcemaps
const offset = 203
const offset = 224

report._getSourceMap = (coverage: Profiler.ScriptCoverage) => {
const path = pathToFileURL(coverage.url).href
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/integrations/spy.ts
Expand Up @@ -167,7 +167,7 @@ function enhanceSpy<TArgs extends any[], TReturns>(
})
},
get lastCall() {
return stub.calls.at(-1)
return stub.calls[stub.calls.length - 1]
},
}

Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/core.ts
Expand Up @@ -78,12 +78,12 @@ export class Vitest {
fetchModule(id: string) {
return node.fetchModule(id)
},
resolveId(id: string, importer: string | undefined) {
resolveId(id: string, importer?: string) {
return node.resolveId(id, importer)
},
})

this.reporters = await createReporters(resolved.reporters, this.runner.executeFile.bind(this.runner))
this.reporters = await createReporters(resolved.reporters, this.runner)

this.runningPromise = undefined

Expand Down
9 changes: 5 additions & 4 deletions packages/vitest/src/node/reporters/utils.ts
@@ -1,11 +1,12 @@
import type { ViteNodeRunner } from 'vite-node/client'
import type { Reporter } from '../../types'
import { ReportersMap } from './index'
import type { BuiltinReporters } from './index'

async function loadCustomReporterModule<C extends Reporter>(path: string, fetchModule: (id: string) => Promise<any>): Promise<new () => C> {
async function loadCustomReporterModule<C extends Reporter>(path: string, runner: ViteNodeRunner): Promise<new () => C> {
let customReporterModule: { default: new () => C }
try {
customReporterModule = await fetchModule(path)
customReporterModule = await runner.executeId(path)
}
catch (customReporterModuleError) {
throw new Error(`Failed to load custom Reporter from ${path}`, { cause: customReporterModuleError as Error })
Expand All @@ -17,15 +18,15 @@ async function loadCustomReporterModule<C extends Reporter>(path: string, fetchM
return customReporterModule.default
}

function createReporters(reporterReferences: Array<string | Reporter | BuiltinReporters>, fetchModule: (id: string) => Promise<any>) {
function createReporters(reporterReferences: Array<string | Reporter | BuiltinReporters>, runner: ViteNodeRunner) {
const promisedReporters = reporterReferences.map(async (referenceOrInstance) => {
if (typeof referenceOrInstance === 'string') {
if (referenceOrInstance in ReportersMap) {
const BuiltinReporter = ReportersMap[referenceOrInstance as BuiltinReporters]
return new BuiltinReporter()
}
else {
const CustomReporter = await loadCustomReporterModule(referenceOrInstance, fetchModule)
const CustomReporter = await loadCustomReporterModule(referenceOrInstance, runner)
return new CustomReporter()
}
}
Expand Down
5 changes: 3 additions & 2 deletions packages/vitest/src/runtime/execute.ts
Expand Up @@ -33,6 +33,7 @@ export class VitestRunner extends ViteNodeRunner {

prepareContext(context: Record<string, any>) {
const request = context.__vite_ssr_import__
const resolveId = context.__vitest_resolve_id__

const mocker = this.mocker.withRequest(request)

Expand All @@ -49,8 +50,8 @@ export class VitestRunner extends ViteNodeRunner {
}

return Object.assign(context, {
__vite_ssr_import__: (dep: string) => mocker.requestWithMock(dep),
__vite_ssr_dynamic_import__: (dep: string) => mocker.requestWithMock(dep),
__vite_ssr_import__: async (dep: string) => mocker.requestWithMock(await resolveId(dep)),
__vite_ssr_dynamic_import__: async (dep: string) => mocker.requestWithMock(await resolveId(dep)),
__vitest_mocker__: mocker,
})
}
Expand Down
19 changes: 14 additions & 5 deletions packages/vitest/src/runtime/mocker.ts
Expand Up @@ -10,19 +10,24 @@ import type { ExecuteOptions } from './execute'

type Callback = (...args: any[]) => unknown

interface ViteRunnerRequest {
(dep: string): any
callstack: string[]
}

export class VitestMocker {
private static pendingIds: PendingSuiteMock[] = []
private static spyModule?: typeof import('../integrations/spy')

private request!: (dep: string) => unknown
private request!: ViteRunnerRequest

private root: string
private callbacks: Record<string, ((...args: any[]) => unknown)[]> = {}

constructor(
public options: ExecuteOptions,
private moduleCache: ModuleCacheMap,
request?: (dep: string) => unknown,
request?: ViteRunnerRequest,
) {
this.root = this.options.root
this.request = request!
Expand Down Expand Up @@ -230,6 +235,8 @@ export class VitestMocker {

const mock = this.getDependencyMock(dep)

const callstack = this.request.callstack

if (mock === null) {
const cacheName = `${dep}__mock`
const cache = this.moduleCache.get(cacheName)
Expand All @@ -241,9 +248,11 @@ export class VitestMocker {
this.emit('mocked', cacheName, { exports })
return exports
}
if (typeof mock === 'function')
if (typeof mock === 'function' && !callstack.includes(`mock:${dep}`)) {
callstack.push(`mock:${dep}`)
return this.callFunctionMock(dep, mock)
if (typeof mock === 'string')
}
if (typeof mock === 'string' && !callstack.includes(mock))
dep = mock
return this.request(dep)
}
Expand All @@ -256,7 +265,7 @@ export class VitestMocker {
VitestMocker.pendingIds.push({ type: 'unmock', id, importer })
}

public withRequest(request: (dep: string) => unknown) {
public withRequest(request: ViteRunnerRequest) {
return new VitestMocker(this.options, this.moduleCache, request)
}
}
19 changes: 8 additions & 11 deletions packages/vitest/src/runtime/worker.ts
Expand Up @@ -10,7 +10,6 @@ import { rpc } from './rpc'

let _viteNode: {
run: (files: string[], config: ResolvedConfig) => Promise<void>
collect: (files: string[], config: ResolvedConfig) => Promise<void>
}

const moduleCache = new ModuleCacheMap()
Expand All @@ -37,7 +36,7 @@ async function startViteNode(ctx: WorkerContext) {

const { config } = ctx

const { run, collect } = (await executeInViteNode({
const { run } = (await executeInViteNode({
files: [
resolve(distDir, 'entry.js'),
],
Expand All @@ -54,7 +53,7 @@ async function startViteNode(ctx: WorkerContext) {
base: config.base,
}))[0]

_viteNode = { run, collect }
_viteNode = { run }

return _viteNode
}
Expand Down Expand Up @@ -86,17 +85,15 @@ function init(ctx: WorkerContext) {
),
}

if (ctx.invalidates)
ctx.invalidates.forEach(i => moduleCache.delete(i))
if (ctx.invalidates) {
ctx.invalidates.forEach((i) => {
moduleCache.delete(i)
moduleCache.delete(`${i}__mock`)
})
}
ctx.files.forEach(i => moduleCache.delete(i))
}

export async function collect(ctx: WorkerContext) {
init(ctx)
const { collect } = await startViteNode(ctx)
return collect(ctx.files, ctx.config)
}

export async function run(ctx: WorkerContext) {
init(ctx)
const { run } = await startViteNode(ctx)
Expand Down
23 changes: 23 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.