Compare commits

...

6 Commits

Author SHA1 Message Date
alex8088
3e9ded666c release: v5.0.0-beta.2 2025-10-29 23:42:04 +08:00
alex8088
1bba6766e8 perf(isolateEntries): optimize entries transformation 2025-10-29 23:35:38 +08:00
alex8088
4edffe3b9a perf(isolateEntries): transform log 2025-10-29 23:25:28 +08:00
alex8088
cfd9812a91 feat: reporter plugin for isolated builds 2025-10-29 22:32:35 +08:00
alex8088
7c7f31b2a3 fix: avoid duplicate chunk emission 2025-10-29 21:22:12 +08:00
alex8088
ae57b2489a fix(asset): normalize imported public asset chunk path 2025-10-29 21:12:13 +08:00
6 changed files with 140 additions and 74 deletions

View File

@ -1,3 +1,11 @@
### v5.0.0-beta.2 (_2025-10-30_)
- feat: reporter plugin for isolated builds
- perf(isolateEntries): transform log
- perf(isolateEntries): optimize entries transformation
- fix(asset): normalize imported public asset chunk path
- fix: avoid duplicate chunk emission
### v5.0.0-beta.1 (_2025-10-29_)
- feat: enhanced string protection

View File

@ -1,6 +1,6 @@
{
"name": "electron-vite",
"version": "5.0.0-beta.1",
"version": "5.0.0-beta.2",
"description": "Electron build tooling based on Vite",
"type": "module",
"main": "./dist/index.js",

View File

@ -1,7 +1,7 @@
import path from 'node:path'
import fs from 'node:fs/promises'
import type { SourceMapInput } from 'rollup'
import { type Plugin } from 'vite'
import { type Plugin, normalizePath } from 'vite'
import MagicString from 'magic-string'
import { cleanUrl, getHash, toRelativePath } from '../utils'
import { supportImportMetaPaths } from '../electron'
@ -28,7 +28,6 @@ export default async function loadWasm(file, importObject = {}) {
export default function assetPlugin(): Plugin {
let publicDir = ''
let outDir = ''
const publicAssetPathCache = new Map<string, string>()
const assetCache = new Map<string, string>()
const isImportMetaPathSupported = supportImportMetaPaths()
@ -42,7 +41,6 @@ export default function assetPlugin(): Plugin {
},
configResolved(config): void {
publicDir = config.publicDir
outDir = config.build.outDir
},
resolveId(id): string | void {
if (id === wasmHelperId) {
@ -105,7 +103,7 @@ export default function assetPlugin(): Plugin {
export default importObject => loadWasm(${referenceId}, importObject)`
}
},
renderChunk(code, chunk, { sourcemap }): { code: string; map: SourceMapInput } | null {
renderChunk(code, chunk, { sourcemap, dir }): { code: string; map: SourceMapInput } | null {
let match: RegExpExecArray | null
let s: MagicString | undefined
@ -126,7 +124,7 @@ export default function assetPlugin(): Plugin {
s ||= new MagicString(code)
const [full, hash] = match
const filename = publicAssetPathCache.get(hash)!
const outputFilepath = toRelativePath(filename, path.join(outDir, chunk.fileName))
const outputFilepath = toRelativePath(filename, normalizePath(path.join(dir!, chunk.fileName)))
const replacement = JSON.stringify(outputFilepath)
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true

View File

@ -0,0 +1,27 @@
import { type Plugin } from 'vite'
type BuildReporterApi = {
getWatchFiles: () => string[]
}
export default function buildReporterPlugin(): Plugin<BuildReporterApi> {
const moduleIds: string[] = []
return {
name: 'vite:build-reporter',
buildEnd() {
const allModuleIds = Array.from(this.getModuleIds())
const sourceFiles = allModuleIds.filter(id => {
const info = this.getModuleInfo(id)
return info && !info.isExternal
})
moduleIds.push(...sourceFiles)
},
api: {
getWatchFiles() {
return moduleIds
}
}
}
}

View File

@ -1,13 +1,24 @@
import { type InlineConfig, type Plugin, type Logger, build as viteBuild, mergeConfig } from 'vite'
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */
import path from 'node:path'
import { type InlineConfig, type Plugin, type Logger, type LogLevel, build as viteBuild, mergeConfig } from 'vite'
import type { InputOptions, RollupOutput } from 'rollup'
import colors from 'picocolors'
import buildReporterPlugin from './buildReporter'
const VIRTUAL_ENTRY_ID = '\0virtual:isolate-entries'
const LogLevels: Record<LogLevel, number> = {
silent: 0,
error: 1,
warn: 2,
info: 3
}
export default function isolateEntriesPlugin(userConfig: InlineConfig): Plugin {
let logger: Logger
let entries: string[] | Record<string, string>
let entries: string[] | { [x: string]: string }[]
let transformedCount = 0
const assetCache = new Set<string>()
@ -15,40 +26,47 @@ export default function isolateEntriesPlugin(userConfig: InlineConfig): Plugin {
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
entries = Array.isArray(input) ? input : Object.entries(input).map(([key, value]) => ({ [key]: value }))
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 shouldLog = LogLevels[userConfig.logLevel || 'info'] >= LogLevels.info
const shouldWatch = this.meta.watchMode
const watchFiles = new Set<string>()
for (const entry of _entries) {
const re = await bundleEntryFile(entry, userConfig, this.meta.watchMode)
for (const entry of entries) {
const re = await bundleEntryFile(entry, userConfig, shouldWatch, shouldLog, transformedCount)
const outputChunks = re.bundles.output
for (const chunk of outputChunks) {
if (chunk.type === 'asset' && assetCache.has(chunk.fileName)) {
if (assetCache.has(chunk.fileName)) {
continue
}
this.emitFile({
@ -56,27 +74,31 @@ export default function isolateEntriesPlugin(userConfig: InlineConfig): Plugin {
fileName: chunk.fileName,
source: chunk.type === 'chunk' ? chunk.code : chunk.source
})
if (chunk.type === 'asset') {
assetCache.add(chunk.fileName)
}
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()
clearLine(-1)
logger.info(`${colors.green(``)} ${transformedCount} modules transformed.`)
},
generateBundle(_, bundle): void {
for (const chunkName in bundle) {
if (chunkName.includes('virtual_isolate-entries')) {
@ -90,41 +112,19 @@ export default function isolateEntriesPlugin(userConfig: InlineConfig): Plugin {
async function bundleEntryFile(
input: string | Record<string, string>,
config: InlineConfig,
watch: boolean
watch: boolean,
shouldLog: boolean,
preTransformedCount: number
): Promise<{ bundles: RollupOutput; watchFiles: string[]; transformedCount: number }> {
const moduleIds: string[] = []
let transformedCount = 0
const transformReporter = transformReporterPlugin(preTransformedCount, shouldLog)
const buildReporter = watch ? buildReporterPlugin() : undefined
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
]
: [])
],
plugins: [transformReporter, buildReporter],
logLevel: 'warn',
configFile: false
}) as InlineConfig
@ -136,13 +136,63 @@ async function bundleEntryFile(
return {
bundles: bundles as RollupOutput,
watchFiles: moduleIds,
transformedCount
watchFiles: buildReporter?.api?.getWatchFiles() || [],
transformedCount: transformReporter?.api?.getTransformedCount() || 0
}
}
function clearLine(): void {
process.stdout.moveCursor(0, -1)
function transformReporterPlugin(
preTransformedCount = 0,
shouldLog = true
): Plugin<{ getTransformedCount: () => number }> {
let transformedCount = 0
let root
const log = throttle(id => {
writeLine(`transforming (${preTransformedCount + transformedCount}) ${colors.dim(path.relative(root, id))}`)
})
return {
name: 'vite:transform-reporter',
configResolved(config) {
root = config.root
},
transform(_, id) {
transformedCount++
if (!shouldLog) return
if (id.includes('?')) return
log(id)
},
api: {
getTransformedCount() {
return transformedCount
}
}
}
}
function writeLine(output: string): void {
clearLine()
if (output.length < process.stdout.columns) {
process.stdout.write(output)
} else {
process.stdout.write(output.substring(0, process.stdout.columns - 1))
}
}
function clearLine(move: number = 0): void {
if (move < 0) {
process.stdout.moveCursor(0, move)
}
process.stdout.clearLine(0)
process.stdout.cursorTo(0)
}
function throttle(fn: Function) {
let timerHandle: NodeJS.Timeout | null = null
return (...args: any[]) => {
if (timerHandle) return
fn(...args)
timerHandle = setTimeout(() => {
timerHandle = null
}, 100)
}
}

View File

@ -2,6 +2,7 @@ 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 buildReporterPlugin from './buildReporter'
import { cleanUrl, toRelativePath } from '../utils'
import { supportImportMetaPaths } from '../electron'
@ -31,7 +32,7 @@ export default function modulePathPlugin(config: InlineConfig): Plugin {
source: outputChunk.code
})
for (const chunk of outputChunks) {
if (chunk.type === 'asset' && assetCache.has(chunk.fileName)) {
if (assetCache.has(chunk.fileName)) {
continue
}
this.emitFile({
@ -39,9 +40,7 @@ export default function modulePathPlugin(config: InlineConfig): Plugin {
fileName: chunk.fileName,
source: chunk.type === 'chunk' ? chunk.code : chunk.source
})
if (chunk.type === 'asset') {
assetCache.add(chunk.fileName)
}
assetCache.add(chunk.fileName)
}
for (const id of re.watchFiles) {
this.addWatchFile(id)
@ -86,7 +85,7 @@ async function bundleEntryFile(
config: InlineConfig,
watch: boolean
): Promise<{ bundles: RollupOutput; watchFiles: string[] }> {
const moduleIds: string[] = []
const reporter = watch ? buildReporterPlugin() : undefined
const viteConfig = mergeConfig(config, {
build: {
rollupOptions: { input },
@ -103,23 +102,7 @@ async function bundleEntryFile(
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
]
: [])
reporter
],
logLevel: 'warn',
configFile: false
@ -128,6 +111,6 @@ async function bundleEntryFile(
return {
bundles: bundles as RollupOutput,
watchFiles: moduleIds
watchFiles: reporter?.api?.getWatchFiles() || []
}
}