diff --git a/packages/mocker/src/node/hoistMocks.ts b/packages/mocker/src/node/hoistMocks.ts index 13a847eeb..56896d646 100644 --- a/packages/mocker/src/node/hoistMocks.ts +++ b/packages/mocker/src/node/hoistMocks.ts @@ -290,9 +290,10 @@ export function hoistMocks( let hasMockApiCall = false esmWalker(ast, { + // called after all call expressions are visited onIdentifier(id, info, parentStack) { const binding = idToImportMap.get(id.name) - if (!binding) { + if (!binding || !hasMockApiCall) { return } diff --git a/packages/mocker/src/node/parsers.ts b/packages/mocker/src/node/parsers.ts index 1ee659d90..ea5872730 100644 --- a/packages/mocker/src/node/parsers.ts +++ b/packages/mocker/src/node/parsers.ts @@ -39,21 +39,22 @@ export function collectModuleExports( if (format === 'module') { const [imports_, exports_] = parseModuleSyntax(code, filename) const fileExports = exports_.map((p) => p.n) + // cached before re-exports are parsed, so circular re-exports stop here + cachedFileExports.set(filename, fileExports) imports_.forEach(({ ss: start, se: end, n: name }) => { const substring = code.substring(start, end).replace(/ +/g, ' ') if (name && substring.startsWith('export *') && !substring.startsWith('export * as')) { fileExports.push(...tryParseModule(name)) } }) - cachedFileExports.set(filename, fileExports) exports.push(...fileExports) } else { const { exports: exports_, reexports } = parseCjsSyntax(code, filename) const fileExports = [...exports_] + cachedFileExports.set(filename, fileExports) reexports.forEach((name) => { fileExports.push(...tryParseModule(name)) }) - cachedFileExports.set(filename, fileExports) exports.push(...fileExports) } @@ -96,27 +97,26 @@ export function collectModuleExports( return cachedFileExports.get(resolvedModulePath)! } - const fileContent = readFileSync(resolvedModulePath, 'utf-8') const ext = extname(resolvedModulePath) - const code = transformCode(fileContent, resolvedModulePath) - if (code == null) { - cachedFileExports.set(resolvedModulePath, []) - return [] - } - - const resolvedModuleFormat = resolveModuleFormat(resolvedModulePath, code) if (ext === '.json') { return ['default'] - } else { + } + + const fileContent = readFileSync(resolvedModulePath, 'utf-8') + const code = transformCode(fileContent, resolvedModulePath) + const resolvedModuleFormat = resolveModuleFormat( + pathToFileURL(resolvedModulePath).toString(), + code, + ) + if (!resolvedModuleFormat) { // can't do wasm, for example console.warn( - `Cannot process '${resolvedModuleFormat}' imported from ${filename} because of unknown file extension: ${ext}.`, + `Cannot process '${resolvedModulePath}' imported from ${filename} because of unknown file extension: ${ext}.`, ) + cachedFileExports.set(resolvedModulePath, []) + return [] } - if (resolvedModuleFormat) { - return collectModuleExports(resolvedModulePath, code, resolvedModuleFormat, exports) - } - return [] + return collectModuleExports(resolvedModulePath, code, resolvedModuleFormat, exports) } return Array.from(new Set(exports)) diff --git a/test/e2e/test/no-module-runner.test.ts b/test/e2e/test/no-module-runner.test.ts index 248994277..3e7b3a78d 100644 --- a/test/e2e/test/no-module-runner.test.ts +++ b/test/e2e/test/no-module-runner.test.ts @@ -508,6 +508,88 @@ if (import.meta.vitest) { `) }) + test('imports are kept when the file has no hoisted calls', async () => { + const { stderr, testTree } = await runNoViteModuleRunnerTests({ + 'add.js': `export const add = (a, b) => a + b`, + 'comment.test.js': /* js */ ` +import { add } from './add.js' +// vi.mock('./add.js') +test('add', () => { + expect(add(1, 2)).toBe(3) +}) + `, + 'import-actual.test.js': /* js */ ` +import { add } from './add.js' +test('add', async () => { + const actual = await vi.importActual('./add.js') + expect(add).toBe(actual.add) +}) + `, + }) + expect(stderr).toBe('') + expect(testTree()).toMatchInlineSnapshot(` + { + "comment.test.js": { + "add": "passed", + }, + "import-actual.test.js": { + "add": "passed", + }, + } + `) + }) + + test('manual mock keeps names re-exported with export *', async () => { + const { stderr, testTree } = await runNoViteModuleRunnerTests({ + 'base.js': `export function fromBase() { return 'base' }`, + 'star.js': /* js */ ` +export * from './base.js' +export function own() { return 'own' } + `, + 'star.test.js': /* js */ ` +import { fromBase, own } from './star.js' +vi.mock(import('./star.js'), () => ({ + own: () => 'mocked-own', + fromBase: () => 'mocked-base', +})) +test('re-exported names are mocked', () => { + expect(own()).toBe('mocked-own') + expect(fromBase()).toBe('mocked-base') +}) + `, + 'users.js': /* js */ ` +export * from './posts.js' +export const getUser = () => 'user' + `, + 'posts.js': /* js */ ` +export * from './users.js' +export const getPost = () => 'post' + `, + 'circular.test.js': /* js */ ` +import { getPost, getUser } from './users.js' +vi.mock(import('./users.js'), () => ({ + getUser: () => 'mocked-user', + getPost: () => 'mocked-post', +})) +test('circular re-exports are mocked', () => { + expect(getUser()).toBe('mocked-user') + expect(getPost()).toBe('mocked-post') +}) + `, + }) + expect(stderr).toBe('') + expect(testTree()).toMatchInlineSnapshot(` + { + "circular.test.js": { + "circular re-exports are mocked": "passed", + }, + "star.test.js": { + "re-exported names are mocked": "passed", + }, + } + `) + }) + test('cannot import JS file without extension in ESM', async () => { const { stderr, root } = await runNoViteModuleRunnerTests({ 'add.js': /* js */ `