diff --git a/packages/vitest/src/node/cache/fsModuleCache.ts b/packages/vitest/src/node/cache/fsModuleCache.ts index 3063a4dda..5607b6bd4 100644 --- a/packages/vitest/src/node/cache/fsModuleCache.ts +++ b/packages/vitest/src/node/cache/fsModuleCache.ts @@ -219,11 +219,11 @@ export class FileSystemModuleCache { id: string, fileContent: string, ): string | null { - // bail out if file has import.meta.glob because it depends on other files + // bail out if file has a glob import because it depends on other files // TODO: figure out a way to still support it - if (fileContent.includes('import.meta.glob(')) { + if (hasGlobImport(fileContent, environment.config.consumer)) { this.saveMemoryCache(environment, id, null) - debugMemory?.(`${c.yellow('[write]')} ${id} was bailed out because it has "import.meta.glob"`) + debugMemory?.(`${c.yellow('[write]')} ${id} was bailed out because it has a glob import`) return null } @@ -536,6 +536,22 @@ function lookupFile(dir: string, fileNames: string[]): string | undefined { } } +// Vite expands these templates into `import.meta.glob` calls +const dynamicImportTemplateRE = /\bimport\s*\(\s*`[^`]*\$\{/ +const newUrlTemplateRE = /\bnew\s+URL\s*\(\s*`[^`]*\$\{[^`]*`\s*,\s*import\.meta\.url\b/ + +function hasGlobImport(code: string, consumer: 'client' | 'server'): boolean { + if (code.includes('import.meta.glob')) { + return true + } + if (!code.includes('${')) { + return false + } + return ( + dynamicImportTemplateRE.test(code) || (consumer === 'client' && newUrlTemplateRE.test(code)) + ) +} + function tryReadFileSync(file: string): string { try { return readFileSync(file, 'utf-8') diff --git a/test/e2e/test/caching.test.ts b/test/e2e/test/caching.test.ts index 132387c6d..89e986163 100644 --- a/test/e2e/test/caching.test.ts +++ b/test/e2e/test/caching.test.ts @@ -48,6 +48,176 @@ test("if file has import.meta.glob, it's not cached", async () => { `) }) +test("if file has import.meta.glob with a type argument, it's not cached", async () => { + const structure = { + 'src/a.js': 'export default 1', + 'src/b.js': 'export default 2', + 'glob.test.ts': /* ts */ ` + import { expect, inject, test } from 'vitest' + + const files = import.meta.glob<{ default: number }>('./src/*.js', { eager: true }) + + test('glob', () => { + expect(Object.keys(files)).toEqual(inject('files')) + }) + `, + } + const config = { + fsModuleCache: true, + fsModuleCachePath: './node_modules/.vitest-fs-cache', + } + + const cold = await runInlineTests(structure, { + ...config, + provide: { files: ['./src/a.js', './src/b.js'] }, + }) + + expect(cold.stderr).toBe('') + expect(cold.errorTree()).toMatchInlineSnapshot(` + { + "glob.test.ts": { + "glob": "passed", + }, + } + `) + await cold.ctx?.close() + + cold.fs.createFile('src/c.js', 'export default 3') + + const warm = await runVitest({ + root: cold.root, + ...config, + provide: { files: ['./src/a.js', './src/b.js', './src/c.js'] }, + }) + + expect(warm.stderr).toBe('') + expect(warm.errorTree()).toMatchInlineSnapshot(` + { + "glob.test.ts": { + "glob": "passed", + }, + } + `) +}) + +test("if file has a dynamic import with a template literal, it's not cached", async () => { + const structure = { + 'src/a.js': `export default 'a'`, + 'src/b.js': `export default 'b'`, + 'load.test.js': /* js */ ` + import { expect, inject, test } from 'vitest' + + const load = (name) => import(\`./src/\${name}.js\`) + + test('load', async () => { + const names = inject('names') + const modules = await Promise.all(names.map(load)) + expect(modules.map((m) => m.default)).toEqual(names) + }) + `, + } + const config = { + fsModuleCache: true, + fsModuleCachePath: './node_modules/.vitest-fs-cache', + } + + const cold = await runInlineTests(structure, { + ...config, + provide: { names: ['a', 'b'] }, + }) + + expect(cold.stderr).toBe('') + expect(cold.errorTree()).toMatchInlineSnapshot(` + { + "load.test.js": { + "load": "passed", + }, + } + `) + await cold.ctx?.close() + + cold.fs.createFile('src/c.js', `export default 'c'`) + + const warm = await runVitest({ + root: cold.root, + ...config, + provide: { names: ['a', 'b', 'c'] }, + }) + + expect(warm.stderr).toBe('') + expect(warm.errorTree()).toMatchInlineSnapshot(` + { + "load.test.js": { + "load": "passed", + }, + } + `) +}) + +test("if file has new URL() with a template literal, it's not cached", async () => { + const structure = { + 'assets/a.png': '', + 'assets/b.png': '', + 'client-env.js': /* js */ ` + export default { + name: 'client-env', + viteEnvironment: 'client', + setup() { + return { teardown() {} } + }, + } + `, + 'url.test.js': /* js */ ` + import { expect, inject, test } from 'vitest' + + const getUrl = (name) => new URL(\`./assets/\${name}.png\`, import.meta.url) + + test('url', () => { + const names = inject('names') + const files = names.map((name) => getUrl(name).pathname.split('/').pop()) + expect(files).toEqual(names.map((name) => \`\${name}.png\`)) + }) + `, + } + const config = { + environment: './client-env.js', + fsModuleCache: true, + fsModuleCachePath: './node_modules/.vitest-fs-cache', + } + + const cold = await runInlineTests(structure, { + ...config, + provide: { names: ['a', 'b'] }, + }) + + expect(cold.stderr).toBe('') + expect(cold.errorTree()).toMatchInlineSnapshot(` + { + "url.test.js": { + "url": "passed", + }, + } + `) + await cold.ctx?.close() + + cold.fs.createFile('assets/c.png', '') + + const warm = await runVitest({ + root: cold.root, + ...config, + provide: { names: ['a', 'b', 'c'] }, + }) + + expect(warm.stderr).toBe('') + expect(warm.errorTree()).toMatchInlineSnapshot(` + { + "url.test.js": { + "url": "passed", + }, + } + `) +}) + test('if no cache key generator is defined, the hash is invalid', async () => { process.env.REPLACED = 'value1' @@ -409,5 +579,6 @@ test('if cache key generator bails out, the file is not cached', async () => { declare module 'vitest' { export interface ProvidedContext { generated: string[] + names: string[] } }