From 0a79da03dbe08b5d62ce4d5be945bd1ca1087021 Mon Sep 17 00:00:00 2001 From: alex8088 <244096523@qq.com> Date: Mon, 27 Oct 2025 23:40:01 +0800 Subject: [PATCH] feat: add `isolatedEntries` option for `preload` and `renderer` to build entries as standalone bundles #154 --- src/config.ts | 80 ++++++++++++++++-- src/plugins/isolateEntries.ts | 148 ++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 src/plugins/isolateEntries.ts diff --git a/src/config.ts b/src/config.ts index 96e676c..86041cb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,10 +21,37 @@ import workerPlugin from './plugins/worker' import importMetaPlugin from './plugins/importMeta' import esmShimPlugin from './plugins/esmShim' import modulePathPlugin from './plugins/modulePath' +import isolateEntriesPlugin from './plugins/isolateEntries' import { isObject, isFilePathESM } from './utils' export { defineConfig as defineViteConfig } from 'vite' +interface ElectronVitePreloadConfig { + /** + * Build each entry point as an isolated bundle without code splitting. + * + * When enabled, each entry will include all its dependencies inline, + * preventing automatic code splitting across entries and ensuring each + * output file is fully standalone. + * + * @default false + */ + isolatedEntries?: boolean +} + +interface ElectronViteRendererConfig { + /** + * Build each entry point as an isolated bundle without code splitting. + * + * When enabled, each entry will include all its dependencies inline, + * preventing automatic code splitting across entries and ensuring each + * output file is fully standalone. + * + * @default false + */ + isolatedEntries?: boolean +} + export interface UserConfig { /** * Vite config options for electron main process @@ -37,13 +64,13 @@ export interface UserConfig { * * https://vitejs.dev/config/ */ - renderer?: ViteConfig & { configFile?: string | false } + renderer?: ViteConfig & { configFile?: string | false } & ElectronViteRendererConfig /** * Vite config options for electron preload files * * https://vitejs.dev/config/ */ - preload?: ViteConfig & { configFile?: string | false } + preload?: ViteConfig & { configFile?: string | false } & ElectronVitePreloadConfig } export interface ElectronViteConfig { @@ -58,13 +85,13 @@ export interface ElectronViteConfig { * * https://vitejs.dev/config/ */ - renderer?: ViteConfigExport + renderer?: ViteConfigExport & ElectronViteRendererConfig /** * Vite config options for electron preload files * * https://vitejs.dev/config/ */ - preload?: ViteConfigExport + preload?: ViteConfigExport & ElectronVitePreloadConfig } export type InlineConfig = Omit & { @@ -167,7 +194,10 @@ export async function resolveConfig( } if (loadResult.config.preload) { - const preloadViteConfig: ViteConfig = mergeConfig(loadResult.config.preload, deepClone(config)) + const preloadViteConfig: ViteConfig & ElectronVitePreloadConfig = mergeConfig( + loadResult.config.preload, + deepClone(config) + ) preloadViteConfig.mode = inlineConfig.mode || preloadViteConfig.mode || defaultMode @@ -178,7 +208,24 @@ export async function resolveConfig( ...electronPreloadVitePlugin({ root }), assetPlugin(), importMetaPlugin(), - esmShimPlugin() + esmShimPlugin(), + ...(preloadViteConfig.isolatedEntries + ? [ + isolateEntriesPlugin( + mergeConfig( + { + plugins: [ + electronPreloadVitePlugin({ root })[0], + assetPlugin(), + importMetaPlugin(), + esmShimPlugin() + ] + }, + preloadViteConfig + ) + ) + ] + : []) ]) loadResult.config.preload = preloadViteConfig @@ -186,7 +233,10 @@ export async function resolveConfig( } if (loadResult.config.renderer) { - const rendererViteConfig: ViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config)) + const rendererViteConfig: ViteConfig & ElectronViteRendererConfig = mergeConfig( + loadResult.config.renderer, + deepClone(config) + ) rendererViteConfig.mode = inlineConfig.mode || rendererViteConfig.mode || defaultMode @@ -194,7 +244,21 @@ export async function resolveConfig( resetOutDir(rendererViteConfig, outDir, 'renderer') } - mergePlugins(rendererViteConfig, electronRendererVitePlugin({ root })) + mergePlugins(rendererViteConfig, [ + ...electronRendererVitePlugin({ root }), + ...(rendererViteConfig.isolatedEntries + ? [ + isolateEntriesPlugin( + mergeConfig( + { + plugins: [electronRendererVitePlugin({ root })[0]] + }, + rendererViteConfig + ) + ) + ] + : []) + ]) loadResult.config.renderer = rendererViteConfig loadResult.config.renderer.configFile = false diff --git a/src/plugins/isolateEntries.ts b/src/plugins/isolateEntries.ts new file mode 100644 index 0000000..27d8fe8 --- /dev/null +++ b/src/plugins/isolateEntries.ts @@ -0,0 +1,148 @@ +import { type InlineConfig, type Plugin, type Logger, build as viteBuild, mergeConfig } from 'vite' +import type { InputOptions, RollupOutput } from 'rollup' +import colors from 'picocolors' + +const VIRTUAL_ENTRY_ID = '\0virtual:isolate-entries' + +export default function isolateEntriesPlugin(userConfig: InlineConfig): Plugin { + let logger: Logger + + let entries: string[] | Record + let transformedCount = 0 + + const assetCache = new Set() + + return { + name: 'vite:isolate-entries', + apply: 'build', + configResolved(config): void { + logger = config.logger + }, + options(opts): InputOptions | void { + const { input } = opts + if (input && typeof input === 'object') { + if ((Array.isArray(input) && input.length > 0) || Object.keys(input).length > 1) { + opts.input = VIRTUAL_ENTRY_ID + entries = input + return opts + } + } + }, + buildStart(): void { + transformedCount = 0 + assetCache.clear() + }, + resolveId(id): string | null { + if (id === VIRTUAL_ENTRY_ID) { + return id + } + return null + }, + async load(id): Promise { + if (id === VIRTUAL_ENTRY_ID) { + const _entries = Array.isArray(entries) + ? entries + : Object.entries(entries).map(([key, value]) => ({ [key]: value })) + const watchFiles = new Set() + for (const entry of _entries) { + const re = await bundleEntryFile(entry, userConfig, this.meta.watchMode) + const outputChunks = re.bundles.output + 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) { + watchFiles.add(id) + } + transformedCount += re.transformedCount + } + for (const id of watchFiles) { + this.addWatchFile(id) + } + return ` + // This is the virtual entry file + console.log(1)` + } + }, + renderStart(): void { + clearLine() + logger.info(`${colors.green(`✓`)} ${transformedCount} modules transformed.`) + }, + generateBundle(_, bundle): void { + for (const chunkName in bundle) { + if (chunkName.includes('virtual_isolate-entries')) { + delete bundle[chunkName] + } + } + } + } +} + +async function bundleEntryFile( + input: string | Record, + config: InlineConfig, + watch: boolean +): Promise<{ bundles: RollupOutput; watchFiles: string[]; transformedCount: number }> { + const moduleIds: string[] = [] + let transformedCount = 0 + + const viteConfig = mergeConfig(config, { + build: { + write: false, + watch: false + }, + plugins: [ + { + name: 'vite:transform-counter', + transform(): void { + transformedCount++ + } + } as Plugin, + ...(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 + }) as InlineConfig + + // rewrite the input instead of merging + viteConfig.build!.rollupOptions!.input = input + + const bundles = await viteBuild(viteConfig) + + return { + bundles: bundles as RollupOutput, + watchFiles: moduleIds, + transformedCount + } +} + +function clearLine(): void { + process.stdout.moveCursor(0, -1) + process.stdout.clearLine(0) + process.stdout.cursorTo(0) +}