import path from 'node:path' import { type Plugin, type InlineConfig, build as viteBuild, mergeConfig } from 'vite' import type { SourceMapInput, RollupOutput, OutputOptions } from 'rollup' import MagicString from 'magic-string' import { cleanUrl, toRelativePath } from '../utils' import { supportImportMetaPaths } from '../electron' const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g /** * Resolve `?modulePath` import and return the module bundle path. */ export default function modulePathPlugin(config: InlineConfig): Plugin { const isImportMetaPathSupported = supportImportMetaPaths() const assetCache = new Set() return { name: 'vite:module-path', apply: 'build', enforce: 'pre', buildStart(): void { assetCache.clear() }, async load(id): Promise { if (id.endsWith('?modulePath')) { // id resolved by Vite resolve plugin const re = await bundleEntryFile(cleanUrl(id), config, this.meta.watchMode) const [outputChunk, ...outputChunks] = re.bundles.output const hash = this.emitFile({ type: 'asset', fileName: outputChunk.fileName, source: outputChunk.code }) for (const chunk of outputChunks) { if (chunk.type === 'asset' && assetCache.has(chunk.fileName)) { continue } this.emitFile({ type: 'asset', fileName: chunk.fileName, source: chunk.type === 'chunk' ? chunk.code : chunk.source }) if (chunk.type === 'asset') { assetCache.add(chunk.fileName) } } for (const id of re.watchFiles) { this.addWatchFile(id) } const refId = `__VITE_MODULE_PATH__${hash}__` const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname' return ` import { join } from 'path' export default join(${dirnameExpr}, ${refId})` } }, renderChunk(code, chunk, { sourcemap }): { code: string; map: SourceMapInput } | null { let match: RegExpExecArray | null let s: MagicString | undefined modulePathRE.lastIndex = 0 while ((match = modulePathRE.exec(code))) { s ||= new MagicString(code) const [full, hash] = match const filename = this.getFileName(hash) const outputFilepath = toRelativePath(filename, chunk.fileName) const replacement = JSON.stringify(outputFilepath) s.overwrite(match.index, match.index + full.length, replacement, { contentOnly: true }) } if (s) { return { code: s.toString(), map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null } } return null } } } async function bundleEntryFile( input: string, config: InlineConfig, watch: boolean ): Promise<{ bundles: RollupOutput; watchFiles: string[] }> { const moduleIds: string[] = [] const viteConfig = mergeConfig(config, { build: { rollupOptions: { input }, write: false, watch: false }, plugins: [ { name: 'vite:entry-file-name', outputOptions(output): OutputOptions { if (typeof output.entryFileNames !== 'function' && output.entryFileNames) { output.entryFileNames = '[name]-[hash]' + path.extname(output.entryFileNames) } return output } }, ...(watch ? [ { name: 'vite:get-watch-files', buildEnd(): void { const allModuleIds = Array.from(this.getModuleIds()) const sourceFiles = allModuleIds.filter(id => { const info = this.getModuleInfo(id) return info && !info.isExternal }) moduleIds.push(...sourceFiles) } } as Plugin ] : []) ], logLevel: 'warn', configFile: false }) const bundles = await viteBuild(viteConfig) return { bundles: bundles as RollupOutput, watchFiles: moduleIds } }