Skip to content

Commit 58ee8e9

Browse files
authored
feat!: rewrite how vite-node resolves id (#2463)
* feat!: rewrite how vite-node resolves id * chore: try to fix global setup test * fix: remove /@fs/ from filepath * chore: cleanup * fix: normalize id for callstack * fix: don't append "/" before disk drive on windows * chore: cleanup * chore: add test that windows drive is not repeated * fix: dirname uses \\, update windows paths tests * refactor: rename variables * fix: don't provide importer only for /@id/ * chore: remove null byte placeholder as part of unwrapRef * chore: cleanup * chore: variables renaming * fix: don't hang on circular mock * test: update c8 snapshot * chore: add compatibility layer for users who don't provide resolveId * test: fix url handling in web worker * test: fix file tests on windows * chore: remove unnecessary normalizations in mocker * chore: use /@fs/ when fetching module, if possible
1 parent 0967b24 commit 58ee8e9

File tree

25 files changed

+313
-277
lines changed

25 files changed

+313
-277
lines changed

examples/mocks/src/A.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { funcB } from './B'
2+
3+
export function funcA() {
4+
return funcB
5+
}

examples/mocks/src/B.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { funcA } from './A'
2+
3+
export function funcB() {
4+
return funcA
5+
}

examples/mocks/src/main.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { funcA } from './A'
2+
3+
export function main() {
4+
return funcA()
5+
}

examples/mocks/test/axios-not-mocked.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import axios from 'axios'
22

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

66
await ax.get('string')
77

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { expect, test, vi } from 'vitest'
2+
import { main } from '../src/main.js'
3+
4+
vi.mock('../src/A', async () => ({
5+
...(await vi.importActual<any>('../src/A')),
6+
funcA: () => 'mockedA',
7+
}))
8+
9+
test('main', () => {
10+
expect(main()).toBe('mockedA')
11+
})

examples/mocks/test/error-mock.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ vi.mock('../src/default', () => {
44

55
test('when using top level variable, gives helpful message', async () => {
66
await expect(() => import('../src/default').then(m => m.default)).rejects
7-
.toThrowErrorMatchingInlineSnapshot('"[vitest] There was an error, when mocking a module. If you are using vi.mock, make sure you are not using top level variables inside, since this call is hoisted. Read more: https://vitest.dev/api/#vi-mock"')
7+
.toThrowErrorMatchingInlineSnapshot('"[vitest] There was an error, when mocking a module. If you are using \\"vi.mock\\" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. Read more: https://vitest.dev/api/#vi-mock"')
88
})

examples/mocks/test/factory.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('mocking with factory', () => {
5757

5858
it('non-object return on factory gives error', async () => {
5959
await expect(() => import('../src/default').then(m => m.default)).rejects
60-
.toThrowError('[vitest] vi.mock(path: string, factory?: () => unknown) is not returning an object. Did you mean to return an object with a "default" key?')
60+
.toThrowError('[vitest] vi.mock("../src/default.ts", factory?: () => unknown) is not returning an object. Did you mean to return an object with a "default" key?')
6161
})
6262

6363
test('defined exports on mock', async () => {

packages/coverage-c8/src/provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export class C8CoverageProvider implements CoverageProvider {
8585
// This is a magic number. It corresponds to the amount of code
8686
// that we add in packages/vite-node/src/client.ts:114 (vm.runInThisContext)
8787
// TODO: Include our transformations in sourcemaps
88-
const offset = 224
88+
const offset = 203
8989

9090
report._getSourceMap = (coverage: Profiler.ScriptCoverage) => {
9191
const path = _url.pathToFileURL(coverage.url.split('?')[0]).href

packages/vite-node/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
},
7171
"scripts": {
7272
"build": "rimraf dist && rollup -c",
73-
"dev": "rollup -c --watch --watch.include=src -m inline",
73+
"dev": "rollup -c --watch --watch.include 'src/**' -m inline",
7474
"prepublishOnly": "pnpm build",
7575
"typecheck": "tsc --noEmit"
7676
},

packages/vite-node/src/client.ts

Lines changed: 65 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { createRequire } from 'module'
2+
// we need native dirname, because windows __dirname has \\
3+
// eslint-disable-next-line no-restricted-imports
4+
import { dirname } from 'path'
25
import { fileURLToPath, pathToFileURL } from 'url'
36
import vm from 'vm'
4-
import { dirname, extname, isAbsolute, resolve } from 'pathe'
5-
import { isNodeBuiltin } from 'mlly'
7+
import { resolve } from 'pathe'
68
import createDebug from 'debug'
7-
import { cleanUrl, isPrimitive, normalizeModuleId, normalizeRequestId, slash, toFilePath } from './utils'
9+
import { VALID_ID_PREFIX, cleanUrl, isInternalRequest, isPrimitive, normalizeModuleId, normalizeRequestId, slash, toFilePath } from './utils'
810
import type { HotContext, ModuleCache, ViteNodeRunnerOptions } from './types'
911
import { extractSourceMap } from './source-map'
1012

@@ -145,24 +147,24 @@ export class ViteNodeRunner {
145147
}
146148

147149
async executeFile(file: string) {
148-
return await this.cachedRequest(`/@fs/${slash(resolve(file))}`, [])
150+
const url = `/@fs/${slash(resolve(file))}`
151+
return await this.cachedRequest(url, url, [])
149152
}
150153

151-
async executeId(id: string) {
152-
return await this.cachedRequest(id, [])
154+
async executeId(rawId: string) {
155+
const [id, url] = await this.resolveUrl(rawId)
156+
return await this.cachedRequest(id, url, [])
153157
}
154158

155159
getSourceMap(id: string) {
156160
return this.moduleCache.getSourceMap(id)
157161
}
158162

159163
/** @internal */
160-
async cachedRequest(rawId: string, callstack: string[]) {
161-
const id = normalizeRequestId(rawId, this.options.base)
162-
const fsPath = toFilePath(id, this.root)
164+
async cachedRequest(id: string, fsPath: string, callstack: string[]) {
165+
const importee = callstack[callstack.length - 1]
163166

164167
const mod = this.moduleCache.get(fsPath)
165-
const importee = callstack[callstack.length - 1]
166168

167169
if (!mod.importers)
168170
mod.importers = new Set()
@@ -188,79 +190,68 @@ export class ViteNodeRunner {
188190
}
189191
}
190192

193+
async resolveUrl(id: string, importee?: string): Promise<[url: string, fsPath: string]> {
194+
if (isInternalRequest(id))
195+
return [id, id]
196+
// we don't pass down importee here, because otherwise Vite doesn't resolve it correctly
197+
if (importee && id.startsWith(VALID_ID_PREFIX))
198+
importee = undefined
199+
id = normalizeRequestId(id, this.options.base)
200+
if (!this.options.resolveId)
201+
return [id, toFilePath(id, this.root)]
202+
const resolved = await this.options.resolveId(id, importee)
203+
const resolvedId = resolved
204+
? normalizeRequestId(resolved.id, this.options.base)
205+
: id
206+
// to be compatible with dependencies that do not resolve id
207+
const fsPath = resolved ? resolvedId : toFilePath(id, this.root)
208+
return [resolvedId, fsPath]
209+
}
210+
191211
/** @internal */
192-
async directRequest(id: string, fsPath: string, _callstack: string[]) {
193-
const callstack = [..._callstack, fsPath]
212+
async dependencyRequest(id: string, fsPath: string, callstack: string[]) {
213+
const getStack = () => {
214+
return `stack:\n${[...callstack, fsPath].reverse().map(p => `- ${p}`).join('\n')}`
215+
}
194216

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

197-
const request = async (dep: string) => {
198-
const depFsPath = toFilePath(normalizeRequestId(dep, this.options.base), this.root)
199-
const getStack = () => {
200-
return `stack:\n${[...callstack, depFsPath].reverse().map(p => `- ${p}`).join('\n')}`
221+
try {
222+
if (callstack.includes(fsPath)) {
223+
const depExports = this.moduleCache.get(fsPath)?.exports
224+
if (depExports)
225+
return depExports
226+
throw new Error(`[vite-node] Failed to resolve circular dependency, ${getStack()}`)
201227
}
202228

203-
let debugTimer: any
204-
if (this.debug)
205-
debugTimer = setTimeout(() => console.warn(() => `module ${depFsPath} takes over 2s to load.\n${getStack()}`), 2000)
206-
207-
try {
208-
if (callstack.includes(depFsPath)) {
209-
const depExports = this.moduleCache.get(depFsPath)?.exports
210-
if (depExports)
211-
return depExports
212-
throw new Error(`[vite-node] Failed to resolve circular dependency, ${getStack()}`)
213-
}
214-
215-
return await this.cachedRequest(dep, callstack)
216-
}
217-
finally {
218-
if (debugTimer)
219-
clearTimeout(debugTimer)
220-
}
229+
return await this.cachedRequest(id, fsPath, callstack)
230+
}
231+
finally {
232+
if (debugTimer)
233+
clearTimeout(debugTimer)
221234
}
235+
}
222236

223-
Object.defineProperty(request, 'callstack', { get: () => callstack })
224-
225-
const resolveId = async (dep: string, callstackPosition = 1): Promise<[dep: string, id: string | undefined]> => {
226-
if (this.options.resolveId && this.shouldResolveId(dep)) {
227-
let importer: string | undefined = callstack[callstack.length - callstackPosition]
228-
if (importer && !dep.startsWith('.'))
229-
importer = undefined
230-
if (importer && importer.startsWith('mock:'))
231-
importer = importer.slice(5)
232-
const resolved = await this.options.resolveId(normalizeRequestId(dep), importer)
233-
return [dep, resolved?.id]
234-
}
237+
/** @internal */
238+
async directRequest(id: string, fsPath: string, _callstack: string[]) {
239+
const moduleId = normalizeModuleId(fsPath)
240+
const callstack = [..._callstack, moduleId]
235241

236-
return [dep, undefined]
237-
}
242+
const mod = this.moduleCache.get(fsPath)
238243

239-
const [dep, resolvedId] = await resolveId(id, 2)
244+
const request = async (dep: string) => {
245+
const [id, depFsPath] = await this.resolveUrl(dep, fsPath)
246+
return this.dependencyRequest(id, depFsPath, callstack)
247+
}
240248

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

245253
// eslint-disable-next-line prefer-const
246-
let { code: transformed, externalize } = await this.options.fetchModule(resolvedId || dep)
247-
248-
// in case we resolved fsPath incorrectly, Vite will return the correct file path
249-
// in that case we need to update cache, so we don't have the same module as different exports
250-
// but we ignore fsPath that has custom query, because it might need to be different
251-
if (resolvedId && !fsPath.includes('?') && fsPath !== resolvedId) {
252-
if (this.moduleCache.has(resolvedId)) {
253-
mod = this.moduleCache.get(resolvedId)
254-
this.moduleCache.set(fsPath, mod)
255-
if (mod.promise)
256-
return mod.promise
257-
if (mod.exports)
258-
return mod.exports
259-
}
260-
else {
261-
this.moduleCache.set(resolvedId, mod)
262-
}
263-
}
254+
let { code: transformed, externalize } = await this.options.fetchModule(id)
264255

265256
if (externalize) {
266257
debugNative(externalize)
@@ -270,13 +261,12 @@ export class ViteNodeRunner {
270261
}
271262

272263
if (transformed == null)
273-
throw new Error(`[vite-node] Failed to load ${id}`)
274-
275-
const file = cleanUrl(resolvedId || fsPath)
264+
throw new Error(`[vite-node] Failed to load "${id}" imported from ${callstack[callstack.length - 2]}`)
276265

266+
const modulePath = cleanUrl(moduleId)
277267
// disambiguate the `<UNIT>:/` on windows: see nodejs/node#31710
278-
const url = pathToFileURL(file).href
279-
const meta = { url }
268+
const href = pathToFileURL(modulePath).href
269+
const meta = { url: href }
280270
const exports = Object.create(null)
281271
Object.defineProperty(exports, Symbol.toStringTag, {
282272
value: 'Module',
@@ -305,7 +295,7 @@ export class ViteNodeRunner {
305295
})
306296

307297
Object.assign(mod, { code: transformed, exports })
308-
const __filename = fileURLToPath(url)
298+
const __filename = fileURLToPath(href)
309299
const moduleProxy = {
310300
set exports(value) {
311301
exportAll(cjsExports, value)
@@ -339,10 +329,9 @@ export class ViteNodeRunner {
339329
__vite_ssr_exports__: exports,
340330
__vite_ssr_exportAll__: (obj: any) => exportAll(exports, obj),
341331
__vite_ssr_import_meta__: meta,
342-
__vitest_resolve_id__: resolveId,
343332

344333
// cjs compact
345-
require: createRequire(url),
334+
require: createRequire(href),
346335
exports: cjsExports,
347336
module: moduleProxy,
348337
__filename,
@@ -373,13 +362,6 @@ export class ViteNodeRunner {
373362
return context
374363
}
375364

376-
shouldResolveId(dep: string) {
377-
if (isNodeBuiltin(dep) || dep in (this.options.requestStubs || DEFAULT_REQUEST_STUBS) || dep.startsWith('/@vite'))
378-
return false
379-
380-
return !isAbsolute(dep) || !extname(dep)
381-
}
382-
383365
/**
384366
* Define if a module should be interop-ed
385367
* This function mostly for the ability to override by subclass

0 commit comments

Comments
 (0)