import path from 'node:path' import fs from 'node:fs' import { pathToFileURL } from 'node:url' import { createRequire } from 'node:module' import colors from 'picocolors' import { type UserConfig as ViteConfig, type UserConfigExport as UserViteConfigExport, type ConfigEnv, type Plugin, type LogLevel, createLogger, mergeConfig, normalizePath } from 'vite' import { build } from 'esbuild' import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugins/electron' import workerPlugin from './plugins/worker' import { isObject, dynamicImport } from './utils' export { defineConfig as defineViteConfig } from 'vite' export interface UserConfig { /** * Vite config options for electron main process * * https://cn.vitejs.dev/config/ */ main?: ViteConfig & { configFile?: string | false } /** * Vite config options for electron renderer process * * https://cn.vitejs.dev/config/ */ renderer?: ViteConfig & { configFile?: string | false } /** * Vite config options for electron preload files * * https://cn.vitejs.dev/config/ */ preload?: ViteConfig & { configFile?: string | false } } export interface UserConfigSchema { /** * Vite config options for electron main process * * https://cn.vitejs.dev/config/ */ main?: UserViteConfigExport /** * Vite config options for electron renderer process * * https://cn.vitejs.dev/config/ */ renderer?: UserViteConfigExport /** * Vite config options for electron preload files * * https://cn.vitejs.dev/config/ */ preload?: UserViteConfigExport } export type InlineConfig = Omit & { configFile?: string | false envFile?: false ignoreConfigWarning?: boolean } export type UserConfigFn = (env: ConfigEnv) => UserConfigSchema | Promise export type UserConfigExport = UserConfigSchema | Promise | UserConfigFn /** * Type helper to make it easier to use `electron.vite.config.*` * accepts a direct {@link UserConfig} object, or a function that returns it. * The function receives a object that exposes two properties: * `command` (either `'build'` or `'serve'`), and `mode`. */ export function defineConfig(config: UserConfigExport): UserConfigExport { return config } export interface ResolvedConfig { config?: UserConfig configFile?: string configFileDependencies: string[] } export async function resolveConfig( inlineConfig: InlineConfig, command: 'build' | 'serve', defaultMode = 'development' ): Promise { const config = inlineConfig const mode = inlineConfig.mode || defaultMode config.mode = mode process.env.NODE_ENV = defaultMode let userConfig: UserConfig | undefined let configFileDependencies: string[] = [] let { configFile } = config if (configFile !== false) { const configEnv = { mode, command } const loadResult = await loadConfigFromFile( configEnv, configFile, config.root, config.logLevel, config.ignoreConfigWarning ) if (loadResult) { const root = config.root delete config.root delete config.configFile const outDir = config.build?.outDir if (loadResult.config.main) { const mainViteConfig: ViteConfig = mergeConfig(loadResult.config.main, deepClone(config)) if (outDir) { resetOutDir(mainViteConfig, outDir, 'main') } mergePlugins(mainViteConfig, [...electronMainVitePlugin({ root }), workerPlugin()]) loadResult.config.main = mainViteConfig loadResult.config.main.configFile = false } if (loadResult.config.preload) { const preloadViteConfig: ViteConfig = mergeConfig(loadResult.config.preload, deepClone(config)) if (outDir) { resetOutDir(preloadViteConfig, outDir, 'preload') } mergePlugins(preloadViteConfig, electronPreloadVitePlugin({ root })) loadResult.config.preload = preloadViteConfig loadResult.config.preload.configFile = false } if (loadResult.config.renderer) { const rendererViteConfig: ViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config)) if (outDir) { resetOutDir(rendererViteConfig, outDir, 'renderer') } mergePlugins(rendererViteConfig, electronRendererVitePlugin({ root })) loadResult.config.renderer = rendererViteConfig loadResult.config.renderer.configFile = false } userConfig = loadResult.config configFile = loadResult.path configFileDependencies = loadResult.dependencies } } const resolved: ResolvedConfig = { config: userConfig, configFile: configFile ? normalizePath(configFile) : undefined, configFileDependencies } return resolved } function deepClone(data: T): T { return JSON.parse(JSON.stringify(data)) } function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void { let userOutDir = config.build?.outDir if (outDir === userOutDir) { userOutDir = path.resolve(config.root || process.cwd(), outDir, subOutDir) if (config.build) { config.build.outDir = userOutDir } else { config.build = { outDir: userOutDir } } } } function mergePlugins(config: ViteConfig, plugins: Plugin[]): void { const userPlugins = config.plugins || [] config.plugins = userPlugins.concat(plugins) } const CONFIG_FILE_NAME = 'electron.vite.config' export async function loadConfigFromFile( configEnv: ConfigEnv, configFile?: string, configRoot: string = process.cwd(), logLevel?: LogLevel, ignoreConfigWarning = false ): Promise<{ path: string config: UserConfig dependencies: string[] }> { if (configFile && /^vite.config.(js|ts|mjs|cjs|mts|cts)$/.test(configFile)) { throw new Error(`config file cannot be named ${configFile}.`) } const resolvedPath = configFile ? path.resolve(configFile) : findConfigFile(configRoot, ['js', 'ts', 'mjs', 'cjs', 'mts', 'cts']) if (!resolvedPath) { return { path: '', config: { main: {}, preload: {}, renderer: {} }, dependencies: [] } } // electron does not support adding type: "module" to package.json let isESM = false if (/\.m[jt]s$/.test(resolvedPath) || resolvedPath.endsWith('.ts')) { isESM = true } try { const bundled = await bundleConfigFile(resolvedPath, isESM) const userConfig = await loadConfigFormBundledFile(configRoot, resolvedPath, bundled.code, isESM) const config = await (typeof userConfig === 'function' ? userConfig(configEnv) : userConfig) if (!isObject(config)) { throw new Error(`config must export or return an object`) } const configRequired: string[] = [] let mainConfig if (config.main) { const mainViteConfig = config.main mainConfig = await (typeof mainViteConfig === 'function' ? mainViteConfig(configEnv) : mainViteConfig) if (!isObject(mainConfig)) { throw new Error(`main config must export or return an object`) } } else { configRequired.push('main') } let rendererConfig if (config.renderer) { const rendererViteConfig = config.renderer rendererConfig = await (typeof rendererViteConfig === 'function' ? rendererViteConfig(configEnv) : rendererViteConfig) if (!isObject(rendererConfig)) { throw new Error(`renderer config must export or return an object`) } } else { configRequired.push('renderer') } let preloadConfig if (config.preload) { const preloadViteConfig = config.preload preloadConfig = await (typeof preloadViteConfig === 'function' ? preloadViteConfig(configEnv) : preloadViteConfig) if (!isObject(preloadConfig)) { throw new Error(`preload config must export or return an object`) } } else { configRequired.push('preload') } if (!ignoreConfigWarning && configRequired.length > 0) { createLogger(logLevel).warn(colors.yellow(`${configRequired.join(' and ')} config is missing`)) } return { path: normalizePath(resolvedPath), config: { main: mainConfig, renderer: rendererConfig, preload: preloadConfig }, dependencies: bundled.dependencies } } catch (e) { createLogger(logLevel).error(colors.red(`failed to load config from ${resolvedPath}`), { error: e as Error }) throw e } } function findConfigFile(configRoot: string, extensions: string[]): string { for (const ext of extensions) { const configFile = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${ext}`) if (fs.existsSync(configFile)) { return configFile } } return '' } async function bundleConfigFile(fileName: string, isESM: boolean): Promise<{ code: string; dependencies: string[] }> { const dirnameVarName = '__electron_vite_injected_dirname' const filenameVarName = '__electron_vite_injected_filename' const importMetaUrlVarName = '__electron_vite_injected_import_meta_url' const result = await build({ absWorkingDir: process.cwd(), entryPoints: [fileName], write: false, target: ['node14.18', 'node16'], platform: 'node', bundle: true, format: isESM ? 'esm' : 'cjs', sourcemap: false, metafile: true, define: { __dirname: dirnameVarName, __filename: filenameVarName, 'import.meta.url': importMetaUrlVarName }, plugins: [ { name: 'externalize-deps', setup(build): void { build.onResolve({ filter: /.*/ }, args => { const id = args.path if (id[0] !== '.' && !path.isAbsolute(id)) { return { external: true } } return null }) } }, { name: 'replace-import-meta', setup(build): void { build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async args => { const contents = await fs.promises.readFile(args.path, 'utf8') const injectValues = `const ${dirnameVarName} = ${JSON.stringify(path.dirname(args.path))};` + `const ${filenameVarName} = ${JSON.stringify(args.path)};` + `const ${importMetaUrlVarName} = ${JSON.stringify(pathToFileURL(args.path).href)};` return { loader: args.path.endsWith('ts') ? 'ts' : 'js', contents: injectValues + contents } }) } } ] }) const { text } = result.outputFiles[0] return { code: text, dependencies: result.metafile ? Object.keys(result.metafile.inputs) : [] } } interface NodeModuleWithCompile extends NodeModule { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ _compile(code: string, filename: string): any } const _require = createRequire(import.meta.url) async function loadConfigFormBundledFile( configRoot: string, configFile: string, bundledCode: string, isESM: boolean ): Promise { if (isESM) { const fileNameTmp = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${Date.now()}.mjs`) fs.writeFileSync(fileNameTmp, bundledCode) const fileUrl = pathToFileURL(fileNameTmp) try { return (await dynamicImport(fileUrl)).default } finally { try { fs.unlinkSync(fileNameTmp) } catch {} } } else { const extension = path.extname(configFile) const realFileName = fs.realpathSync(configFile) const loaderExt = extension in _require.extensions ? extension : '.js' const defaultLoader = _require.extensions[loaderExt]! _require.extensions[loaderExt] = (module: NodeModule, filename: string): void => { if (filename === realFileName) { ;(module as NodeModuleWithCompile)._compile(bundledCode, filename) } else { defaultLoader(module, filename) } } delete _require.cache[_require.resolve(configFile)] const raw = _require(configFile) _require.extensions[loaderExt] = defaultLoader return raw.__esModule ? raw.default : raw } }