import type { SourceMap } from 'magic-string' import type { Environment, Plugin, Rollup } from 'vite' import type { Expression, Positioned } from './esmWalker' import MagicString from 'magic-string' import { esmWalker } from './esmWalker' const regexDynamicImport = /import\s*\(/ export interface DynamicImportPluginOptions { /** * @default `"__vitest_mocker__"` */ globalThisAccessor?: string filter?: (id: string, environment: Environment) => boolean } export function dynamicImportPlugin(options: DynamicImportPluginOptions = {}): Plugin { return { name: 'vitest:browser:esm-injector', enforce: 'post', transform: { order: 'post', handler(source, id) { // TODO: test is not called for static imports if (!regexDynamicImport.test(source)) { return } if (options.filter && !options.filter(id, this.environment)) { return } return injectDynamicImport(source, id, this.parse, options) }, }, } } export interface DynamicImportInjectorResult { code: string map: SourceMap } export function injectDynamicImport( code: string, id: string, parse: Rollup.PluginContext['parse'], options: DynamicImportPluginOptions = {}, ): DynamicImportInjectorResult | undefined { if (code.includes('wrapDynamicImport')) { return } const s = new MagicString(code) let ast: ReturnType try { ast = parse(code) } catch (err) { console.error(`Cannot parse ${id}:\n${(err as any).message}`) return } // 3. convert references to import bindings & import.meta references esmWalker(ast, { // TODO: make env updatable onImportMeta() { // s.update(node.start, node.end, viImportMetaKey) }, onDynamicImport(node) { const globalThisAccessor = options.globalThisAccessor || '"__vitest_mocker__"' const replaceString = `globalThis[${globalThisAccessor}].wrapDynamicImport(async () => import(` const importSubstring = code.substring(node.start, node.end) const hasIgnore = importSubstring.includes('/* @vite-ignore */') s.overwrite( node.start, (node.source as Positioned).start, replaceString + (hasIgnore ? '/* @vite-ignore */ ' : ''), ) s.overwrite(node.end - 1, node.end, '))') }, }) return { code: s.toString(), map: s.generateMap({ hires: 'boundary', source: id }), } }