Skip to content

Commit 1ad63b0

Browse files
authoredJun 6, 2023
feat!: throw an error, if module cannot be resolved (#3307)
1 parent a9a8ee7 commit 1ad63b0

File tree

12 files changed

+128
-44
lines changed

12 files changed

+128
-44
lines changed
 

‎docs/.vitepress/config.ts

+4
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ export default withPwa(defineConfig({
219219
text: 'Migration Guide',
220220
link: '/guide/migration',
221221
},
222+
{
223+
text: 'Common Errors',
224+
link: '/guide/common-errors',
225+
},
222226
],
223227
},
224228
{

‎docs/guide/common-errors.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Common Errors
2+
3+
## Cannot find module './relative-path'
4+
5+
If you receive an error that module cannot be found, it might mean several different things:
6+
7+
- 1. You misspelled the path. Make sure the path is correct.
8+
9+
- 2. It's possible that your rely on `baseUrl` in your `tsconfig.json`. Vite doesn't take into account `tsconfig.json` by default, so you might need to install [`vite-tsconfig-paths`](https://www.npmjs.com/package/vite-tsconfig-paths) yourself, if you rely on this behaviour.
10+
11+
```ts
12+
import { defineConfig } from 'vitest/config'
13+
import tsconfigPaths from 'vite-tsconfig-paths'
14+
15+
export default defineConfig({
16+
plugins: [tsconfigPaths()]
17+
})
18+
```
19+
20+
Or rewrite your path to not be relative to root:
21+
22+
```diff
23+
- import helpers from 'src/helpers'
24+
+ import helpers from '../src/helpers'
25+
```
26+
27+
- 3. Make sure you don't have relative [aliases](/config/#alias). Vite treats them as relative to the file where the import is instead of the root.
28+
29+
```diff
30+
import { defineConfig } from 'vitest/config'
31+
32+
export default defineConfig({
33+
test: {
34+
alias: {
35+
- '@/': './src/',
36+
+ '@/': new URL('./src/', import.meta.url).pathname,
37+
}
38+
}
39+
})
40+
```

‎packages/vite-node/src/client.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ export class ViteNodeRunner {
228228
}
229229

230230
shouldResolveId(id: string, _importee?: string) {
231-
return !isInternalRequest(id) && !isNodeBuiltin(id)
231+
return !isInternalRequest(id) && !isNodeBuiltin(id) && !id.startsWith('data:')
232232
}
233233

234234
private async _resolveUrl(id: string, importer?: string): Promise<[url: string, fsPath: string]> {
@@ -243,12 +243,18 @@ export class ViteNodeRunner {
243243
if (!this.options.resolveId || exists)
244244
return [id, path]
245245
const resolved = await this.options.resolveId(id, importer)
246-
const resolvedId = resolved
247-
? normalizeRequestId(resolved.id, this.options.base)
248-
: id
249-
// to be compatible with dependencies that do not resolve id
250-
const fsPath = resolved ? resolvedId : path
251-
return [resolvedId, fsPath]
246+
if (!resolved) {
247+
const error = new Error(
248+
`Cannot find module '${id}'${importer ? ` imported from '${importer}'` : ''}.`
249+
+ '\n\n- If you rely on tsconfig.json to resolve modules, please install "vite-tsconfig-paths" plugin to handle module resolution.'
250+
+ '\n - Make sure you don\'t have relative aliases in your Vitest config. Use absolute paths instead. Read more: https://vitest.dev/guide/common-errors',
251+
)
252+
Object.defineProperty(error, 'code', { value: 'ERR_MODULE_NOT_FOUND', enumerable: true })
253+
Object.defineProperty(error, Symbol.for('vitest.error.not_found.data'), { value: { id, importer }, enumerable: false })
254+
throw error
255+
}
256+
const resolvedId = normalizeRequestId(resolved.id, this.options.base)
257+
return [resolvedId, resolvedId]
252258
}
253259

254260
async resolveUrl(id: string, importee?: string) {

‎packages/vitest/src/runtime/execute.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -99,18 +99,37 @@ export class VitestExecutor extends ViteNodeRunner {
9999
}
100100

101101
shouldResolveId(id: string, _importee?: string | undefined): boolean {
102-
if (isInternalRequest(id))
102+
if (isInternalRequest(id) || id.startsWith('data:'))
103103
return false
104104
const environment = getCurrentEnvironment()
105105
// do not try and resolve node builtins in Node
106106
// import('url') returns Node internal even if 'url' package is installed
107107
return environment === 'node' ? !isNodeBuiltin(id) : !id.startsWith('node:')
108108
}
109109

110+
async originalResolveUrl(id: string, importer?: string) {
111+
return super.resolveUrl(id, importer)
112+
}
113+
110114
async resolveUrl(id: string, importer?: string) {
115+
if (VitestMocker.pendingIds.length)
116+
await this.mocker.resolveMocks()
117+
111118
if (importer && importer.startsWith('mock:'))
112119
importer = importer.slice(5)
113-
return super.resolveUrl(id, importer)
120+
try {
121+
return await super.resolveUrl(id, importer)
122+
}
123+
catch (error: any) {
124+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
125+
const { id } = error[Symbol.for('vitest.error.not_found.data')]
126+
const path = this.mocker.normalizePath(id)
127+
const mock = this.mocker.getDependencyMock(path)
128+
if (mock !== undefined)
129+
return [id, id] as [string, string]
130+
}
131+
throw error
132+
}
114133
}
115134

116135
async dependencyRequest(id: string, fsPath: string, callstack: string[]): Promise<any> {

‎packages/vitest/src/runtime/mocker.ts

+21-6
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function isSpecialProp(prop: Key, parentType: string) {
3939
}
4040

4141
export class VitestMocker {
42-
private static pendingIds: PendingSuiteMock[] = []
42+
public static pendingIds: PendingSuiteMock[] = []
4343
private resolveCache = new Map<string, Record<string, string>>()
4444

4545
constructor(
@@ -88,7 +88,22 @@ export class VitestMocker {
8888
}
8989

9090
private async resolvePath(rawId: string, importer: string) {
91-
const [id, fsPath] = await this.executor.resolveUrl(rawId, importer)
91+
let id: string
92+
let fsPath: string
93+
try {
94+
[id, fsPath] = await this.executor.originalResolveUrl(rawId, importer)
95+
}
96+
catch (error: any) {
97+
// it's allowed to mock unresolved modules
98+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
99+
const { id: unresolvedId } = error[Symbol.for('vitest.error.not_found.data')]
100+
id = unresolvedId
101+
fsPath = unresolvedId
102+
}
103+
else {
104+
throw error
105+
}
106+
}
92107
// external is node_module or unresolved module
93108
// for example, some people mock "vscode" and don't have it installed
94109
const external = (!isAbsolute(fsPath) || this.isAModuleDirectory(fsPath)) ? rawId : null
@@ -100,7 +115,10 @@ export class VitestMocker {
100115
}
101116
}
102117

103-
private async resolveMocks() {
118+
public async resolveMocks() {
119+
if (!VitestMocker.pendingIds.length)
120+
return
121+
104122
await Promise.all(VitestMocker.pendingIds.map(async (mock) => {
105123
const { fsPath, external } = await this.resolvePath(mock.id, mock.importer)
106124
if (mock.type === 'unmock')
@@ -353,9 +371,6 @@ export class VitestMocker {
353371
}
354372

355373
public async requestWithMock(url: string, callstack: string[]) {
356-
if (VitestMocker.pendingIds.length)
357-
await this.resolveMocks()
358-
359374
const id = this.normalizePath(url)
360375
const mock = this.getDependencyMock(id)
361376

‎packages/web-worker/src/shared-worker.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export function createSharedWorkerConstructor(): typeof SharedWorker {
110110

111111
debug('initialize shared worker %s', this._vw_name)
112112

113-
runner.executeFile(fsPath).then(() => {
113+
return runner.executeFile(fsPath).then(() => {
114114
// worker should be new every time, invalidate its sub dependency
115115
runnerOptions.moduleCache.invalidateSubDepTree([fsPath, runner.mocker.getMockPath(fsPath)])
116116
this._vw_workerTarget.dispatchEvent(
@@ -119,17 +119,17 @@ export function createSharedWorkerConstructor(): typeof SharedWorker {
119119
}),
120120
)
121121
debug('shared worker %s successfully initialized', this._vw_name)
122-
}).catch((e) => {
123-
debug('shared worker %s failed to initialize: %o', this._vw_name, e)
124-
const EventConstructor = globalThis.ErrorEvent || globalThis.Event
125-
const error = new EventConstructor('error', {
126-
error: e,
127-
message: e.message,
128-
})
129-
this.dispatchEvent(error)
130-
this.onerror?.(error)
131-
console.error(e)
132122
})
123+
}).catch((e) => {
124+
debug('shared worker %s failed to initialize: %o', this._vw_name, e)
125+
const EventConstructor = globalThis.ErrorEvent || globalThis.Event
126+
const error = new EventConstructor('error', {
127+
error: e,
128+
message: e.message,
129+
})
130+
this.dispatchEvent(error)
131+
this.onerror?.(error)
132+
console.error(e)
133133
})
134134
}
135135
}

‎packages/web-worker/src/worker.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -75,25 +75,25 @@ export function createWorkerConstructor(options?: DefineWorkerOptions): typeof W
7575

7676
debug('initialize worker %s', this._vw_name)
7777

78-
runner.executeFile(fsPath).then(() => {
78+
return runner.executeFile(fsPath).then(() => {
7979
// worker should be new every time, invalidate its sub dependency
8080
runnerOptions.moduleCache.invalidateSubDepTree([fsPath, runner.mocker.getMockPath(fsPath)])
8181
const q = this._vw_messageQueue
8282
this._vw_messageQueue = null
8383
if (q)
8484
q.forEach(([data, transfer]) => this.postMessage(data, transfer), this)
8585
debug('worker %s successfully initialized', this._vw_name)
86-
}).catch((e) => {
87-
debug('worker %s failed to initialize: %o', this._vw_name, e)
88-
const EventConstructor = globalThis.ErrorEvent || globalThis.Event
89-
const error = new EventConstructor('error', {
90-
error: e,
91-
message: e.message,
92-
})
93-
this.dispatchEvent(error)
94-
this.onerror?.(error)
95-
console.error(e)
9686
})
87+
}).catch((e) => {
88+
debug('worker %s failed to initialize: %o', this._vw_name, e)
89+
const EventConstructor = globalThis.ErrorEvent || globalThis.Event
90+
const error = new EventConstructor('error', {
91+
error: e,
92+
message: e.message,
93+
})
94+
this.dispatchEvent(error)
95+
this.onerror?.(error)
96+
console.error(e)
9797
})
9898
}
9999

‎test/core/test/imports.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ test('dynamic import has null prototype', async () => {
7474
test('dynamic import throws an error', async () => {
7575
const path = './some-unknown-path'
7676
const imported = import(path)
77-
await expect(imported).rejects.toThrowError(/Failed to load/)
77+
await expect(imported).rejects.toThrowError(/Cannot find module '\.\/some-unknown-path'/)
7878
// @ts-expect-error path does not exist
79-
await expect(() => import('./some-unknown-path')).rejects.toThrowError(/Failed to load/)
79+
await expect(() => import('./some-unknown-path')).rejects.toThrowError(/Cannot find module/)
8080
})
8181

8282
test('can import @vite/client', async () => {

‎test/core/test/unmock-import.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ test('first import', async () => {
2020
expect(data.state).toBe('STOPPED')
2121
})
2222

23-
test('second import should had been re-mock', async () => {
23+
test('second import should have been re-mocked', async () => {
2424
// @ts-expect-error I know this
2525
const { data } = await import('/data')
2626
expect(data.state).toBe('STARTED')

‎test/web-worker/test/init.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ it('worker with invalid url throws an error', async () => {
6666
})
6767
expect(event).toBeInstanceOf(ErrorEvent)
6868
expect(event.error).toBeInstanceOf(Error)
69-
expect(event.error.message).toContain('Failed to load')
69+
expect(event.error.message).toContain('Cannot find module')
7070
})
7171

7272
it('self injected into worker and its deps should be equal', async () => {

‎test/web-worker/test/sharedWorker.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, it } from 'vitest'
2-
import MySharedWorker from './src/sharedWorker?sharedworker'
2+
import MySharedWorker from '../src/sharedWorker?sharedworker'
33

44
function sendEventMessage(worker: SharedWorker, msg: any) {
55
worker.port.postMessage(msg)
@@ -50,7 +50,7 @@ it('throws an error on invalid path', async () => {
5050
})
5151
expect(event).toBeInstanceOf(ErrorEvent)
5252
expect(event.error).toBeInstanceOf(Error)
53-
expect(event.error.message).toContain('Failed to load')
53+
expect(event.error.message).toContain('Cannot find module')
5454
})
5555

5656
it('doesn\'t trigger events, if closed', async () => {

‎test/web-worker/vitest.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default defineConfig({
1212
],
1313
},
1414
onConsoleLog(log) {
15-
if (log.includes('Failed to load'))
15+
if (log.includes('Cannot find module'))
1616
return false
1717
},
1818
},

0 commit comments

Comments
 (0)
Please sign in to comment.