diff --git a/packages/vite-node/src/client.ts b/packages/vite-node/src/client.ts index b465eb67a8ae..3d357cfe7b3f 100644 --- a/packages/vite-node/src/client.ts +++ b/packages/vite-node/src/client.ts @@ -9,6 +9,7 @@ import { extractSourceMap } from './source-map' import { cleanUrl, createImportMetaEnvProxy, + isBareImport, isInternalRequest, isNodeBuiltin, isPrimitive, @@ -325,6 +326,28 @@ export class ViteNodeRunner { return await this.cachedRequest(id, fsPath, callstack) } + private async _fetchModule(id: string, importer?: string) { + try { + return await this.options.fetchModule(id) + } + catch (cause: any) { + // rethrow vite error if it cannot load the module because it's not resolved + if ( + (typeof cause === 'object' && cause.code === 'ERR_LOAD_URL') + || (typeof cause?.message === 'string' && cause.message.includes('Failed to load url')) + ) { + const error = new Error( + `Cannot find ${isBareImport(id) ? 'package' : 'module'} '${id}'${importer ? ` imported from '${importer}'` : ''}`, + { cause }, + ) as Error & { code: string } + error.code = 'ERR_MODULE_NOT_FOUND' + throw error + } + + throw cause + } + } + /** @internal */ async directRequest(id: string, fsPath: string, _callstack: string[]) { const moduleId = normalizeModuleId(fsPath) @@ -345,7 +368,10 @@ export class ViteNodeRunner { if (id in requestStubs) { return requestStubs[id] } - let { code: transformed, externalize } = await this.options.fetchModule(id) + let { code: transformed, externalize } = await this._fetchModule( + id, + callstack[callstack.length - 2], + ) if (externalize) { debugNative(externalize) diff --git a/packages/vite-node/src/utils.ts b/packages/vite-node/src/utils.ts index a220690ee79d..7d3b2e8d4436 100644 --- a/packages/vite-node/src/utils.ts +++ b/packages/vite-node/src/utils.ts @@ -21,6 +21,12 @@ export function slash(str: string): string { return str.replace(/\\/g, '/') } +const bareImportRE = /^(?![a-z]:)[\w@](?!.*:\/\/)/i + +export function isBareImport(id: string): boolean { + return bareImportRE.test(id) +} + export const VALID_ID_PREFIX = '/@id/' export function normalizeRequestId(id: string, base?: string): string { diff --git a/packages/vitest/src/runtime/external-executor.ts b/packages/vitest/src/runtime/external-executor.ts index c2d0875baadc..680294603a23 100644 --- a/packages/vitest/src/runtime/external-executor.ts +++ b/packages/vitest/src/runtime/external-executor.ts @@ -6,7 +6,7 @@ import fs from 'node:fs' import { dirname } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { extname, join, normalize } from 'pathe' -import { getCachedData, isNodeBuiltin, setCacheData } from 'vite-node/utils' +import { getCachedData, isBareImport, isNodeBuiltin, setCacheData } from 'vite-node/utils' import { CommonjsExecutor } from './vm/commonjs-executor' import { EsmExecutor } from './vm/esm-executor' import { ViteExecutor } from './vm/vite-executor' @@ -209,7 +209,7 @@ export class ExternalModulesExecutor { (type === 'module' || type === 'commonjs' || type === 'wasm') && !existsSync(path) ) { - const error = new Error(`Cannot find module '${path}'`); + const error = new Error(`Cannot find ${isBareImport(path) ? 'package' : 'module'} '${path}'`); (error as any).code = 'ERR_MODULE_NOT_FOUND' throw error } diff --git a/packages/vitest/src/runtime/vm/vite-executor.ts b/packages/vitest/src/runtime/vm/vite-executor.ts index c27f11af0d4f..a99491a384fa 100644 --- a/packages/vitest/src/runtime/vm/vite-executor.ts +++ b/packages/vitest/src/runtime/vm/vite-executor.ts @@ -63,13 +63,30 @@ export class ViteExecutor { return cached } return this.esm.createEsModule(fileUrl, async () => { - const result = await this.options.transform(fileUrl, 'web') - if (!result.code) { - throw new Error( - `[vitest] Failed to transform ${fileUrl}. Does the file exist?`, - ) + try { + const result = await this.options.transform(fileUrl, 'web') + if (result.code) { + return result.code + } } - return result.code + catch (cause: any) { + // rethrow vite error if it cannot load the module because it's not resolved + if ( + (typeof cause === 'object' && cause.code === 'ERR_LOAD_URL') + || (typeof cause?.message === 'string' && cause.message.includes('Failed to load url')) + ) { + const error = new Error( + `Cannot find module '${fileUrl}'`, + { cause }, + ) as Error & { code: string } + error.code = 'ERR_MODULE_NOT_FOUND' + throw error + } + } + + throw new Error( + `[vitest] Failed to transform ${fileUrl}. Does the file exist?`, + ) }) } diff --git a/test/cli/fixtures/vm-threads/not-found.test.ts b/test/cli/fixtures/vm-threads/not-found.test.ts index 67e537ae05c2..a0590a193dd5 100644 --- a/test/cli/fixtures/vm-threads/not-found.test.ts +++ b/test/cli/fixtures/vm-threads/not-found.test.ts @@ -22,7 +22,7 @@ it('package', async () => { it('builtin', async () => { await expect(() => notFound.importBuiltin()).rejects.toMatchObject({ code: 'ERR_MODULE_NOT_FOUND', - message: 'Cannot find module \'node:non-existing-builtin\'', + message: 'Cannot find package \'node:non-existing-builtin\'', }) }) @@ -31,6 +31,6 @@ it('builtin', async () => { it('namespace', async () => { await expect(() => notFound.importNamespace()).rejects.toMatchObject({ code: 'ERR_MODULE_NOT_FOUND', - message: 'Cannot find module \'non-existing-namespace:xyz\'', + message: 'Cannot find package \'non-existing-namespace:xyz\'', }) }) diff --git a/test/core/test/dynamic-import.test.ts b/test/core/test/dynamic-import.test.ts new file mode 100644 index 000000000000..ed8ecb5ade4a --- /dev/null +++ b/test/core/test/dynamic-import.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from 'vitest' + +test('dynamic import', async () => { + try { + await import('non-existing-module' as any) + expect.unreachable() + } + catch (err: any) { + expect(err.message).toBe( + `Cannot find package 'non-existing-module' imported from '${import.meta.filename}'`, + ) + expect(err.code).toBe('ERR_MODULE_NOT_FOUND') + } +}) diff --git a/test/core/test/imports.test.ts b/test/core/test/imports.test.ts index 9d1d18fe1633..e0d599180c2e 100644 --- a/test/core/test/imports.test.ts +++ b/test/core/test/imports.test.ts @@ -82,9 +82,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 url \.\/some-unknown-path/) + await expect(imported).rejects.toThrowError(/Cannot find module '\.\/some-unknown-path' imported/) // @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 () => { diff --git a/test/core/test/web-worker-jsdom.test.ts b/test/core/test/web-worker-jsdom.test.ts index 35165476974c..4a6a7795f36b 100644 --- a/test/core/test/web-worker-jsdom.test.ts +++ b/test/core/test/web-worker-jsdom.test.ts @@ -18,7 +18,7 @@ it('worker with invalid url throws an error', async () => { if (!import.meta.env.VITEST_VM_POOL) { expect(event.error).toBeInstanceOf(Error) } - expect(event.error.message).toContain('Failed to load') + expect(event.error.message).toContain('Cannot find module') }) it('throws an error on invalid path', async () => { @@ -34,7 +34,7 @@ it('throws an error on invalid path', async () => { if (!import.meta.env.VITEST_VM_POOL) { expect(event.error).toBeInstanceOf(Error) } - expect(event.error.message).toContain('Failed to load') + expect(event.error.message).toContain('Cannot find module') }) it('returns globals on self correctly', async () => {