import path from 'node:path' import fs from 'node:fs' import { builtinModules } from 'node:module' import colors from 'picocolors' import { type Plugin, type LibraryOptions, type Rolldown, mergeConfig, normalizePath } from 'vite' import { getElectronNodeTarget, getElectronChromeTarget, supportESM } from '../electron' import { loadPackageData } from '../utils' export interface ElectronPluginOptions { root?: string } function findLibEntry(root: string, scope: string): string | undefined { for (const name of ['index', scope]) { for (const ext of ['js', 'ts', 'mjs', 'cjs']) { const entryFile = path.resolve(root, 'src', scope, `${name}.${ext}`) if (fs.existsSync(entryFile)) { return entryFile } } } return undefined } function findInput(root: string, scope = 'renderer'): string { const rendererDir = path.resolve(root, 'src', scope, 'index.html') if (fs.existsSync(rendererDir)) { return rendererDir } return '' } function processEnvDefine(): Record { return { 'process.env': `process.env`, 'global.process.env': `global.process.env`, 'globalThis.process.env': `globalThis.process.env` } } function resolveBuildOutputs( outputs: Rolldown.OutputOptions | Rolldown.OutputOptions[] | undefined, libOptions: LibraryOptions | false ): Rolldown.OutputOptions | Rolldown.OutputOptions[] | undefined { if (libOptions && !Array.isArray(outputs)) { const libFormats = libOptions.formats || [] return libFormats.map(format => ({ ...outputs, format })) } return outputs } export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions): Plugin { return { name: 'vite:electron-main-config-preset', apply: 'build', enforce: 'pre', config(config): void { const root = options?.root || process.cwd() const nodeTarget = getElectronNodeTarget() const pkg = loadPackageData() || { type: 'commonjs' } const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs' const defaultConfig = { resolve: { browserField: false, mainFields: ['module', 'jsnext:main', 'jsnext'], conditions: ['node'] }, build: { outDir: path.resolve(root, 'out', 'main'), target: nodeTarget, assetsDir: 'chunks', rolldownOptions: { external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])], output: {} }, reportCompressedSize: false, minify: false } } const build = config.build || {} const rolldownOptions = build.rolldownOptions || {} if (!rolldownOptions.input) { const libOptions = build.lib const outputOptions = rolldownOptions.output defaultConfig.build['lib'] = { entry: findLibEntry(root, 'main'), formats: libOptions && libOptions.formats && libOptions.formats.length > 0 ? [] : [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format] } } else { defaultConfig.build.rolldownOptions.output['format'] = format } defaultConfig.build.rolldownOptions.output['assetFileNames'] = path.posix.join( build.assetsDir || defaultConfig.build.assetsDir, '[name]-[hash].[ext]' ) const buildConfig = mergeConfig(defaultConfig.build, build) config.build = buildConfig config.resolve = mergeConfig(defaultConfig.resolve, config.resolve || {}) config.define = config.define || {} config.define = { ...processEnvDefine(), ...config.define } config.envPrefix = config.envPrefix || ['MAIN_VITE_', 'VITE_'] config.publicDir = config.publicDir || 'resources' // do not copy public dir config.build.copyPublicDir = false // module preload polyfill does not apply to nodejs (main process) config.build.modulePreload = false // enable ssr build config.build.ssr = true config.build.ssrEmitAssets = true config.ssr = { ...config.ssr, ...{ noExternal: true } } config.build.rollupOptions = config.build.rolldownOptions } } } export function electronMainConfigValidatorPlugin(): Plugin { return { name: 'vite:electron-main-config-validator', apply: 'build', enforce: 'post', configResolved(config): void { const build = config.build if (!build.target) { throw new Error('build.target option is required in the electron vite main config.') } else { const targets = Array.isArray(build.target) ? build.target : [build.target] if (targets.some(t => !t.startsWith('node'))) { throw new Error('The electron vite main config build.target option must be "node?".') } } const libOptions = build.lib const rolldownOptions = build.rolldownOptions if (!(libOptions && libOptions.entry) && !rolldownOptions?.input) { throw new Error( 'An entry point is required in the electron vite main config, ' + 'which can be specified using build.lib.entry, build.rollupOptions.input, ' + 'or build.rolldownOptions.input.' ) } const resolvedOutputs = resolveBuildOutputs(rolldownOptions.output, libOptions) if (resolvedOutputs) { const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs] if (outputs.length > 1) { throw new Error('The electron vite main config does not support multiple outputs.') } else { const outpout = outputs[0] if (['es', 'cjs'].includes(outpout.format || '')) { if (outpout.format === 'es' && !supportESM()) { throw new Error( 'The electron vite main config output format does not support "es", ' + 'you can upgrade electron to the latest version or switch to "cjs" format.' ) } } else { throw new Error( `The electron vite main config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.` ) } } } } } } export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOptions): Plugin { return { name: 'vite:electron-preload-config-preset', apply: 'build', enforce: 'pre', config(config): void { const root = options?.root || process.cwd() const nodeTarget = getElectronNodeTarget() const pkg = loadPackageData() || { type: 'commonjs' } const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs' const defaultConfig = { ssr: { resolve: { conditions: ['module', 'browser', 'development|production'], mainFields: ['browser', 'module', 'jsnext:main', 'jsnext'] } }, build: { outDir: path.resolve(root, 'out', 'preload'), target: nodeTarget, assetsDir: 'chunks', rolldownOptions: { external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])], output: {} }, reportCompressedSize: false, minify: false } } const build = config.build || {} const rolldownOptions = build.rolldownOptions || {} if (!rolldownOptions.input) { const libOptions = build.lib const outputOptions = rolldownOptions.output defaultConfig.build['lib'] = { entry: findLibEntry(root, 'preload'), formats: libOptions && libOptions.formats && libOptions.formats.length > 0 ? [] : [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format] } } else { defaultConfig.build.rolldownOptions.output['format'] = format } defaultConfig.build.rolldownOptions.output['assetFileNames'] = path.posix.join( build.assetsDir || defaultConfig.build.assetsDir, '[name]-[hash].[ext]' ) const buildConfig = mergeConfig(defaultConfig.build, build) config.build = buildConfig const resolvedOutputs = resolveBuildOutputs(config.build.rolldownOptions!.output, config.build.lib || false) if (resolvedOutputs) { const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs] if (outputs.find(({ format }) => format === 'es')) { if (Array.isArray(config.build.rolldownOptions!.output)) { config.build.rolldownOptions!.output.forEach(output => { if (output.format === 'es') { output['entryFileNames'] = '[name].mjs' output['chunkFileNames'] = '[name]-[hash].mjs' } }) } else { config.build.rolldownOptions!.output!['entryFileNames'] = '[name].mjs' config.build.rolldownOptions!.output!['chunkFileNames'] = config.build.lib ? '[name]-[hash].mjs' : path.posix.join(build.assetsDir || defaultConfig.build.assetsDir, '[name]-[hash].mjs') } } } config.define = config.define || {} config.define = { ...processEnvDefine(), ...config.define } config.envPrefix = config.envPrefix || ['PRELOAD_VITE_', 'VITE_'] config.publicDir = config.publicDir || 'resources' // do not copy public dir config.build.copyPublicDir = false // module preload polyfill does not apply to nodejs (preload scripts) config.build.modulePreload = false // enable ssr build config.build.ssr = true config.build.ssrEmitAssets = true config.ssr = mergeConfig(defaultConfig.ssr, config.ssr || {}) config.ssr.noExternal = true config.build.rollupOptions = config.build.rolldownOptions } } } export function electronPreloadConfigValidatorPlugin(): Plugin { return { name: 'vite:electron-preload-config-validator', apply: 'build', enforce: 'post', configResolved(config): void { const build = config.build if (!build.target) { throw new Error('build.target option is required in the electron vite preload config.') } else { const targets = Array.isArray(build.target) ? build.target : [build.target] if (targets.some(t => !t.startsWith('node'))) { throw new Error('The electron vite preload config build.target must be "node?".') } } const libOptions = build.lib const rolldownOptions = build.rolldownOptions if (!(libOptions && libOptions.entry) && !rolldownOptions?.input) { throw new Error( 'An entry point is required in the electron vite preload config, ' + 'which can be specified using build.lib.entry, build.rollupOptions.input, ' + ' or build.rolldownOptions.input.' ) } const resolvedOutputs = resolveBuildOutputs(rolldownOptions.output, libOptions) if (resolvedOutputs) { const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs] if (outputs.length > 1) { throw new Error('The electron vite preload config does not support multiple outputs.') } else { const outpout = outputs[0] if (['es', 'cjs'].includes(outpout.format || '')) { if (outpout.format === 'es' && !supportESM()) { throw new Error( 'The electron vite preload config output format does not support "es", ' + 'you can upgrade electron to the latest version or switch to "cjs" format.' ) } } else { throw new Error( `The electron vite preload config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.` ) } } } } } } export function electronRendererConfigPresetPlugin(options?: ElectronPluginOptions): Plugin { return { name: 'vite:electron-renderer-config-preset', enforce: 'pre', config(config): void { const root = options?.root || process.cwd() config.base = config.mode === 'production' || process.env.NODE_ENV_ELECTRON_VITE === 'production' ? './' : config.base config.root = config.root || './src/renderer' const chromeTarget = getElectronChromeTarget() const emptyOutDir = (): boolean => { let outDir = config.build?.outDir if (outDir) { if (!path.isAbsolute(outDir)) { outDir = path.resolve(root, outDir) } const resolvedRoot = normalizePath(path.resolve(root)) return normalizePath(outDir).startsWith(resolvedRoot + '/') } return true } const defaultConfig = { build: { outDir: path.resolve(root, 'out', 'renderer'), target: chromeTarget, modulePreload: { polyfill: false }, rolldownOptions: { input: findInput(root) }, reportCompressedSize: false, minify: false, emptyOutDir: emptyOutDir() } } if (config.build?.outDir) { config.build.outDir = path.resolve(root, config.build.outDir) } const buildConfig = mergeConfig(defaultConfig.build, config.build || {}) config.build = buildConfig config.envDir = config.envDir || path.resolve(root) config.envPrefix = config.envPrefix || ['RENDERER_VITE_', 'VITE_'] config.build.rollupOptions = config.build.rolldownOptions } } } export function electronRendererConfigValidatorPlugin(): Plugin { return { name: 'vite:electron-renderer-config-validator', enforce: 'post', configResolved(config): void { if (config.base !== './' && config.base !== '/') { config.logger.warn(colors.yellow('(!) Should not set "base" option for the electron vite renderer config.')) } const build = config.build if (!build.target) { throw new Error('build.target option is required in the electron vite renderer config.') } else { const targets = Array.isArray(build.target) ? build.target : [build.target] if (targets.some(t => !t.startsWith('chrome') && !/^es((202\d{1})|next)$/.test(t))) { config.logger.warn( 'The electron vite renderer config build.target is not "chrome?" or "es?". This could be a mistake.' ) } } const rolldownOptions = build.rolldownOptions if (!rolldownOptions.input) { config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory.`)) throw new Error( 'build.rollupOptions.input or build.rolldownOptions.input option is required in the electron vite renderer config.' ) } } } }