Skip to content

Commit

Permalink
feat!: rewrite how vite-node resolves id
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Dec 8, 2022
1 parent 3d86865 commit 72e429a
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 158 deletions.
2 changes: 1 addition & 1 deletion examples/mocks/test/axios-not-mocked.test.ts
@@ -1,7 +1,7 @@
import axios from 'axios'

test('mocked axios', async () => {
const { default: ax } = await vi.importMock('axios')
const { default: ax } = await vi.importMock<any>('axios')

await ax.get('string')

Expand Down
2 changes: 1 addition & 1 deletion packages/coverage-c8/src/provider.ts
Expand Up @@ -85,7 +85,7 @@ export class C8CoverageProvider implements CoverageProvider {
// 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 = 224
const offset = 203

report._getSourceMap = (coverage: Profiler.ScriptCoverage) => {
const path = _url.pathToFileURL(coverage.url.split('?')[0]).href
Expand Down
122 changes: 51 additions & 71 deletions packages/vite-node/src/client.ts
Expand Up @@ -145,39 +145,40 @@ export class ViteNodeRunner {
}

async executeFile(file: string) {
return await this.cachedRequest(`/@fs/${slash(resolve(file))}`, [])
const id = slash(resolve(file))
const url = `/@fs/${slash(resolve(file))}`
return await this.cachedRequest(id, url, [])
}

async executeId(id: string) {
return await this.cachedRequest(id, [])
const url = await this.resolveUrl(id)
return await this.cachedRequest(id, url, [])
}

getSourceMap(id: string) {
return this.moduleCache.getSourceMap(id)
}

/** @internal */
async cachedRequest(rawId: string, callstack: string[]) {
const id = normalizeRequestId(rawId, this.options.base)
const fsPath = toFilePath(id, this.root)

const mod = this.moduleCache.get(fsPath)
async cachedRequest(rawId: string, url: string, callstack: string[]) {
const importee = callstack[callstack.length - 1]

const mod = this.moduleCache.get(url)

if (!mod.importers)
mod.importers = new Set()
if (importee)
mod.importers.add(importee)

// the callstack reference itself circularly
if (callstack.includes(fsPath) && mod.exports)
if (callstack.includes(url) && mod.exports)
return mod.exports

// cached module
if (mod.promise)
return mod.promise

const promise = this.directRequest(id, fsPath, callstack)
const promise = this.directRequest(rawId, url, callstack)
Object.assign(mod, { promise, evaluated: false })

promise.finally(() => {
Expand All @@ -187,79 +188,60 @@ export class ViteNodeRunner {
return await promise
}

async resolveUrl(url: string, importee?: string) {
url = normalizeRequestId(url, this.options.base)
if (!this.options.resolveId)
return toFilePath(url, this.root)
if (importee && url[0] !== '.')
importee = undefined
const resolved = await this.options.resolveId(url, importee)
const resolvedId = resolved?.id || url
return normalizeRequestId(resolvedId, this.options.base)
}

/** @internal */
async directRequest(id: string, fsPath: string, _callstack: string[]) {
const callstack = [..._callstack, fsPath]
async dependencyRequest(id: string, url: string, callstack: string[]) {
const getStack = () => {
return `stack:\n${[...callstack, url].reverse().map(p => `- ${p}`).join('\n')}`
}

let mod = this.moduleCache.get(fsPath)
let debugTimer: any
if (this.debug)
debugTimer = setTimeout(() => console.warn(() => `module ${url} takes over 2s to load.\n${getStack()}`), 2000)

const request = async (dep: string) => {
const depFsPath = toFilePath(normalizeRequestId(dep, this.options.base), this.root)
const getStack = () => {
return `stack:\n${[...callstack, depFsPath].reverse().map(p => `- ${p}`).join('\n')}`
try {
if (callstack.includes(url)) {
const depExports = this.moduleCache.get(url)?.exports
if (depExports)
return depExports
throw new Error(`[vite-node] Failed to resolve circular dependency, ${getStack()}`)
}

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

try {
if (callstack.includes(depFsPath)) {
const depExports = this.moduleCache.get(depFsPath)?.exports
if (depExports)
return depExports
throw new Error(`[vite-node] Failed to resolve circular dependency, ${getStack()}`)
}

return await this.cachedRequest(dep, callstack)
}
finally {
if (debugTimer)
clearTimeout(debugTimer)
}
return await this.cachedRequest(id, url, callstack)
}
finally {
if (debugTimer)
clearTimeout(debugTimer)
}
}

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

const resolveId = async (dep: string, callstackPosition = 1): Promise<[dep: string, id: string | undefined]> => {
if (this.options.resolveId && this.shouldResolveId(dep)) {
let importer: string | undefined = callstack[callstack.length - callstackPosition]
if (importer && !dep.startsWith('.'))
importer = undefined
if (importer && importer.startsWith('mock:'))
importer = importer.slice(5)
const resolved = await this.options.resolveId(normalizeRequestId(dep), importer)
return [dep, resolved?.id]
}
/** @internal */
async directRequest(id: string, fsPath: string, _callstack: string[]) {
const callstack = [..._callstack, fsPath]

return [dep, undefined]
}
const mod = this.moduleCache.get(fsPath)

const [dep, resolvedId] = await resolveId(id, 2)
const request = async (dep: string) => {
const depFsPath = await this.resolveUrl(dep, fsPath)
return this.dependencyRequest(dep, depFsPath, callstack)
}

const requestStubs = this.options.requestStubs || DEFAULT_REQUEST_STUBS
if (id in requestStubs)
return requestStubs[id]

// eslint-disable-next-line prefer-const
let { code: transformed, externalize } = await this.options.fetchModule(resolvedId || dep)

// in case we resolved fsPath incorrectly, Vite will return the correct file path
// in that case we need to update cache, so we don't have the same module as different exports
// but we ignore fsPath that has custom query, because it might need to be different
if (resolvedId && !fsPath.includes('?') && fsPath !== resolvedId) {
if (this.moduleCache.has(resolvedId)) {
mod = this.moduleCache.get(resolvedId)
this.moduleCache.set(fsPath, mod)
if (mod.promise)
return mod.promise
if (mod.exports)
return mod.exports
}
else {
this.moduleCache.set(resolvedId, mod)
}
}
let { code: transformed, externalize } = await this.options.fetchModule(fsPath)

if (externalize) {
debugNative(externalize)
Expand All @@ -269,10 +251,9 @@ export class ViteNodeRunner {
}

if (transformed == null)
throw new Error(`[vite-node] Failed to load ${id}`)
throw new Error(`[vite-node] Failed to load "${id}" imported from ${callstack[callstack.length - 2]}`)

const file = cleanUrl(resolvedId || fsPath)
// console.log('file', file)
const file = cleanUrl(fsPath)
// disambiguate the `<UNIT>:/` on windows: see nodejs/node#31710
const url = pathToFileURL(file).href
const meta = { url }
Expand Down Expand Up @@ -338,7 +319,6 @@ export class ViteNodeRunner {
__vite_ssr_exports__: exports,
__vite_ssr_exportAll__: (obj: any) => exportAll(exports, obj),
__vite_ssr_import_meta__: meta,
__vitest_resolve_id__: resolveId,

// cjs compact
require: createRequire(url),
Expand Down
24 changes: 5 additions & 19 deletions packages/vitest/src/integrations/vi.ts
Expand Up @@ -2,7 +2,7 @@ import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers'
import { parseStacktrace } from '../utils/source-map'
import type { VitestMocker } from '../runtime/mocker'
import type { ResolvedConfig, RuntimeConfig } from '../types'
import { getWorkerState, resetModules, setTimeout } from '../utils'
import { getWorkerState, resetModules, waitForImportsToResolve } from '../utils'
import { FakeTimers } from './mock/timers'
import type { EnhancedSpy, MaybeMocked, MaybeMockedDeep, MaybePartiallyMocked, MaybePartiallyMockedDeep } from './spy'
import { fn, isMockFunction, spies, spyOn } from './spy'
Expand Down Expand Up @@ -245,26 +245,12 @@ class VitestUtils {
}

/**
* Wait for all imports to load.
* Useful, if you have a synchronous call that starts
* importing a module that you cannot wait otherwise.
* Wait for all imports to load. Useful, if you have a synchronous call that starts
* importing a module that you cannot await otherwise.
* Will also wait for new imports, started during the wait.
*/
public async dynamicImportSettled() {
const state = getWorkerState()
const promises: Promise<unknown>[] = []
for (const mod of state.moduleCache.values()) {
if (mod.promise && !mod.evaluated)
promises.push(mod.promise)
}
if (!promises.length)
return
await Promise.allSettled(promises)
// wait until the end of the loop, so `.then` on modules is called,
// like in import('./example').then(...)
// also call dynamicImportSettled again in case new imports were added
await new Promise(resolve => setTimeout(resolve, 1))
.then(() => Promise.resolve())
.then(() => this.dynamicImportSettled())
return waitForImportsToResolve()
}

private _config: null | ResolvedConfig = null
Expand Down
31 changes: 20 additions & 11 deletions packages/vitest/src/runtime/execute.ts
Expand Up @@ -23,20 +23,31 @@ export async function executeInViteNode(options: ExecuteOptions & { files: strin
}

export class VitestRunner extends ViteNodeRunner {
public mocker: VitestMocker

constructor(public options: ExecuteOptions) {
super(options)

this.mocker = new VitestMocker(this)
}

prepareContext(context: Record<string, any>) {
const request = context.__vite_ssr_import__
const resolveId = context.__vitest_resolve_id__
const resolveUrl = async (dep: string) => {
const [id, resolvedId] = await resolveId(dep)
return resolvedId || id
}
async resolveUrl(url: string, importee?: string): Promise<string> {
if (importee && importee.startsWith('mock:'))
importee = importee.slice(5)
return super.resolveUrl(url, importee)
}

async dependencyRequest(id: string, url: string, callstack: string[]): Promise<any> {
const mocked = await this.mocker.requestWithMock(url, callstack)

const mocker = new VitestMocker(this.options, this.moduleCache, request)
if (typeof mocked === 'string')
return super.dependencyRequest(id, mocked, callstack)
if (mocked && typeof mocked === 'object')
return mocked
return super.cachedRequest(id, url, callstack)
}

prepareContext(context: Record<string, any>) {
const workerState = getWorkerState()

// support `import.meta.vitest` for test entry
Expand All @@ -46,9 +57,7 @@ export class VitestRunner extends ViteNodeRunner {
}

return Object.assign(context, {
__vite_ssr_import__: async (dep: string) => mocker.requestWithMock(await resolveUrl(dep)),
__vite_ssr_dynamic_import__: async (dep: string) => mocker.requestWithMock(await resolveUrl(dep)),
__vitest_mocker__: mocker,
__vitest_mocker__: this.mocker,
})
}
}

0 comments on commit 72e429a

Please sign in to comment.