feat: add isolatedEntries option for preload and renderer to build entries as standalone bundles #154

This commit is contained in:
alex8088 2025-10-27 23:40:01 +08:00
parent c3939ade45
commit 0a79da03db
2 changed files with 220 additions and 8 deletions

View File

@ -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<ViteConfig, 'base'> & {
@ -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

View File

@ -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<string, string>
let transformedCount = 0
const assetCache = new Set<string>()
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<string | void> {
if (id === VIRTUAL_ENTRY_ID) {
const _entries = Array.isArray(entries)
? entries
: Object.entries(entries).map(([key, value]) => ({ [key]: value }))
const watchFiles = new Set<string>()
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<string, string>,
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)
}