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

feat!: throw an error, if module cannot be resolved #3307

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ export default withPwa(defineConfig({
text: 'Migration Guide',
link: '/guide/migration',
},
{
text: 'Common Errors',
link: '/guide/common-errors',
},
],
},
{
Expand Down
40 changes: 40 additions & 0 deletions docs/guide/common-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Common Errors

## Cannot find module './relative-path'

If you receive an error that module cannot be found, it might mean several different things:

- 1. You misspelled the path. Make sure the path is correct.

- 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.

```ts
import { defineConfig } from 'vitest/config'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
plugins: [tsconfigPaths()]
})
```

Or rewrite your path to not be relative to root:

```diff
- import helpers from 'src/helpers'
+ import helpers from '../src/helpers'
```

- 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.

```diff
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
alias: {
- '@/': './src/',
+ '@/': new URL('./src/', import.meta.url).pathname,
}
}
})
```
20 changes: 13 additions & 7 deletions packages/vite-node/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export class ViteNodeRunner {
}

shouldResolveId(id: string, _importee?: string) {
return !isInternalRequest(id) && !isNodeBuiltin(id)
return !isInternalRequest(id) && !isNodeBuiltin(id) && !id.startsWith('data:')
}

private async _resolveUrl(id: string, importer?: string): Promise<[url: string, fsPath: string]> {
Expand All @@ -234,12 +234,18 @@ export class ViteNodeRunner {
if (!this.options.resolveId || exists)
return [id, path]
const resolved = await this.options.resolveId(id, importer)
const resolvedId = resolved
? normalizeRequestId(resolved.id, this.options.base)
: id
// to be compatible with dependencies that do not resolve id
const fsPath = resolved ? resolvedId : path
return [resolvedId, fsPath]
if (!resolved) {
const error = new Error(
`Cannot find module '${id}'${importer ? ` imported from '${importer}'` : ''}.`
+ '\n\n- If you rely on tsconfig.json to resolve modules, please install "vite-tsconfig-paths" plugin to handle module resolution.'
+ '\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',
)
Object.defineProperty(error, 'code', { value: 'ERR_MODULE_NOT_FOUND', enumerable: true })
Object.defineProperty(error, Symbol.for('vitest.error.not_found.data'), { value: { id, importer }, enumerable: false })
throw error
}
const resolvedId = normalizeRequestId(resolved.id, this.options.base)
return [resolvedId, resolvedId]
}

async resolveUrl(id: string, importee?: string) {
Expand Down
23 changes: 21 additions & 2 deletions packages/vitest/src/runtime/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,37 @@ export class VitestExecutor extends ViteNodeRunner {
}

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

async originalResolveUrl(id: string, importer?: string) {
return super.resolveUrl(id, importer)
}

async resolveUrl(id: string, importer?: string) {
if (VitestMocker.pendingIds.length)
await this.mocker.resolveMocks()

if (importer && importer.startsWith('mock:'))
importer = importer.slice(5)
return super.resolveUrl(id, importer)
try {
return await super.resolveUrl(id, importer)
}
catch (error: any) {
if (error.code === 'ERR_MODULE_NOT_FOUND') {
const { id } = error[Symbol.for('vitest.error.not_found.data')]
const path = this.mocker.normalizePath(id)
const mock = this.mocker.getDependencyMock(path)
if (mock !== undefined)
return [id, id] as [string, string]
}
throw error
}
}

async dependencyRequest(id: string, fsPath: string, callstack: string[]): Promise<any> {
Expand Down
27 changes: 21 additions & 6 deletions packages/vitest/src/runtime/mocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function isSpecialProp(prop: Key, parentType: string) {
}

export class VitestMocker {
private static pendingIds: PendingSuiteMock[] = []
public static pendingIds: PendingSuiteMock[] = []
private resolveCache = new Map<string, Record<string, string>>()

constructor(
Expand Down Expand Up @@ -80,7 +80,22 @@ export class VitestMocker {
}

private async resolvePath(rawId: string, importer: string) {
const [id, fsPath] = await this.executor.resolveUrl(rawId, importer)
let id: string
let fsPath: string
try {
[id, fsPath] = await this.executor.originalResolveUrl(rawId, importer)
}
catch (error: any) {
// it's allowed to mock unresolved modules
if (error.code === 'ERR_MODULE_NOT_FOUND') {
const { id: unresolvedId } = error[Symbol.for('vitest.error.not_found.data')]
id = unresolvedId
fsPath = unresolvedId
}
else {
throw error
}
}
// external is node_module or unresolved module
// for example, some people mock "vscode" and don't have it installed
const external = (!isAbsolute(fsPath) || fsPath.includes('/node_modules/')) ? rawId : null
Expand All @@ -92,7 +107,10 @@ export class VitestMocker {
}
}

private async resolveMocks() {
public async resolveMocks() {
if (!VitestMocker.pendingIds.length)
return

await Promise.all(VitestMocker.pendingIds.map(async (mock) => {
const { fsPath, external } = await this.resolvePath(mock.id, mock.importer)
if (mock.type === 'unmock')
Expand Down Expand Up @@ -340,9 +358,6 @@ export class VitestMocker {
}

public async requestWithMock(url: string, callstack: string[]) {
if (VitestMocker.pendingIds.length)
await this.resolveMocks()

const id = this.normalizePath(url)
const mock = this.getDependencyMock(id)

Expand Down
22 changes: 11 additions & 11 deletions packages/web-worker/src/shared-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function createSharedWorkerConstructor(): typeof SharedWorker {

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

runner.executeFile(fsPath).then(() => {
return runner.executeFile(fsPath).then(() => {
// worker should be new every time, invalidate its sub dependency
runnerOptions.moduleCache.invalidateSubDepTree([fsPath, runner.mocker.getMockPath(fsPath)])
this._vw_workerTarget.dispatchEvent(
Expand All @@ -119,17 +119,17 @@ export function createSharedWorkerConstructor(): typeof SharedWorker {
}),
)
debug('shared worker %s successfully initialized', this._vw_name)
}).catch((e) => {
debug('shared worker %s failed to initialize: %o', this._vw_name, e)
const EventConstructor = globalThis.ErrorEvent || globalThis.Event
const error = new EventConstructor('error', {
error: e,
message: e.message,
})
this.dispatchEvent(error)
this.onerror?.(error)
console.error(e)
})
}).catch((e) => {
debug('shared worker %s failed to initialize: %o', this._vw_name, e)
const EventConstructor = globalThis.ErrorEvent || globalThis.Event
const error = new EventConstructor('error', {
error: e,
message: e.message,
})
this.dispatchEvent(error)
this.onerror?.(error)
console.error(e)
})
}
}
Expand Down
22 changes: 11 additions & 11 deletions packages/web-worker/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,25 +75,25 @@ export function createWorkerConstructor(options?: DefineWorkerOptions): typeof W

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

runner.executeFile(fsPath).then(() => {
return runner.executeFile(fsPath).then(() => {
// worker should be new every time, invalidate its sub dependency
runnerOptions.moduleCache.invalidateSubDepTree([fsPath, runner.mocker.getMockPath(fsPath)])
const q = this._vw_messageQueue
this._vw_messageQueue = null
if (q)
q.forEach(([data, transfer]) => this.postMessage(data, transfer), this)
debug('worker %s successfully initialized', this._vw_name)
}).catch((e) => {
debug('worker %s failed to initialize: %o', this._vw_name, e)
const EventConstructor = globalThis.ErrorEvent || globalThis.Event
const error = new EventConstructor('error', {
error: e,
message: e.message,
})
this.dispatchEvent(error)
this.onerror?.(error)
console.error(e)
})
}).catch((e) => {
debug('worker %s failed to initialize: %o', this._vw_name, e)
const EventConstructor = globalThis.ErrorEvent || globalThis.Event
const error = new EventConstructor('error', {
error: e,
message: e.message,
})
this.dispatchEvent(error)
this.onerror?.(error)
console.error(e)
})
}

Expand Down
4 changes: 2 additions & 2 deletions test/core/test/imports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ test('dynamic import has null prototype', async () => {
test('dynamic import throws an error', async () => {
const path = './some-unknown-path'
const imported = import(path)
await expect(imported).rejects.toThrowError(/Failed to load/)
await expect(imported).rejects.toThrowError(/Cannot find module '\.\/some-unknown-path'/)
// @ts-expect-error path does not exist
await expect(() => import('./some-unknown-path')).rejects.toThrowError(/Failed to load/)
await expect(() => import('./some-unknown-path')).rejects.toThrowError(/Cannot find module/)
})

test('can import @vite/client', async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/core/test/unmock-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ test('first import', async () => {
expect(data.state).toBe('STOPPED')
})

test('second import should had been re-mock', async () => {
test('second import should have been re-mocked', async () => {
// @ts-expect-error I know this
const { data } = await import('/data')
expect(data.state).toBe('STARTED')
Expand Down
2 changes: 1 addition & 1 deletion test/web-worker/test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ it('worker with invalid url throws an error', async () => {
})
expect(event).toBeInstanceOf(ErrorEvent)
expect(event.error).toBeInstanceOf(Error)
expect(event.error.message).toContain('Failed to load')
expect(event.error.message).toContain('Cannot find module')
})

it('self injected into worker and its deps should be equal', async () => {
Expand Down
4 changes: 2 additions & 2 deletions test/web-worker/test/sharedWorker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, it } from 'vitest'
import MySharedWorker from './src/sharedWorker?sharedworker'
import MySharedWorker from '../src/sharedWorker?sharedworker'

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

it('doesn\'t trigger events, if closed', async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/web-worker/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default defineConfig({
],
},
onConsoleLog(log) {
if (log.includes('Failed to load'))
if (log.includes('Cannot find module'))
return false
},
},
Expand Down