Compare commits

...

78 Commits

Author SHA1 Message Date
alex8088
91368b6655 release: v5.0.0 2025-12-07 22:26:56 +08:00
alex8088
b1fd596afe docs: update 2025-11-09 23:31:08 +08:00
alex8088
438e9e7672 chore: fix jsdoc 2025-11-09 23:03:06 +08:00
alex8088
9eba4df577 release: v5.0.0-beta.3 2025-11-01 17:11:12 +08:00
alex8088
465690ab0d refactor(config)!: remove function resolution for nested config fields 2025-10-31 23:28:40 +08:00
alex8088
6aae37833e perf(buildReport): exclude node_modules from watch list 2025-10-31 21:59:15 +08:00
alex8088
9152dfc943 fix(modulePath): rewrite the build input instead of merging 2025-10-31 01:16:10 +08:00
alex8088
0276407b5b refactor: deprecated externalizeDepsPlugin and bytecodePlugin 2025-10-31 01:16:02 +08:00
alex8088
fe7e631f47 refactor(config): move the isolateEntries options to the build option 2025-10-31 01:09:44 +08:00
alex8088
08be346407 feat(config): add build.externalizeDeps and build.bytecode config options to replace externalizeDepsPlugin and bytecodePlugin 2025-10-31 01:03:34 +08:00
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
alex8088
397b02e384 release: v5.0.0-beta.1 2025-10-29 00:19:49 +08:00
alex8088
a4f7693712 perf: build compatibility target for Electron 39 2025-10-29 00:08:03 +08:00
alex8088
56fb519092 refactor: replace JSON.parse/stringify with manual deep clone 2025-10-28 23:18:45 +08:00
alex8088
eb0a7e3ffe refactor(build): simplify build logic 2025-10-28 22:53:03 +08:00
alex8088
de70dfe1dc refactor(config)!: restructure Electron Vite config interfaces 2025-10-28 22:36:25 +08:00
alex8088
8892bf3679 fix(modulePath): support watch mode 2025-10-28 21:50:54 +08:00
alex8088
2576484604 chore: improve logging message clarity and consistency 2025-10-28 21:50:47 +08:00
alex8088
88f6db2239 refactor: split electron plugin into preset and validator plugins 2025-10-28 21:50:12 +08:00
alex8088
0a79da03db feat: add isolatedEntries option for preload and renderer to build entries as standalone bundles #154 2025-10-27 23:40:01 +08:00
alex8088
c3939ade45 refactor(asset): remove redundant path normalization 2025-10-24 23:57:12 +08:00
alex8088
38228f9b3f fix(modulePath): prevent duplicate asset emission 2025-10-24 22:10:03 +08:00
alex8088
8b193864fd feat: enhanced string protection 2025-10-20 23:35:21 +08:00
alex8088
f264d41d7f release: v5.0.0-beta.0 2025-10-19 20:09:04 +08:00
alex8088
5debb6f83b refactor: remove Electron 18, 19, 20, 21 build compatilibity target 2025-10-19 16:00:01 +08:00
alex8088
d530597339 chore: rename the file esm to esmShim 2025-10-19 15:53:19 +08:00
alex8088
a9b5326544 perf(plugin): enhance path resolution using import.meta.dirname for ES modules 2025-10-19 15:47:35 +08:00
alex8088
7587d2c674 perf(bytecodePlugin): better way to count bytecode chunks 2025-10-19 14:49:09 +08:00
alex8088
0fb8918090 perf(plugin): lazily initialize MagicString and remove the redundant pre-check 2025-10-19 14:42:35 +08:00
alex8088
c7955aa6fd perf(plugin): no need to cache sourcemap option 2025-10-19 12:11:29 +08:00
alex8088
70e027d38a perf(plugin): more efficient module filtering via regular expressions 2025-10-19 11:57:23 +08:00
alex8088
28bb22b353 refactor(modulePath): better support for tree-shaking and code-splitting 2025-10-18 17:10:04 +08:00
alex8088
327adc23df refactor(bytecodePlugin): improved bytecode bundle generation and made a new string protection plugin 2025-10-18 16:33:49 +08:00
alex8088
4a6aea3704 chore: remove redundant external id 2025-10-17 21:53:35 +08:00
alex8088
3f7f65bf57 chore: replace tseslint.config with defineConfig 2025-10-17 21:29:48 +08:00
alex8088
0badfc493f chore: update eslint config 2025-10-17 21:28:47 +08:00
alex8088
cbb039c3e9 chore(deps): update all non-major dependencies 2025-10-17 21:07:21 +08:00
alex8088
ad50cba495 release: 4.0.1 2025-09-21 17:03:52 +08:00
alex8088
b55eb725c4 perf: build compatibility target for Electron 38 2025-09-21 16:52:57 +08:00
alex8088
1eeb15ea77 release: v4.0.0 2025-07-06 23:12:09 +08:00
alex8088
52abc48abf release: v4.0.0-beta.0 2025-06-28 13:29:56 +08:00
alex8088
cbee52c8bb perf: build compatibility target for Electron 37 2025-06-28 13:12:31 +08:00
alex8088
48e6f4f570 fix(deps)!: update Vite to v7 and remove cjs build 2025-06-28 13:08:38 +08:00
alex8088
4071778f07 fix: use import type for type-only imports 2025-06-28 00:01:48 +08:00
alex8088
3fd16d0c23 refactor!: bump required node version to 20.19+, 22.12+ 2025-06-27 23:10:23 +08:00
alex8088
d2e8b1271b chore: allow breaking commit message 2025-06-27 23:07:33 +08:00
alex8088
28d7df6e91 chore(deps): update lint-staged to v16 2025-06-27 22:02:26 +08:00
alex8088
b7763a7f77 chore(deps): update all non-major dependencies 2025-06-27 21:59:22 +08:00
alex8088
1c084cc090 chore(deps): update pnpm to v10 2025-06-27 21:50:18 +08:00
alex8088
9ec164d33e chore: add funding 2025-05-10 17:58:35 +08:00
alex8088
a9197f5cc9 chore: remove deprecated rules and adjust rules 2025-05-07 22:03:59 +08:00
jonz94
3d5c9f68a1
perf: build compatibility target for Electron 36 (#766) 2025-05-06 10:12:44 +08:00
alex8088
1b411d3633 release: v3.1.0 2025-03-25 21:30:53 +08:00
alex8088
b56d3c2d21 fix(bytecodePlugin): optimize 'use strict' directive replacement (#681) 2025-03-16 19:24:22 +08:00
alex8088
f2eff25268 release: v3.1.0-beta.0 2025-03-12 22:05:32 +08:00
alex8088
2d8e513e07 chore(deps): update esbuild to v0.25 2025-03-12 21:52:14 +08:00
alex8088
d8063320dc chore(deps): update globals to v16 2025-03-12 21:43:32 +08:00
alex8088
f33c5b2abe chore(deps): update all non-major dependencies 2025-03-12 21:40:35 +08:00
alex.wei
e91e70c105
Merge pull request #729 from jonz94/electron-35
perf: build compatibility target for Electron 35
2025-03-11 10:10:09 +08:00
jonz94
ea144aef19
perf: build compatibility target for Electron 35 2025-03-09 04:16:52 +08:00
alex8088
987c55ee8b release: v3.0.0 2025-02-16 20:37:18 +08:00
alex8088
8064bd81ff release: v3.0.0-beta.0 2025-01-22 23:05:02 +08:00
alex8088
6e8572d9b7 feat: resolve conditions for preload 2025-01-22 22:24:04 +08:00
alex8088
4b47ef0bd4 perf: build compatilibity target for Electron 34 2025-01-22 21:16:13 +08:00
alex8088
5a5af050b2 chore(deps): update @type/node to v22 2025-01-22 21:13:05 +08:00
alex8088
96ae3c5cd9 chore(deps): update vite to v6 2025-01-22 01:24:58 +08:00
alex8088
79ac91dee2 chore(deps): update esbuild to v0.24 2025-01-22 01:15:46 +08:00
alex8088
1599d730f6 chore(deps): update @rollup/plugin-typescript to v12 2025-01-22 01:12:57 +08:00
alex8088
3c6e08b2f2 chore(deps): update @rollup/plugin-node-resolve to v16 2025-01-22 01:10:54 +08:00
alex8088
dfe6a3e3f8 chore(deps): update all non-major dependencies 2025-01-22 01:04:31 +08:00
alex8088
6c01417909 chore: move to eslint flat config 2025-01-22 00:47:48 +08:00
kye shimizu
5ffd49eddc
perf: build compatilibity target for Electron 33 (#651) 2024-11-11 23:49:02 +08:00
alex8088
bf1220875f perf: build compatilibity target for Electron 32 2024-08-21 21:49:44 +08:00
31 changed files with 3423 additions and 2899 deletions

View File

@ -1,2 +0,0 @@
node_modules
dist

View File

@ -1,39 +0,0 @@
module.exports = {
root: true,
env: {
commonjs: true,
es6: true,
node: true
},
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaVersion: 2022
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:prettier/recommended'
],
rules: {
'prettier/prettier': 'warn',
'no-empty': ['warn', { allowEmptyCatch: true }],
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 'off'
},
overrides: [
{
files: ['*.js'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off'
}
}
]
}

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: alex8088

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
node_modules node_modules
dist dist
.DS_Store .DS_Store
.eslintcache
*.log* *.log*

View File

@ -1,3 +1,108 @@
### v5.0.0 (_2025-12-07_)
- feat(config): add `build.externalizeDeps` and `build.bytecode` config options to replace `externalizeDepsPlugin` and `bytecodePlugin`
- feat: reporter plugin for isolated builds
- feat: enhanced string protection
- feat: add `isolatedEntries` option for `preload` and `renderer` to build entries as standalone bundles [#154](https://github.com/alex8088/electron-vite/issues/154)
- refactor(config): move the `isolateEntries` options to the `build` option
- refactor: deprecated `externalizeDepsPlugin` and `bytecodePlugin`
- refactor(config)!: remove function resolution for nested config fields
- refactor(asset): remove redundant path normalization
- refactor: split electron plugin into preset and validator plugins
- refactor(config)!: restructure Electron Vite config interfaces
- refactor(build): simplify build logic
- refactor: replace `JSON.parse/stringify` with manual deep clone
- refactor(bytecodePlugin): improved bytecode bundle generation and made a new string protection plugin
- refactor(modulePath): better support for tree-shaking and code-splitting
- refactor: remove Electron 18, 19, 20, 21 build compatilibity target
- perf(buildReport): exclude node_modules from watch list
- perf(isolateEntries): transform log
- perf(isolateEntries): optimize entries transformation
- perf: build compatibility target for Electron 39
- perf(plugin): more efficient module filtering via regular expressions
- perf(plugin): no need to cache `sourcemap` option
- perf(plugin): lazily initialize `MagicString` and remove the redundant pre-check
- perf(bytecodePlugin): better way to count bytecode chunks
- perf(plugin): enhance path resolution using `import.meta.dirname` for ES modules
- fix(modulePath): rewrite the build input instead of merging
- fix(asset): normalize imported public asset chunk path
- fix: avoid duplicate chunk emission
- fix(modulePath): prevent duplicate asset emission
- fix(modulePath): support watch mode
- chore: fix jsdoc
- chore: improve logging message clarity and consistency
- chore(deps): update all non-major dependencies
- chore: update eslint config
- chore: replace `tseslint.config` with `defineConfig`
- chore: remove redundant external id
- chore: rename the file `esm` to `esmShim`
- docs: update
### v5.0.0-beta.3 (_2025-11-01_)
See [v5.0.0-beta.3 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.3/CHANGELOG.md)
### v5.0.0-beta.2 (_2025-10-30_)
See [v5.0.0-beta.2 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.2/CHANGELOG.md)
### v5.0.0-beta.1 (_2025-10-29_)
See [v5.0.0-beta.1 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.1/CHANGELOG.md)
### v5.0.0-beta.0 (_2025-10-19_)
See [v5.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.0/CHANGELOG.md)
### v4.0.1 (_2025-09-21_)
- perf: build compatibility target for Electron 38
### v4.0.0 (_2025-07-06_)
- refactor!: bump required node version to 20.19+, 22.12+
- fix(deps)!: update Vite to v7 and remove cjs build
- fix: use `import type` for type-only imports
- perf: build compatibility target for Electron 36 ([#766](https://github.com/alex8088/electron-vite/pull/766))
- perf: build compatibility target for Electron 37
- chore(deps): update pnpm to v10
- chore(deps): update all non-major dependencies
- chore(deps): update lint-staged to v16
### v4.0.0-beta.0 (_2025-06-28_)
See [v4.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v4.0.0-beta.0/CHANGELOG.md)
### v3.1.0 (_2025-03-25_)
- fix(bytecodePlugin): optimize 'use strict' directive replacement ([#681](https://github.com/alex8088/electron-vite/issues/681))
- perf: build compatilibity target for Electron 35 ([#729](https://github.com/alex8088/electron-vite/pull/729))
- chore(deps): update all non-major dependencies
- chore(deps): update globals to v16
- chore(deps): update esbuild to v0.25
### v3.1.0-beta.0 (_2025-03-12_)
See [v3.1.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v3.1.0-beta.0/CHANGELOG.md)
### v3.0.0 (_2025-02-16_)
- feat: resolve conditions for preload
- perf: build compatilibity target for Electron 32
- perf: build compatilibity target for Electron 33 ([#651](https://github.com/alex8088/electron-vite/pull/651))
- perf: build compatilibity target for Electron 34
- chore: move to eslint flat config
- chore(deps): update all non-major dependencies
- chore(deps): update @rollup/plugin-node-resolve to v16
- chore(deps): update @rollup/plugin-typescript to v12
- chore(deps): update esbuild to v0.24
- chore(deps): update vite to v6
- chore(deps): update @type/node to v22
### v3.0.0-beta.0 (_2025-01-22_)
See [v3.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v3.0.0-beta.0/CHANGELOG.md)
### v2.3.0 (_2024-06-23_) ### v2.3.0 (_2024-06-23_)
- feat: resolve import.meta.\[dirname|filename\] to support CommonJS format - feat: resolve import.meta.\[dirname|filename\] to support CommonJS format
@ -7,7 +112,7 @@
- perf: build compatilibity target for Electron 31 - perf: build compatilibity target for Electron 31
- perf: improve cjs shim - perf: improve cjs shim
- chore(deps): update all non-major dependencies - chore(deps): update all non-major dependencies
- chore(deps): update @typescript-eslint/* to v7 - chore(deps): update @typescript-eslint/\* to v7
- chore(deps): update esbuild to v0.21 - chore(deps): update esbuild to v0.21
### v2.2.0 (_2024-04-21_) ### v2.2.0 (_2024-04-21_)

View File

@ -28,13 +28,13 @@
## Features ## Features
- ⚡️ [Vite](https://vitejs.dev) powered and use the same way. - ⚡️ [Vite](https://vitejs.dev) powered and use the same way.
- 🛠 Pre-configured for Electron, don't worry about configuration. - 🛠 Pre-configure with sensible defaults optimized for Electron.
- 💡 Optimize asset handling (Node.js addons, WebAssembly, Worker Thread, etc). - 💡 Optimize asset handling for Electron main process.
- 🚀 Fast HMR for renderer processes. - 🚀 Fast HMR & hot reloading.
- 🔥 Hot reloading for main process and preload scripts. - 🔥 Isolated build for multi-entry application development.
- 🔌 Easy to debug in IDEs like VSCode or WebStorm. - ✨ Simplify multi-threading development.
- 🔒 Compile to v8 bytecode to protect source code. - 🔒 Compile code to v8 bytecode to protect source code.
- 🏷️ Support for TypeScript decorators. - 🔌 Easy to debug in IDEs such as VSCode or WebStorm.
- 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more. - 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more.
## Usage ## Usage
@ -83,7 +83,7 @@ export default {
Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project. Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project.
```bash ```bash
npm create @quick-start/electron npm create @quick-start/electron@latest
``` ```
Currently supported template presets include: Currently supported template presets include:

View File

@ -24,7 +24,7 @@ if (debugIndex > 0) {
} }
function run() { function run() {
import('../dist/cli.mjs') import('../dist/cli.js')
} }
run() run()

69
eslint.config.js Normal file
View File

@ -0,0 +1,69 @@
// ts-check
import { defineConfig } from 'eslint/config'
import eslint from '@eslint/js'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import globals from 'globals'
import tseslint from 'typescript-eslint'
export default defineConfig(
{ ignores: ['**/node_modules', '**/dist', '**/bin'] },
eslint.configs.recommended,
tseslint.configs.recommended,
eslintPluginPrettierRecommended,
{
languageOptions: {
parser: tseslint.parser,
parserOptions: {
sourceType: 'module',
ecmaVersion: 2022
},
globals: {
...globals.es2021,
...globals.node
}
},
settings: {
node: {
version: '^20.19.0 || >=22.12.0'
}
},
rules: {
'prettier/prettier': 'warn',
'no-empty': ['warn', { allowEmptyCatch: true }],
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],
'@typescript-eslint/explicit-function-return-type': [
'error',
{
allowExpressions: true,
allowTypedFunctionExpressions: true,
allowHigherOrderFunctions: true,
allowIIFEs: true
}
],
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }],
'@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'always' }],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTaggedTemplates: true,
allowTernary: true
}
],
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', disallowTypeAnnotations: false }
]
}
},
{
files: ['*.js', '*.mjs'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off'
}
}
)

2
node.d.ts vendored
View File

@ -1,6 +1,6 @@
// node worker // node worker
declare module '*?nodeWorker' { declare module '*?nodeWorker' {
import { Worker, WorkerOptions } from 'node:worker_threads' import type { Worker, WorkerOptions } from 'node:worker_threads'
export default function (options: WorkerOptions): Worker export default function (options: WorkerOptions): Worker
} }

View File

@ -1,17 +1,12 @@
{ {
"name": "electron-vite", "name": "electron-vite",
"version": "2.3.0", "version": "5.0.0",
"description": "Electron build tooling based on Vite", "description": "Electron build tooling based on Vite",
"type": "module", "type": "module",
"main": "dist/index.cjs", "main": "./dist/index.js",
"module": "dist/index.mjs", "types": "./dist/index.d.ts",
"types": "dist/index.d.ts",
"exports": { "exports": {
".": { ".": "./dist/index.js",
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./node": { "./node": {
"types": "./node.d.ts" "types": "./node.d.ts"
}, },
@ -26,9 +21,9 @@
"node.d.ts" "node.d.ts"
], ],
"engines": { "engines": {
"node": "^18.0.0 || >=20.0.0" "node": "^20.19.0 || >=22.12.0"
}, },
"packageManager": "pnpm@8.6.10", "packageManager": "pnpm@10.12.4",
"author": "Alex Wei<https://github.com/alex8088>", "author": "Alex Wei<https://github.com/alex8088>",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
@ -47,7 +42,7 @@
], ],
"scripts": { "scripts": {
"format": "prettier --write .", "format": "prettier --write .",
"lint": "eslint --ext .ts src/**", "lint": "eslint --cache .",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"build": "pnpm run lint && rollup -c rollup.config.ts --configPlugin typescript" "build": "pnpm run lint && rollup -c rollup.config.ts --configPlugin typescript"
}, },
@ -66,7 +61,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@swc/core": "^1.0.0", "@swc/core": "^1.0.0",
"vite": "^4.0.0 || ^5.0.0" "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@swc/core": { "@swc/core": {
@ -74,32 +69,41 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.37.0",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^11.1.6", "@rollup/plugin-typescript": "^12.1.4",
"@swc/core": "^1.6.5", "@swc/core": "^1.13.5",
"@types/node": "^18.19.39", "@types/babel__core": "^7.20.5",
"@typescript-eslint/eslint-plugin": "^7.13.1", "@types/node": "^22.18.11",
"@typescript-eslint/parser": "^7.13.1", "eslint": "^9.37.0",
"eslint": "^8.57.0", "eslint-config-prettier": "^10.1.8",
"eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-prettier": "^5.1.3", "globals": "^16.4.0",
"lint-staged": "^15.2.7", "lint-staged": "^16.2.4",
"prettier": "^3.3.2", "prettier": "^3.6.2",
"rollup": "^4.18.0", "rollup": "^4.52.4",
"rollup-plugin-dts": "^6.1.1", "rollup-plugin-dts": "^6.2.3",
"rollup-plugin-rm": "^1.0.2", "rollup-plugin-rm": "^1.0.2",
"simple-git-hooks": "^2.11.1", "simple-git-hooks": "^2.13.1",
"tslib": "^2.6.3", "tslib": "^2.8.1",
"typescript": "^5.3.3", "typescript": "^5.9.3",
"vite": "^5.3.1" "typescript-eslint": "^8.46.1",
"vite": "^7.1.10"
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.24.7", "@babel/core": "^7.28.4",
"@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-arrow-functions": "^7.27.1",
"cac": "^6.7.14", "cac": "^6.7.14",
"esbuild": "^0.21.5", "esbuild": "^0.25.11",
"magic-string": "^0.30.10", "magic-string": "^0.30.19",
"picocolors": "^1.0.1" "picocolors": "^1.1.1"
},
"pnpm": {
"onlyBuiltDependencies": [
"@swc/core",
"esbuild",
"simple-git-hooks"
]
} }
} }

3949
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ import rm from 'rollup-plugin-rm'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
const pkg = require('./package.json') const pkg = require('./package.json')
const external = ['esbuild', ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})] const external = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]
export default defineConfig([ export default defineConfig([
{ {
@ -17,14 +17,8 @@ export default defineConfig([
output: [ output: [
{ {
dir: 'dist', dir: 'dist',
entryFileNames: '[name].cjs', entryFileNames: '[name].js',
chunkFileNames: 'chunks/lib-[hash].cjs', chunkFileNames: 'chunks/lib-[hash].js',
format: 'cjs'
},
{
dir: 'dist',
entryFileNames: '[name].mjs',
chunkFileNames: 'chunks/lib-[hash].mjs',
format: 'es' format: 'es'
} }
], ],

View File

@ -7,7 +7,7 @@ const msgPath = process.argv[2]
const msg = fs.readFileSync(msgPath, 'utf-8').trim() const msg = fs.readFileSync(msgPath, 'utf-8').trim()
const commitRE = const commitRE =
/^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?: .{1,50}/ /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?!?: .{1,50}/
if (!commitRE.test(msg)) { if (!commitRE.test(msg)) {
console.log() console.log()

View File

@ -1,5 +1,5 @@
import { build as viteBuild } from 'vite' import { build as viteBuild } from 'vite'
import { InlineConfig, resolveConfig } from './config' import { type InlineConfig, resolveConfig } from './config'
/** /**
* Bundles the electron app for production. * Bundles the electron app for production.
@ -7,27 +7,22 @@ import { InlineConfig, resolveConfig } from './config'
export async function build(inlineConfig: InlineConfig = {}): Promise<void> { export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
process.env.NODE_ENV_ELECTRON_VITE = 'production' process.env.NODE_ENV_ELECTRON_VITE = 'production'
const config = await resolveConfig(inlineConfig, 'build', 'production') const config = await resolveConfig(inlineConfig, 'build', 'production')
if (config.config) {
const mainViteConfig = config.config?.main if (!config.config) {
if (mainViteConfig) { return
if (mainViteConfig.build?.watch) { }
mainViteConfig.build.watch = null
// Build targets in order: main -> preload -> renderer
const buildTargets = ['main', 'preload', 'renderer'] as const
for (const target of buildTargets) {
const viteConfig = config.config[target]
if (viteConfig) {
// Disable watch mode in production builds
if (viteConfig.build?.watch) {
viteConfig.build.watch = null
} }
await viteBuild(mainViteConfig) await viteBuild(viteConfig)
}
const preloadViteConfig = config.config?.preload
if (preloadViteConfig) {
if (preloadViteConfig.build?.watch) {
preloadViteConfig.build.watch = null
}
await viteBuild(preloadViteConfig)
}
const rendererViteConfig = config.config?.renderer
if (rendererViteConfig) {
if (rendererViteConfig.build?.watch) {
rendererViteConfig.build.watch = null
}
await viteBuild(rendererViteConfig)
} }
} }
} }

View File

@ -1,7 +1,7 @@
import { cac } from 'cac' import { cac } from 'cac'
import colors from 'picocolors' import colors from 'picocolors'
import { LogLevel, createLogger } from 'vite' import { type LogLevel, createLogger } from 'vite'
import { InlineConfig } from './config' import type { InlineConfig } from './config'
import { version } from '../package.json' import { version } from '../package.json'
const cli = cac('electron-vite') const cli = cac('electron-vite')

View File

@ -5,9 +5,10 @@ import { createRequire } from 'node:module'
import colors from 'picocolors' import colors from 'picocolors'
import { import {
type UserConfig as ViteConfig, type UserConfig as ViteConfig,
type UserConfigExport as ViteConfigExport,
type ConfigEnv, type ConfigEnv,
type PluginOption,
type Plugin, type Plugin,
type BuildEnvironmentOptions as ViteBuildOptions,
type LogLevel, type LogLevel,
createLogger, createLogger,
mergeConfig, mergeConfig,
@ -15,89 +16,138 @@ import {
} from 'vite' } from 'vite'
import { build } from 'esbuild' import { build } from 'esbuild'
import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugins/electron' import {
electronMainConfigPresetPlugin,
electronMainConfigValidatorPlugin,
electronPreloadConfigPresetPlugin,
electronPreloadConfigValidatorPlugin,
electronRendererConfigPresetPlugin,
electronRendererConfigValidatorPlugin
} from './plugins/electron'
import assetPlugin from './plugins/asset' import assetPlugin from './plugins/asset'
import workerPlugin from './plugins/worker' import workerPlugin from './plugins/worker'
import importMetaPlugin from './plugins/importMeta' import importMetaPlugin from './plugins/importMeta'
import esmShimPlugin from './plugins/esm' import esmShimPlugin from './plugins/esmShim'
import modulePathPlugin from './plugins/modulePath' import modulePathPlugin from './plugins/modulePath'
import { isObject, isFilePathESM } from './utils' import isolateEntriesPlugin from './plugins/isolateEntries'
import { type ExternalOptions, externalizeDepsPlugin } from './plugins/externalizeDeps'
import { type BytecodeOptions, bytecodePlugin } from './plugins/bytecode'
import { isObject, isFilePathESM, deepClone, asyncFlatten } from './utils'
export { defineConfig as defineViteConfig } from 'vite' export { defineConfig as defineViteConfig } from 'vite'
interface IsolatedEntriesMixin {
/**
* 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.
*
* **Important**: When using `isolatedEntries` in `preload` config, you
* should also disable `build.externalizeDeps` to ensure third-party dependencies
* from `node_modules` are bundled together, which is required for Electron
* sandbox support.
*
* @experimental
* @default false
*/
isolatedEntries?: boolean
}
interface ExternalizeDepsMixin {
/**
* Options pass on to `externalizeDeps` plugin in electron-vite.
*
* Automatically externalize dependencies.
*
* @default true
*/
externalizeDeps?: boolean | ExternalOptions
}
interface BytecodeMixin {
/**
* Options pass on to `bytecode` plugin in electron-vite.
* https://electron-vite.org/guide/source-code-protection#options
*
* Compile source code to v8 bytecode.
*/
bytecode?: boolean | BytecodeOptions
}
interface MainBuildOptions extends ViteBuildOptions, ExternalizeDepsMixin, BytecodeMixin {}
interface PreloadBuildOptions extends ViteBuildOptions, ExternalizeDepsMixin, BytecodeMixin, IsolatedEntriesMixin {}
interface RendererBuildOptions extends ViteBuildOptions, IsolatedEntriesMixin {}
interface BaseViteConfig<T> extends Omit<ViteConfig, 'build'> {
/**
* Build specific options
*/
build?: T
}
export interface MainViteConfig extends BaseViteConfig<MainBuildOptions> {}
export interface PreloadViteConfig extends BaseViteConfig<PreloadBuildOptions> {}
export interface RendererViteConfig extends BaseViteConfig<RendererBuildOptions> {}
export interface UserConfig { export interface UserConfig {
/** /**
* Vite config options for electron main process * Vite config options for electron main process
* *
* https://vitejs.dev/config/ * @see https://vitejs.dev/config/
*/ */
main?: ViteConfig & { configFile?: string | false } main?: MainViteConfig
/** /**
* Vite config options for electron renderer process * Vite config options for electron renderer process
* *
* https://vitejs.dev/config/ * @see https://vitejs.dev/config/
*/ */
renderer?: ViteConfig & { configFile?: string | false } renderer?: RendererViteConfig
/** /**
* Vite config options for electron preload files * Vite config options for electron preload scripts
* *
* https://vitejs.dev/config/ * @see https://vitejs.dev/config/
*/ */
preload?: ViteConfig & { configFile?: string | false } preload?: PreloadViteConfig
} }
export interface ElectronViteConfig { export type ElectronViteConfigFnObject = (env: ConfigEnv) => UserConfig
/** export type ElectronViteConfigFnPromise = (env: ConfigEnv) => Promise<UserConfig>
* Vite config options for electron main process export type ElectronViteConfigFn = (env: ConfigEnv) => UserConfig | Promise<UserConfig>
*
* https://vitejs.dev/config/
*/
main?: ViteConfigExport
/**
* Vite config options for electron renderer process
*
* https://vitejs.dev/config/
*/
renderer?: ViteConfigExport
/**
* Vite config options for electron preload files
*
* https://vitejs.dev/config/
*/
preload?: ViteConfigExport
}
export type InlineConfig = Omit<ViteConfig, 'base'> & {
configFile?: string | false
envFile?: false
ignoreConfigWarning?: boolean
}
export type ElectronViteConfigFnObject = (env: ConfigEnv) => ElectronViteConfig
export type ElectronViteConfigFnPromise = (env: ConfigEnv) => Promise<ElectronViteConfig>
export type ElectronViteConfigFn = (env: ConfigEnv) => ElectronViteConfig | Promise<ElectronViteConfig>
export type ElectronViteConfigExport = export type ElectronViteConfigExport =
| ElectronViteConfig | UserConfig
| Promise<ElectronViteConfig> | Promise<UserConfig>
| ElectronViteConfigFnObject | ElectronViteConfigFnObject
| ElectronViteConfigFnPromise | ElectronViteConfigFnPromise
| ElectronViteConfigFn | ElectronViteConfigFn
/** /**
* Type helper to make it easier to use `electron.vite.config.*` * Type helper to make it easier to use `electron.vite.config.*`
* accepts a direct {@link ElectronViteConfig} object, or a function that returns it. * accepts a direct {@link UserConfig} object, or a function that returns it.
* The function receives a object that exposes two properties: * The function receives a object that exposes two properties:
* `command` (either `'build'` or `'serve'`), and `mode`. * `command` (either `'build'` or `'serve'`), and `mode`.
*/ */
export function defineConfig(config: ElectronViteConfig): ElectronViteConfig export function defineConfig(config: UserConfig): UserConfig
export function defineConfig(config: Promise<ElectronViteConfig>): Promise<ElectronViteConfig> export function defineConfig(config: Promise<UserConfig>): Promise<UserConfig>
export function defineConfig(config: ElectronViteConfigFnObject): ElectronViteConfigFnObject export function defineConfig(config: ElectronViteConfigFnObject): ElectronViteConfigFnObject
export function defineConfig(config: ElectronViteConfigFnPromise): ElectronViteConfigFnPromise
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport { export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport {
return config return config
} }
export type InlineConfig = Omit<ViteConfig, 'base'> & {
configFile?: string | false
envFile?: false
ignoreConfigWarning?: boolean
}
export interface ResolvedConfig { export interface ResolvedConfig {
config?: UserConfig config?: UserConfig
configFile?: string configFile?: string
@ -123,6 +173,7 @@ export async function resolveConfig(
mode, mode,
command command
} }
const loadResult = await loadConfigFromFile( const loadResult = await loadConfigFromFile(
configEnv, configEnv,
configFile, configFile,
@ -130,15 +181,18 @@ export async function resolveConfig(
config.logLevel, config.logLevel,
config.ignoreConfigWarning config.ignoreConfigWarning
) )
if (loadResult) { if (loadResult) {
const root = config.root const root = config.root
delete config.root delete config.root
delete config.configFile delete config.configFile
config.configFile = false
const outDir = config.build?.outDir const outDir = config.build?.outDir
if (loadResult.config.main) { if (loadResult.config.main) {
const mainViteConfig: ViteConfig = mergeConfig(loadResult.config.main, deepClone(config)) const mainViteConfig: MainViteConfig = mergeConfig(loadResult.config.main, deepClone(config))
mainViteConfig.mode = inlineConfig.mode || mainViteConfig.mode || defaultMode mainViteConfig.mode = inlineConfig.mode || mainViteConfig.mode || defaultMode
@ -146,40 +200,83 @@ export async function resolveConfig(
resetOutDir(mainViteConfig, outDir, 'main') resetOutDir(mainViteConfig, outDir, 'main')
} }
mergePlugins(mainViteConfig, [ const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(mainViteConfig)
...electronMainVitePlugin({ root }),
const builtInMainPlugins: PluginOption[] = [
electronMainConfigPresetPlugin({ root }),
electronMainConfigValidatorPlugin(),
assetPlugin(), assetPlugin(),
workerPlugin(), workerPlugin(),
modulePathPlugin(), modulePathPlugin(
mergeConfig(
{
plugins: [
electronMainConfigPresetPlugin({ root }),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
},
mainViteConfig
)
),
importMetaPlugin(), importMetaPlugin(),
esmShimPlugin() esmShimPlugin(),
]) ...configDrivenPlugins
]
mainViteConfig.plugins = builtInMainPlugins.concat(mainViteConfig.plugins || [])
loadResult.config.main = mainViteConfig loadResult.config.main = mainViteConfig
loadResult.config.main.configFile = false
} }
if (loadResult.config.preload) { if (loadResult.config.preload) {
const preloadViteConfig: ViteConfig = mergeConfig(loadResult.config.preload, deepClone(config)) const preloadViteConfig: PreloadViteConfig = mergeConfig(loadResult.config.preload, deepClone(config))
preloadViteConfig.mode = inlineConfig.mode || preloadViteConfig.mode || defaultMode preloadViteConfig.mode = inlineConfig.mode || preloadViteConfig.mode || defaultMode
if (outDir) { if (outDir) {
resetOutDir(preloadViteConfig, outDir, 'preload') resetOutDir(preloadViteConfig, outDir, 'preload')
} }
mergePlugins(preloadViteConfig, [
...electronPreloadVitePlugin({ root }), const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(preloadViteConfig)
const builtInPreloadPlugins: PluginOption[] = [
electronPreloadConfigPresetPlugin({ root }),
electronPreloadConfigValidatorPlugin(),
assetPlugin(), assetPlugin(),
importMetaPlugin(), importMetaPlugin(),
esmShimPlugin() esmShimPlugin(),
]) ...configDrivenPlugins
]
if (preloadViteConfig.build?.isolatedEntries) {
builtInPreloadPlugins.push(
isolateEntriesPlugin(
mergeConfig(
{
plugins: [
electronPreloadConfigPresetPlugin({ root }),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
},
preloadViteConfig
)
)
)
}
preloadViteConfig.plugins = builtInPreloadPlugins.concat(preloadViteConfig.plugins)
loadResult.config.preload = preloadViteConfig loadResult.config.preload = preloadViteConfig
loadResult.config.preload.configFile = false
} }
if (loadResult.config.renderer) { if (loadResult.config.renderer) {
const rendererViteConfig: ViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config)) const rendererViteConfig: RendererViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config))
rendererViteConfig.mode = inlineConfig.mode || rendererViteConfig.mode || defaultMode rendererViteConfig.mode = inlineConfig.mode || rendererViteConfig.mode || defaultMode
@ -187,10 +284,27 @@ export async function resolveConfig(
resetOutDir(rendererViteConfig, outDir, 'renderer') resetOutDir(rendererViteConfig, outDir, 'renderer')
} }
mergePlugins(rendererViteConfig, electronRendererVitePlugin({ root })) const builtInRendererPlugins: PluginOption[] = [
electronRendererConfigPresetPlugin({ root }),
electronRendererConfigValidatorPlugin()
]
if (rendererViteConfig.build?.isolatedEntries) {
builtInRendererPlugins.push(
isolateEntriesPlugin(
mergeConfig(
{
plugins: [electronRendererConfigPresetPlugin({ root })]
},
rendererViteConfig
)
)
)
}
rendererViteConfig.plugins = builtInRendererPlugins.concat(rendererViteConfig.plugins || [])
loadResult.config.renderer = rendererViteConfig loadResult.config.renderer = rendererViteConfig
loadResult.config.renderer.configFile = false
} }
userConfig = loadResult.config userConfig = loadResult.config
@ -208,10 +322,6 @@ export async function resolveConfig(
return resolved return resolved
} }
function deepClone<T>(data: T): T {
return JSON.parse(JSON.stringify(data))
}
function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void { function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void {
let userOutDir = config.build?.outDir let userOutDir = config.build?.outDir
if (outDir === userOutDir) { if (outDir === userOutDir) {
@ -224,9 +334,36 @@ function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): voi
} }
} }
function mergePlugins(config: ViteConfig, plugins: Plugin[]): void { async function resolveConfigDrivenPlugins(config: MainViteConfig | PreloadViteConfig): Promise<PluginOption[]> {
const userPlugins = config.plugins || [] const userPlugins = (await asyncFlatten(config.plugins || [])).filter(Boolean) as Plugin[]
config.plugins = userPlugins.concat(plugins)
const configDrivenPlugins: PluginOption[] = []
const hasExternalizeDepsPlugin = userPlugins.some(p => p.name === 'vite:externalize-deps')
if (!hasExternalizeDepsPlugin) {
const externalOptions = config.build?.externalizeDeps ?? true
if (externalOptions) {
isOptions<ExternalOptions>(externalOptions)
? configDrivenPlugins.push(externalizeDepsPlugin(externalOptions))
: configDrivenPlugins.push(externalizeDepsPlugin())
}
}
const hasBytecodePlugin = userPlugins.some(p => p.name === 'vite:bytecode')
if (!hasBytecodePlugin) {
const bytecodeOptions = config.build?.bytecode
if (bytecodeOptions) {
isOptions<BytecodeOptions>(bytecodeOptions)
? configDrivenPlugins.push(bytecodePlugin(bytecodeOptions))
: configDrivenPlugins.push(bytecodePlugin())
}
}
return configDrivenPlugins
}
function isOptions<T extends object>(value: boolean | T): value is T {
return typeof value === 'object' && value !== null
} }
const CONFIG_FILE_NAME = 'electron.vite.config' const CONFIG_FILE_NAME = 'electron.vite.config'
@ -261,63 +398,27 @@ export async function loadConfigFromFile(
const isESM = isFilePathESM(resolvedPath) const isESM = isFilePathESM(resolvedPath)
try { try {
const bundled = await bundleConfigFile(resolvedPath, isESM) const { code, dependencies } = await bundleConfigFile(resolvedPath, isESM)
const userConfig = await loadConfigFormBundledFile(configRoot, resolvedPath, bundled.code, isESM) const configExport = await loadConfigFormBundledFile(configRoot, resolvedPath, code, isESM)
const config = await (typeof userConfig === 'function' ? userConfig(configEnv) : userConfig) const config = await (typeof configExport === 'function' ? configExport(configEnv) : configExport)
if (!isObject(config)) { if (!isObject(config)) {
throw new Error(`config must export or return an object`) throw new Error(`config must export or return an object`)
} }
const configRequired: string[] = [] if (!ignoreConfigWarning) {
const missingFields = ['main', 'renderer', 'preload'].filter(field => !config[field])
let mainConfig if (missingFields.length > 0) {
if (config.main) { createLogger(logLevel).warn(
const mainViteConfig = config.main `${colors.yellow(colors.bold('(!)'))} ${colors.yellow(`${missingFields.join(' and ')} config is missing`)}\n`
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 { return {
path: normalizePath(resolvedPath), path: normalizePath(resolvedPath),
config: { config,
main: mainConfig, dependencies
renderer: rendererConfig,
preload: preloadConfig
},
dependencies: bundled.dependencies
} }
} catch (e) { } catch (e) {
createLogger(logLevel).error(colors.red(`failed to load config from ${resolvedPath}`), { error: e as Error }) createLogger(logLevel).error(colors.red(`failed to load config from ${resolvedPath}`), { error: e as Error })
@ -343,7 +444,7 @@ async function bundleConfigFile(fileName: string, isESM: boolean): Promise<{ cod
absWorkingDir: process.cwd(), absWorkingDir: process.cwd(),
entryPoints: [fileName], entryPoints: [fileName],
write: false, write: false,
target: ['node18'], target: ['node20'],
platform: 'node', platform: 'node',
bundle: true, bundle: true,
format: isESM ? 'esm' : 'cjs', format: isESM ? 'esm' : 'cjs',

View File

@ -41,9 +41,9 @@ export function supportESM(): boolean {
return parseInt(majorVer) >= 28 return parseInt(majorVer) >= 28
} }
export function getElectronMajorVersion(): number { export function supportImportMetaPaths(): boolean {
const majorVer = getElectronMajorVer() const majorVer = getElectronMajorVer()
return parseInt(majorVer) return parseInt(majorVer) >= 30
} }
export function getElectronPath(): string { export function getElectronPath(): string {
@ -69,6 +69,14 @@ export function getElectronNodeTarget(): string {
const electronVer = getElectronMajorVer() const electronVer = getElectronMajorVer()
const nodeVer = { const nodeVer = {
'39': '22.20',
'38': '22.19',
'37': '22.16',
'36': '22.14',
'35': '22.14',
'34': '20.18',
'33': '20.18',
'32': '20.16',
'31': '20.14', '31': '20.14',
'30': '20.11', '30': '20.11',
'29': '20.9', '29': '20.9',
@ -78,16 +86,7 @@ export function getElectronNodeTarget(): string {
'25': '18.15', '25': '18.15',
'24': '18.14', '24': '18.14',
'23': '18.12', '23': '18.12',
'22': '16.17', '22': '16.17'
'21': '16.16',
'20': '16.15',
'19': '16.14',
'18': '16.13',
'17': '16.13',
'16': '16.9',
'15': '16.5',
'14': '14.17',
'13': '14.17'
} }
if (electronVer && parseInt(electronVer) > 10) { if (electronVer && parseInt(electronVer) > 10) {
let target = nodeVer[electronVer] let target = nodeVer[electronVer]
@ -101,6 +100,14 @@ export function getElectronChromeTarget(): string {
const electronVer = getElectronMajorVer() const electronVer = getElectronMajorVer()
const chromeVer = { const chromeVer = {
'39': '142',
'38': '140',
'37': '138',
'36': '136',
'35': '134',
'34': '132',
'33': '130',
'32': '128',
'31': '126', '31': '126',
'30': '124', '30': '124',
'29': '122', '29': '122',
@ -110,16 +117,7 @@ export function getElectronChromeTarget(): string {
'25': '114', '25': '114',
'24': '112', '24': '112',
'23': '110', '23': '110',
'22': '108', '22': '108'
'21': '106',
'20': '104',
'19': '102',
'18': '100',
'17': '98',
'16': '96',
'15': '94',
'14': '93',
'13': '91'
} }
if (electronVer && parseInt(electronVer) > 10) { if (electronVer && parseInt(electronVer) > 10) {
let target = chromeVer[electronVer] let target = chromeVer[electronVer]

View File

@ -1,4 +1,4 @@
export { type LogLevel, createLogger, mergeConfig, splitVendorChunkPlugin, splitVendorChunk } from 'vite' export { type LogLevel, createLogger, mergeConfig } from 'vite'
export * from './config' export * from './config'
export { createServer } from './server' export { createServer } from './server'
export { build } from './build' export { build } from './build'

View File

@ -3,48 +3,16 @@ import fs from 'node:fs/promises'
import type { SourceMapInput } from 'rollup' import type { SourceMapInput } from 'rollup'
import { type Plugin, normalizePath } from 'vite' import { type Plugin, normalizePath } from 'vite'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { cleanUrl, parseRequest, getHash, toRelativePath } from '../utils' import { cleanUrl, getHash, toRelativePath } from '../utils'
import { supportImportMetaPaths } from '../electron'
interface AssetResolved {
type: 'asset' | 'native' | 'wasm'
file: string
query: Record<string, string> | null
}
function resolveAsset(id: string): AssetResolved | null {
const file = cleanUrl(id)
const query = parseRequest(id)
if (query && typeof query.asset === 'string') {
return {
type: 'asset',
file,
query
}
}
if (file.endsWith('.node')) {
return {
type: 'native',
file,
query
}
}
if (id.endsWith('.wasm?loader')) {
return {
type: 'wasm',
file,
query
}
}
return null
}
const nodeAssetRE = /__VITE_NODE_ASSET__([\w$]+)__/g const nodeAssetRE = /__VITE_NODE_ASSET__([\w$]+)__/g
const nodePublicAssetRE = /__VITE_NODE_PUBLIC_ASSET__([a-z\d]{8})__/g const nodePublicAssetRE = /__VITE_NODE_PUBLIC_ASSET__([a-z\d]{8})__/g
const assetImportRE = /(?:[?|&]asset(?:&|$)|\.wasm\?loader$|\.node$)/
const assetRE = /[?|&]asset(?:&|$)/
const assetUnpackRE = /[?|&]asset&asarUnpack$/
const wasmHelperId = '\0__electron-vite-wasm-helper' const wasmHelperId = '\0__electron-vite-wasm-helper'
const wasmHelperCode = ` const wasmHelperCode = `
@ -59,11 +27,10 @@ export default async function loadWasm(file, importObject = {}) {
` `
export default function assetPlugin(): Plugin { export default function assetPlugin(): Plugin {
let sourcemap: boolean | 'inline' | 'hidden' = false
let publicDir = '' let publicDir = ''
let outDir = ''
const publicAssetPathCache = new Map<string, string>() const publicAssetPathCache = new Map<string, string>()
const assetCache = new Map<string, string>() const assetCache = new Map<string, string>()
const isImportMetaPathSupported = supportImportMetaPaths()
return { return {
name: 'vite:node-asset', name: 'vite:node-asset',
apply: 'build', apply: 'build',
@ -73,9 +40,7 @@ export default function assetPlugin(): Plugin {
assetCache.clear() assetCache.clear()
}, },
configResolved(config): void { configResolved(config): void {
sourcemap = config.build.sourcemap publicDir = config.publicDir
publicDir = normalizePath(config.publicDir)
outDir = normalizePath(path.resolve(config.root, config.build.outDir))
}, },
resolveId(id): string | void { resolveId(id): string | void {
if (id === wasmHelperId) { if (id === wasmHelperId) {
@ -87,19 +52,12 @@ export default function assetPlugin(): Plugin {
return wasmHelperCode return wasmHelperCode
} }
if (id.startsWith('\0')) { if (id.startsWith('\0') || !assetImportRE.test(id)) {
// Rollup convention, this id should be handled by the
// plugin that marked it with \0
return
}
const assetResolved = resolveAsset(id)
if (!assetResolved) {
return return
} }
let referenceId: string let referenceId: string
const file = assetResolved.file const file = cleanUrl(id)
if (publicDir && file.startsWith(publicDir)) { if (publicDir && file.startsWith(publicDir)) {
const hash = getHash(file) const hash = getHash(file)
if (!publicAssetPathCache.get(hash)) { if (!publicAssetPathCache.get(hash)) {
@ -115,65 +73,62 @@ export default function assetPlugin(): Plugin {
const hash = this.emitFile({ const hash = this.emitFile({
type: 'asset', type: 'asset',
name: path.basename(file), name: path.basename(file),
source source: source as unknown as Uint8Array
}) })
referenceId = `__VITE_NODE_ASSET__${hash}__` referenceId = `__VITE_NODE_ASSET__${hash}__`
assetCache.set(file, referenceId) assetCache.set(file, referenceId)
} }
} }
if (assetResolved.type === 'asset') { if (assetRE.test(id)) {
if (assetResolved.query && typeof assetResolved.query.asarUnpack === 'string') { const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname'
if (assetUnpackRE.test(id)) {
return ` return `
import { join } from 'path' import { join } from 'path'
export default join(__dirname, ${referenceId}).replace('app.asar', 'app.asar.unpacked')` export default join(${dirnameExpr}, ${referenceId}).replace('app.asar', 'app.asar.unpacked')`
} else { } else {
return ` return `
import { join } from 'path' import { join } from 'path'
export default join(__dirname, ${referenceId})` export default join(${dirnameExpr}, ${referenceId})`
} }
} }
if (assetResolved.type === 'native') { if (id.endsWith('.node')) {
return `export default require(${referenceId})` return `export default require(${referenceId})`
} }
if (assetResolved.type === 'wasm') { if (id.endsWith('.wasm?loader')) {
return ` return `
import loadWasm from ${JSON.stringify(wasmHelperId)} import loadWasm from ${JSON.stringify(wasmHelperId)}
export default importObject => loadWasm(${referenceId}, importObject)` export default importObject => loadWasm(${referenceId}, importObject)`
} }
}, },
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null { renderChunk(code, chunk, { sourcemap, dir }): { code: string; map: SourceMapInput } | null {
let match: RegExpExecArray | null let match: RegExpExecArray | null
let s: MagicString | undefined let s: MagicString | undefined
nodeAssetRE.lastIndex = 0 nodeAssetRE.lastIndex = 0
if (code.match(nodeAssetRE)) { while ((match = nodeAssetRE.exec(code))) {
while ((match = nodeAssetRE.exec(code))) { s ||= new MagicString(code)
s ||= new MagicString(code) const [full, hash] = match
const [full, hash] = match const filename = this.getFileName(hash)
const filename = this.getFileName(hash) const outputFilepath = toRelativePath(filename, chunk.fileName)
const outputFilepath = toRelativePath(filename, chunk.fileName) const replacement = JSON.stringify(outputFilepath)
const replacement = JSON.stringify(outputFilepath) s.overwrite(match.index, match.index + full.length, replacement, {
s.overwrite(match.index, match.index + full.length, replacement, { contentOnly: true
contentOnly: true })
})
}
} }
nodePublicAssetRE.lastIndex = 0 nodePublicAssetRE.lastIndex = 0
if (code.match(nodePublicAssetRE)) { while ((match = nodePublicAssetRE.exec(code))) {
while ((match = nodePublicAssetRE.exec(code))) { s ||= new MagicString(code)
s ||= new MagicString(code) const [full, hash] = match
const [full, hash] = match const filename = publicAssetPathCache.get(hash)!
const filename = publicAssetPathCache.get(hash)! const outputFilepath = toRelativePath(filename, normalizePath(path.join(dir!, chunk.fileName)))
const outputFilepath = toRelativePath(filename, normalizePath(path.join(outDir, chunk.fileName))) const replacement = JSON.stringify(outputFilepath)
const replacement = JSON.stringify(outputFilepath) s.overwrite(match.index, match.index + full.length, replacement, {
s.overwrite(match.index, match.index + full.length, replacement, { contentOnly: true
contentOnly: true })
})
}
} }
if (s) { if (s) {
@ -181,9 +136,9 @@ export default function assetPlugin(): Plugin {
code: s.toString(), code: s.toString(),
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
} }
} else {
return null
} }
return null
} }
} }
} }

View File

@ -0,0 +1,30 @@
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 => {
if (id.includes('node_modules')) {
return false
}
const info = this.getModuleInfo(id)
return info && !info.isExternal
})
moduleIds.push(...sourceFiles)
},
api: {
getWatchFiles() {
return moduleIds
}
}
}
}

View File

@ -1,12 +1,11 @@
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs'
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import colors from 'picocolors' import colors from 'picocolors'
import { type Plugin, type ResolvedConfig, normalizePath, createFilter } from 'vite' import { type Plugin, type Logger, type LibraryOptions, normalizePath } from 'vite'
import * as babel from '@babel/core' import * as babel from '@babel/core'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import type { SourceMapInput, OutputChunk } from 'rollup' import type { SourceMapInput, OutputChunk, OutputOptions } from 'rollup'
import { getElectronPath } from '../electron' import { getElectronPath } from '../electron'
import { toRelativePath } from '../utils' import { toRelativePath } from '../utils'
@ -141,6 +140,8 @@ const bytecodeModuleLoaderCode = [
`};` `};`
] ]
const bytecodeChunkExtensionRE = /.(jsc|cjsc)$/
export interface BytecodeOptions { export interface BytecodeOptions {
chunkAlias?: string | string[] chunkAlias?: string | string[]
transformArrowFunctions?: boolean transformArrowFunctions?: boolean
@ -149,7 +150,9 @@ export interface BytecodeOptions {
} }
/** /**
* Compile to v8 bytecode to protect source code. * Compile source code to v8 bytecode.
*
* @deprecated use `build.bytecode` config option instead
*/ */
export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null { export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') { if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') {
@ -159,174 +162,138 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options
const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias] const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
const filter = createFilter(/\.(m?[jt]s|[jt]sx)$/)
const escapeRegExpString = (str: string): string => {
return str
.replace(/\\/g, '\\\\\\\\')
.replace(/[|{}()[\]^$+*?.]/g, '\\$&')
.replace(/-/g, '\\u002d')
}
const transformAllChunks = _chunkAlias.length === 0 const transformAllChunks = _chunkAlias.length === 0
const isBytecodeChunk = (chunkName: string): boolean => { const isBytecodeChunk = (chunkName: string): boolean => {
return transformAllChunks || _chunkAlias.some(alias => alias === chunkName) return transformAllChunks || _chunkAlias.some(alias => alias === chunkName)
} }
const _transform = (code: string): string => { const plugins: babel.PluginItem[] = []
const re = babel.transform(code, {
plugins: ['@babel/plugin-transform-arrow-functions'] if (transformArrowFunctions) {
}) plugins.push('@babel/plugin-transform-arrow-functions')
return re.code || '' }
if (protectedStrings.length > 0) {
plugins.push([protectStringsPlugin, { protectedStrings: new Set(protectedStrings) }])
}
const shouldTransformBytecodeChunk = plugins.length !== 0
const _transform = (code: string, sourceMaps: boolean = false): { code: string; map?: SourceMapInput } | null => {
const re = babel.transform(code, { plugins, sourceMaps })
return re ? { code: re.code || '', map: re.map } : null
} }
const useStrict = '"use strict";' const useStrict = '"use strict";'
const bytecodeModuleLoader = 'bytecode-loader.cjs' const bytecodeModuleLoader = 'bytecode-loader.cjs'
let config: ResolvedConfig let logger: Logger
let useInRenderer = false let supported = false
let bytecodeRequired = false
let bytecodeFiles: { name: string; size: number }[] = []
return { return {
name: 'vite:bytecode', name: 'vite:bytecode',
apply: 'build', apply: 'build',
enforce: 'post', enforce: 'post',
configResolved(resolvedConfig): void { configResolved(config): void {
config = resolvedConfig if (supported) {
useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config') return
}
logger = config.logger
const useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config')
if (useInRenderer) { if (useInRenderer) {
config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.')) config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.'))
return
} }
if (resolvedConfig.build.minify && protectedStrings.length > 0) { const build = config.build
config.logger.warn(colors.yellow('Strings cannot be protected when minification is enabled.')) const resolvedOutputs = resolveBuildOutputs(build.rollupOptions.output, build.lib)
} if (resolvedOutputs) {
}, const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
transform(code, id): void | { code: string; map: SourceMapInput } { const output = outputs[0]
if (config.build.minify || protectedStrings.length === 0 || !filter(id)) return if (output.format === 'es') {
config.logger.warn(
let match: RegExpExecArray | null colors.yellow(
let s: MagicString | undefined 'bytecodePlugin does not support ES module, please remove "type": "module" ' +
'in package.json or set the "build.rollupOptions.output.format" option to "cjs".'
protectedStrings.forEach(str => { )
const escapedStr = escapeRegExpString(str)
const re = new RegExp(`\\u0027${escapedStr}\\u0027|\\u0022${escapedStr}\\u0022`, 'g')
const charCodes = Array.from(str).map(s => s.charCodeAt(0))
const replacement = `String.fromCharCode(${charCodes.toString()})`
while ((match = re.exec(code))) {
s ||= new MagicString(code)
const [full] = match
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true
})
}
})
if (s) {
return {
code: s.toString(),
map: config.build.sourcemap ? s.generateMap({ hires: 'boundary' }) : null
}
}
},
renderChunk(code, chunk, options): { code: string } | null {
if (options.format === 'es') {
config.logger.warn(
colors.yellow(
'bytecodePlugin does not support ES module, please remove "type": "module" ' +
'in package.json or set the "build.rollupOptions.output.format" option to "cjs".'
) )
)
return null
}
if (useInRenderer) {
return null
}
if (chunk.type === 'chunk' && isBytecodeChunk(chunk.name)) {
bytecodeRequired = true
if (transformArrowFunctions) {
return {
code: _transform(code)
}
} }
supported = output.format === 'cjs' && !useInRenderer
}
},
renderChunk(code, chunk, { sourcemap }): { code: string; map?: SourceMapInput } | null {
if (supported && isBytecodeChunk(chunk.name) && shouldTransformBytecodeChunk) {
return _transform(code, !!sourcemap)
} }
return null return null
}, },
generateBundle(options): void { async generateBundle(_, output): Promise<void> {
if (options.format !== 'es' && !useInRenderer && bytecodeRequired) { if (!supported) {
this.emitFile({ return
type: 'asset',
source: bytecodeModuleLoaderCode.join('\n') + '\n',
name: 'Bytecode Loader File',
fileName: bytecodeModuleLoader
})
} }
}, const _chunks = Object.values(output)
async writeBundle(options, output): Promise<void> { const chunks = _chunks.filter(chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name)) as OutputChunk[]
if (options.format === 'es' || useInRenderer || !bytecodeRequired) {
if (chunks.length === 0) {
return return
} }
const outDir = options.dir!
bytecodeFiles = []
const bundles = Object.keys(output)
const chunks = Object.values(output).filter(
chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name) && chunk.fileName !== bytecodeModuleLoader
) as OutputChunk[]
const bytecodeChunks = chunks.map(chunk => chunk.fileName) const bytecodeChunks = chunks.map(chunk => chunk.fileName)
const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName)) const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName))
const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|') const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|')
const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null
const keepBundle = (chunkFileName: string): void => {
const newFileName = path.resolve(path.dirname(chunkFileName), `_${path.basename(chunkFileName)}`)
fs.renameSync(chunkFileName, newFileName)
}
const getBytecodeLoaderBlock = (chunkFileName: string): string => { const getBytecodeLoaderBlock = (chunkFileName: string): string => {
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");` return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
} }
let bytecodeChunkCount = 0
const bundles = Object.keys(output)
await Promise.all( await Promise.all(
bundles.map(async name => { bundles.map(async name => {
const chunk = output[name] const chunk = output[name]
if (chunk.type === 'chunk') { if (chunk.type === 'chunk') {
let _code = chunk.code let _code = chunk.code
if (bytecodeRE && _code.match(bytecodeRE)) { if (bytecodeRE) {
let match: RegExpExecArray | null let match: RegExpExecArray | null
const s = new MagicString(_code) let s: MagicString | undefined
while ((match = bytecodeRE.exec(_code))) { while ((match = bytecodeRE.exec(_code))) {
s ||= new MagicString(_code)
const [prefix, chunkName] = match const [prefix, chunkName] = match
const len = prefix.length + chunkName.length const len = prefix.length + chunkName.length
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', { s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
contentOnly: true contentOnly: true
}) })
} }
_code = s.toString() if (s) {
_code = s.toString()
}
} }
const chunkFileName = path.resolve(outDir, name)
if (bytecodeChunks.includes(name)) { if (bytecodeChunks.includes(name)) {
const bytecodeBuffer = await compileToBytecode(_code) const bytecodeBuffer = await compileToBytecode(_code)
fs.writeFileSync(path.resolve(outDir, name + 'c'), bytecodeBuffer) this.emitFile({
type: 'asset',
fileName: name + 'c',
source: bytecodeBuffer
})
if (!removeBundleJS) {
this.emitFile({
type: 'asset',
fileName: '_' + chunk.fileName,
source: chunk.code
})
}
if (chunk.isEntry) { if (chunk.isEntry) {
if (!removeBundleJS) {
keepBundle(chunkFileName)
}
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName) const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");` const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n` const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
fs.writeFileSync(chunkFileName, code) chunk.code = code
} else { } else {
if (removeBundleJS) { delete output[chunk.fileName]
fs.unlinkSync(chunkFileName)
} else {
keepBundle(chunkFileName)
}
} }
bytecodeFiles.push({ name: name + 'c', size: bytecodeBuffer.length }) bytecodeChunkCount += 1
} else { } else {
if (chunk.isEntry) { if (chunk.isEntry) {
let hasBytecodeMoudle = false let hasBytecodeMoudle = false
@ -343,34 +310,118 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
for (const importerId of dynamicImporters) idsToHandle.add(importerId) for (const importerId of dynamicImporters) idsToHandle.add(importerId)
} }
} }
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName) _code = hasBytecodeMoudle
_code = hasBytecodeMoudle ? _code.replace(useStrict, `${useStrict}\n${bytecodeLoaderBlock}`) : _code ? _code.replace(
/("use strict";)|('use strict';)/,
`${useStrict}\n${getBytecodeLoaderBlock(chunk.fileName)}`
)
: _code
} }
fs.writeFileSync(chunkFileName, _code) chunk.code = _code
} }
} }
}) })
) )
if (bytecodeChunkCount && !_chunks.some(ass => ass.type === 'asset' && ass.fileName === bytecodeModuleLoader)) {
this.emitFile({
type: 'asset',
source: bytecodeModuleLoaderCode.join('\n') + '\n',
name: 'Bytecode Loader File',
fileName: bytecodeModuleLoader
})
}
}, },
closeBundle(): void { writeBundle(_, output): void {
if (!useInRenderer) { if (supported) {
const chunkLimit = config.build.chunkSizeWarningLimit const bytecodeChunkCount = Object.keys(output).filter(chunk => bytecodeChunkExtensionRE.test(chunk)).length
const outDir = normalizePath(path.relative(config.root, path.resolve(config.root, config.build.outDir))) + '/' logger.info(`${colors.green(``)} ${bytecodeChunkCount} chunks compiled into bytecode.`)
config.logger.info(`${colors.green(``)} ${bytecodeFiles.length} bundles compiled into bytecode.`) }
let longest = 0 }
bytecodeFiles.forEach(file => { }
const len = file.name.length }
if (len > longest) longest = len
}) function resolveBuildOutputs(
bytecodeFiles.forEach(file => { outputs: OutputOptions | OutputOptions[] | undefined,
const kbs = file.size / 1000 libOptions: LibraryOptions | false
config.logger.info( ): OutputOptions | OutputOptions[] | undefined {
`${colors.gray(colors.white(colors.dim(outDir)))}${colors.green(file.name.padEnd(longest + 2))} ${ if (libOptions && !Array.isArray(outputs)) {
kbs > chunkLimit ? colors.yellow(`${kbs.toFixed(2)} kB`) : colors.dim(`${kbs.toFixed(2)} kB`) const libFormats = libOptions.formats || []
}` return libFormats.map(format => ({ ...outputs, format }))
) }
}) return outputs
bytecodeFiles = [] }
interface ProtectStringsPluginState extends babel.PluginPass {
opts: { protectedStrings: Set<string> }
}
function protectStringsPlugin(api: typeof babel & babel.ConfigAPI): babel.PluginObj<ProtectStringsPluginState> {
const { types: t } = api
function createFromCharCodeFunction(value: string): babel.types.CallExpression {
const charCodes = Array.from(value).map(s => s.charCodeAt(0))
const charCodeLiterals = charCodes.map(code => t.numericLiteral(code))
// String.fromCharCode
const memberExpression = t.memberExpression(t.identifier('String'), t.identifier('fromCharCode'))
// String.fromCharCode(...arr)
const callExpression = t.callExpression(memberExpression, [t.spreadElement(t.identifier('arr'))])
// return String.fromCharCode(...arr)
const returnStatement = t.returnStatement(callExpression)
// function (arr) { return ... }
const functionExpression = t.functionExpression(null, [t.identifier('arr')], t.blockStatement([returnStatement]))
// (function(...) { ... })([x, x, x])
return t.callExpression(functionExpression, [t.arrayExpression(charCodeLiterals)])
}
return {
name: 'protect-strings-plugin',
visitor: {
StringLiteral(path, state) {
// obj['property']
if (path.parentPath.isMemberExpression({ property: path.node, computed: true })) {
return
}
// { 'key': value }
if (path.parentPath.isObjectProperty({ key: path.node, computed: false })) {
return
}
// require('fs')
if (
path.parentPath.isCallExpression() &&
t.isIdentifier(path.parentPath.node.callee) &&
path.parentPath.node.callee.name === 'require' &&
path.parentPath.node.arguments[0] === path.node
) {
return
}
// Only CommonJS is supported, import declaration and export declaration checks are ignored
const { value } = path.node
if (state.opts.protectedStrings.has(value)) {
path.replaceWith(createFromCharCodeFunction(value))
}
},
TemplateLiteral(path, state) {
// Must be a pure static template literal
// expressions must be empty (no ${variables})
// quasis must have only one element (meaning the entire string is a single static part).
if (path.node.expressions.length > 0 || path.node.quasis.length !== 1) {
return
}
// Extract the raw value of the template literal
// path.node.quasis[0].value.raw is used to get the raw string, including escape sequences
// path.node.quasis[0].value.cooked is used to get the processed/cooked string (with escape sequences handled)
const value = path.node.quasis[0].value.cooked
if (value && state.opts.protectedStrings.has(value)) {
path.replaceWith(createFromCharCodeFunction(value))
}
} }
} }
} }

View File

@ -50,361 +50,363 @@ function resolveBuildOutputs(
return outputs return outputs
} }
export function electronMainVitePlugin(options?: ElectronPluginOptions): Plugin[] { export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
return [ return {
{ name: 'vite:electron-main-config-preset',
name: 'vite:electron-main-preset-config', apply: 'build',
apply: 'build', enforce: 'pre',
enforce: 'pre', config(config): void {
config(config): void { const root = options?.root || process.cwd()
const root = options?.root || process.cwd()
const nodeTarget = getElectronNodeTarget() const nodeTarget = getElectronNodeTarget()
const pkg = loadPackageData() || { type: 'commonjs' } const pkg = loadPackageData() || { type: 'commonjs' }
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs' const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
const defaultConfig = { const defaultConfig = {
resolve: { resolve: {
browserField: false, browserField: false,
mainFields: ['module', 'jsnext:main', 'jsnext'], mainFields: ['module', 'jsnext:main', 'jsnext'],
conditions: ['node'] conditions: ['node']
},
build: {
outDir: path.resolve(root, 'out', 'main'),
target: nodeTarget,
assetsDir: 'chunks',
rollupOptions: {
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {}
}, },
build: { reportCompressedSize: false,
outDir: path.resolve(root, 'out', 'main'), minify: false
target: nodeTarget,
assetsDir: 'chunks',
rollupOptions: {
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {}
},
reportCompressedSize: false,
minify: false
}
} }
const build = config.build || {}
const rollupOptions = build.rollupOptions || {}
if (!rollupOptions.input) {
const libOptions = build.lib
const outputOptions = rollupOptions.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.rollupOptions.output['format'] = format
}
defaultConfig.build.rollupOptions.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 } }
} }
},
{
name: 'vite:electron-main-resolved-config',
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 build = config.build || {}
const rollupOptions = build.rollupOptions || {}
if (!rollupOptions.input) {
const libOptions = build.lib const libOptions = build.lib
const rollupOptions = build.rollupOptions const outputOptions = rollupOptions.output
defaultConfig.build['lib'] = {
if (!(libOptions && libOptions.entry) && !rollupOptions?.input) { entry: findLibEntry(root, 'main'),
throw new Error( formats:
'An entry point is required in the electron vite main config, ' + libOptions && libOptions.formats && libOptions.formats.length > 0
'which can be specified using "build.lib.entry" or "build.rollupOptions.input".' ? []
) : [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
}
const resolvedOutputs = resolveBuildOutputs(rollupOptions.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"' : ''}.`
)
}
}
} }
} else {
defaultConfig.build.rollupOptions.output['format'] = format
} }
defaultConfig.build.rollupOptions.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 } }
} }
] }
} }
export function electronPreloadVitePlugin(options?: ElectronPluginOptions): Plugin[] { export function electronMainConfigValidatorPlugin(): Plugin {
return [ return {
{ name: 'vite:electron-main-config-validator',
name: 'vite:electron-preload-preset-config', apply: 'build',
apply: 'build', enforce: 'post',
enforce: 'pre', configResolved(config): void {
config(config): void { const build = config.build
const root = options?.root || process.cwd() if (!build.target) {
throw new Error('build.target option is required in the electron vite main config.')
const nodeTarget = getElectronNodeTarget() } else {
const targets = Array.isArray(build.target) ? build.target : [build.target]
const pkg = loadPackageData() || { type: 'commonjs' } if (targets.some(t => !t.startsWith('node'))) {
throw new Error('The electron vite main config build.target option must be "node?".')
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
const defaultConfig = {
build: {
outDir: path.resolve(root, 'out', 'preload'),
target: nodeTarget,
assetsDir: 'chunks',
rollupOptions: {
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {}
},
reportCompressedSize: false,
minify: false
}
} }
const build = config.build || {}
const rollupOptions = build.rollupOptions || {}
if (!rollupOptions.input) {
const libOptions = build.lib
const outputOptions = rollupOptions.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.rollupOptions.output['format'] = format
}
defaultConfig.build.rollupOptions.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.rollupOptions!.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.rollupOptions!.output)) {
config.build.rollupOptions!.output.forEach(output => {
if (output.format === 'es') {
output['entryFileNames'] = '[name].mjs'
output['chunkFileNames'] = '[name]-[hash].mjs'
}
})
} else {
config.build.rollupOptions!.output!['entryFileNames'] = '[name].mjs'
config.build.rollupOptions!.output!['chunkFileNames'] = '[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 = { ...config.ssr, ...{ noExternal: true } }
} }
},
{ const libOptions = build.lib
name: 'vite:electron-preload-resolved-config', const rollupOptions = build.rollupOptions
apply: 'build',
enforce: 'post', if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
configResolved(config): void { throw new Error(
const build = config.build 'An entry point is required in the electron vite main config, ' +
if (!build.target) { 'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
throw new Error('build.target option is required in the electron vite preload config.') )
}
const resolvedOutputs = resolveBuildOutputs(rollupOptions.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 { } else {
const targets = Array.isArray(build.target) ? build.target : [build.target] const outpout = outputs[0]
if (targets.some(t => !t.startsWith('node'))) { if (['es', 'cjs'].includes(outpout.format || '')) {
throw new Error('The electron vite preload config build.target must be "node?".') if (outpout.format === 'es' && !supportESM()) {
}
}
const libOptions = build.lib
const rollupOptions = build.rollupOptions
if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
throw new Error(
'An entry point is required in the electron vite preload config, ' +
'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
)
}
const resolvedOutputs = resolveBuildOutputs(rollupOptions.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( throw new Error(
`The electron vite preload config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.` '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 electronRendererVitePlugin(options?: ElectronPluginOptions): Plugin[] {
return [
{
name: 'vite:electron-renderer-preset-config',
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 },
rollupOptions: {
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_']
}
},
{
name: 'vite:electron-renderer-resolved-config',
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 rollupOptions = build.rollupOptions export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
if (!rollupOptions.input) { return {
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory.`)) name: 'vite:electron-preload-config-preset',
throw new Error('build.rollupOptions.input option is required in the electron vite renderer config.') 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',
rollupOptions: {
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {}
},
reportCompressedSize: false,
minify: false
}
}
const build = config.build || {}
const rollupOptions = build.rollupOptions || {}
if (!rollupOptions.input) {
const libOptions = build.lib
const outputOptions = rollupOptions.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.rollupOptions.output['format'] = format
}
defaultConfig.build.rollupOptions.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.rollupOptions!.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.rollupOptions!.output)) {
config.build.rollupOptions!.output.forEach(output => {
if (output.format === 'es') {
output['entryFileNames'] = '[name].mjs'
output['chunkFileNames'] = '[name]-[hash].mjs'
}
})
} else {
config.build.rollupOptions!.output!['entryFileNames'] = '[name].mjs'
config.build.rollupOptions!.output!['chunkFileNames'] = '[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
}
}
}
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 rollupOptions = build.rollupOptions
if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
throw new Error(
'An entry point is required in the electron vite preload config, ' +
'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
)
}
const resolvedOutputs = resolveBuildOutputs(rollupOptions.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 },
rollupOptions: {
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_']
}
}
}
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 rollupOptions = build.rollupOptions
if (!rollupOptions.input) {
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory.`))
throw new Error('build.rollupOptions.input option is required in the electron vite renderer config.')
}
}
}
} }

View File

@ -8,7 +8,7 @@ import MagicString from 'magic-string'
import type { SourceMapInput } from 'rollup' import type { SourceMapInput } from 'rollup'
import type { Plugin } from 'vite' import type { Plugin } from 'vite'
import { getElectronMajorVersion } from '../electron' import { supportImportMetaPaths } from '../electron'
const CJSyntaxRe = /__filename|__dirname|require\(|require\.resolve\(/ const CJSyntaxRe = /__filename|__dirname|require\(|require\.resolve\(/
@ -46,19 +46,14 @@ function findStaticImports(code: string): StaticImport[] {
} }
export default function esmShimPlugin(): Plugin { export default function esmShimPlugin(): Plugin {
let sourcemap: boolean | 'inline' | 'hidden' = false const CJSShim = supportImportMetaPaths() ? CJSShim_node_20_11 : CJSShim_normal
const CJSShim = getElectronMajorVersion() >= 30 ? CJSShim_node_20_11 : CJSShim_normal
return { return {
name: 'vite:esm-shim', name: 'vite:esm-shim',
apply: 'build', apply: 'build',
enforce: 'post', enforce: 'post',
configResolved(config): void { renderChunk(code, _chunk, { format, sourcemap }): { code: string; map?: SourceMapInput } | null {
sourcemap = config.build.sourcemap if (format === 'es') {
},
renderChunk(code, _chunk, options): { code: string; map?: SourceMapInput } | null {
if (options.format === 'es') {
if (code.includes(CJSShim) || !CJSyntaxRe.test(code)) { if (code.includes(CJSShim) || !CJSyntaxRe.test(code)) {
return null return null
} }

View File

@ -7,7 +7,9 @@ export interface ExternalOptions {
} }
/** /**
* Automatically externalize dependencies * Automatically externalize dependencies.
*
* @deprecated use `build.externalizeDeps` config option instead
*/ */
export function externalizeDepsPlugin(options: ExternalOptions = {}): Plugin | null { export function externalizeDepsPlugin(options: ExternalOptions = {}): Plugin | null {
const { exclude = [], include = [] } = options const { exclude = [], include = [] } = options

View File

@ -0,0 +1,198 @@
/* 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[] | { [x: 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 = 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 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, shouldWatch, shouldLog, transformedCount)
const outputChunks = re.bundles.output
for (const chunk of outputChunks) {
if (assetCache.has(chunk.fileName)) {
continue
}
this.emitFile({
type: 'asset',
fileName: chunk.fileName,
source: chunk.type === 'chunk' ? chunk.code : chunk.source
})
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(-1)
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,
shouldLog: boolean,
preTransformedCount: number
): Promise<{ bundles: RollupOutput; watchFiles: string[]; transformedCount: number }> {
const transformReporter = transformReporterPlugin(preTransformedCount, shouldLog)
const buildReporter = watch ? buildReporterPlugin() : undefined
const viteConfig = mergeConfig(config, {
build: {
write: false,
watch: false
},
plugins: [transformReporter, buildReporter],
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: buildReporter?.api?.getWatchFiles() || [],
transformedCount: transformReporter?.api?.getTransformedCount() || 0
}
}
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

@ -1,58 +1,74 @@
import type { Plugin } from 'vite' import path from 'node:path'
import type { SourceMapInput } from 'rollup' import { type Plugin, type InlineConfig, build as viteBuild, mergeConfig } from 'vite'
import type { SourceMapInput, RollupOutput, OutputOptions } from 'rollup'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { cleanUrl, parseRequest, toRelativePath } from '../utils' import buildReporterPlugin from './buildReporter'
import { cleanUrl, toRelativePath } from '../utils'
import { supportImportMetaPaths } from '../electron'
const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g
/** /**
* Resolve `?modulePath` import and return the module bundle path. * Resolve `?modulePath` import and return the module bundle path.
*/ */
export default function modulePathPlugin(): Plugin { export default function modulePathPlugin(config: InlineConfig): Plugin {
let sourcemap: boolean | 'inline' | 'hidden' = false const isImportMetaPathSupported = supportImportMetaPaths()
const assetCache = new Set<string>()
return { return {
name: 'vite:module-path', name: 'vite:module-path',
apply: 'build', apply: 'build',
enforce: 'pre', enforce: 'pre',
configResolved(config): void { buildStart(): void {
sourcemap = config.build.sourcemap assetCache.clear()
}, },
resolveId(id, importer): string | void { async load(id): Promise<string | void> {
const query = parseRequest(id) if (id.endsWith('?modulePath')) {
if (query && typeof query.modulePath === 'string') { // id resolved by Vite resolve plugin
return id + `&importer=${importer}` const re = await bundleEntryFile(cleanUrl(id), config, this.meta.watchMode)
} const [outputChunk, ...outputChunks] = re.bundles.output
},
load(id): string | void {
const query = parseRequest(id)
if (query && typeof query.modulePath === 'string' && typeof query.importer === 'string') {
const cleanPath = cleanUrl(id)
const hash = this.emitFile({ const hash = this.emitFile({
type: 'chunk', type: 'asset',
id: cleanPath, fileName: outputChunk.fileName,
importer: query.importer source: outputChunk.code
}) })
for (const chunk of outputChunks) {
if (assetCache.has(chunk.fileName)) {
continue
}
this.emitFile({
type: 'asset',
fileName: chunk.fileName,
source: chunk.type === 'chunk' ? chunk.code : chunk.source
})
assetCache.add(chunk.fileName)
}
for (const id of re.watchFiles) {
this.addWatchFile(id)
}
const refId = `__VITE_MODULE_PATH__${hash}__` const refId = `__VITE_MODULE_PATH__${hash}__`
const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname'
return ` return `
import { join } from 'path' import { join } from 'path'
export default join(__dirname, ${refId})` export default join(${dirnameExpr}, ${refId})`
} }
}, },
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null { renderChunk(code, chunk, { sourcemap }): { code: string; map: SourceMapInput } | null {
if (code.match(modulePathRE)) { let match: RegExpExecArray | null
let match: RegExpExecArray | null let s: MagicString | undefined
const s = new MagicString(code)
while ((match = modulePathRE.exec(code))) { modulePathRE.lastIndex = 0
const [full, hash] = match while ((match = modulePathRE.exec(code))) {
const filename = this.getFileName(hash) s ||= new MagicString(code)
const outputFilepath = toRelativePath(filename, chunk.fileName) const [full, hash] = match
const replacement = JSON.stringify(outputFilepath) const filename = this.getFileName(hash)
s.overwrite(match.index, match.index + full.length, replacement, { const outputFilepath = toRelativePath(filename, chunk.fileName)
contentOnly: true const replacement = JSON.stringify(outputFilepath)
}) s.overwrite(match.index, match.index + full.length, replacement, {
} contentOnly: true
})
}
if (s) {
return { return {
code: s.toString(), code: s.toString(),
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
@ -63,3 +79,45 @@ export default function modulePathPlugin(): Plugin {
} }
} }
} }
async function bundleEntryFile(
input: string,
config: InlineConfig,
watch: boolean
): Promise<{ bundles: RollupOutput; watchFiles: string[] }> {
const reporter = watch ? buildReporterPlugin() : undefined
const viteConfig = mergeConfig(config, {
build: {
write: false,
watch: false
},
plugins: [
{
name: 'vite:entry-file-name',
outputOptions(output): OutputOptions {
if (typeof output.entryFileNames !== 'function' && output.entryFileNames) {
output.entryFileNames = '[name]-[hash]' + path.extname(output.entryFileNames)
}
return output
}
},
reporter
],
logLevel: 'warn',
configFile: false
}) as InlineConfig
// rewrite the input instead of merging
const buildOptions = viteConfig.build!
buildOptions.rollupOptions = {
...buildOptions.rollupOptions,
input
}
const bundles = await viteBuild(viteConfig)
return {
bundles: bundles as RollupOutput,
watchFiles: reporter?.api?.getWatchFiles() || []
}
}

View File

@ -1,58 +1,58 @@
import type { Plugin } from 'vite' import type { Plugin } from 'vite'
import type { SourceMapInput } from 'rollup' import type { SourceMapInput } from 'rollup'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { cleanUrl, parseRequest, toRelativePath } from '../utils' import { cleanUrl, toRelativePath } from '../utils'
const nodeWorkerAssetUrlRE = /__VITE_NODE_WORKER_ASSET__([\w$]+)__/g const nodeWorkerAssetUrlRE = /__VITE_NODE_WORKER_ASSET__([\w$]+)__/g
const nodeWorkerRE = /\?nodeWorker(?:&|$)/
const nodeWorkerImporterRE = /(?:\?)nodeWorker&importer=([^&]+)(?:&|$)/
/** /**
* Resolve `?nodeWorker` import and automatically generate `Worker` wrapper. * Resolve `?nodeWorker` import and automatically generate `Worker` wrapper.
*/ */
export default function workerPlugin(): Plugin { export default function workerPlugin(): Plugin {
let sourcemap: boolean | 'inline' | 'hidden' = false
return { return {
name: 'vite:node-worker', name: 'vite:node-worker',
apply: 'build', apply: 'build',
enforce: 'pre', enforce: 'pre',
configResolved(config): void {
sourcemap = config.build.sourcemap
},
resolveId(id, importer): string | void { resolveId(id, importer): string | void {
const query = parseRequest(id) if (id.endsWith('?nodeWorker')) {
if (query && typeof query.nodeWorker === 'string') {
return id + `&importer=${importer}` return id + `&importer=${importer}`
} }
}, },
load(id): string | void { load(id): string | void {
const query = parseRequest(id) if (nodeWorkerRE.test(id)) {
if (query && typeof query.nodeWorker === 'string' && typeof query.importer === 'string') { const match = nodeWorkerImporterRE.exec(id)
const cleanPath = cleanUrl(id) if (match) {
const hash = this.emitFile({ const hash = this.emitFile({
type: 'chunk', type: 'chunk',
id: cleanPath, id: cleanUrl(id),
importer: query.importer importer: match[1]
}) })
const assetRefId = `__VITE_NODE_WORKER_ASSET__${hash}__` const assetRefId = `__VITE_NODE_WORKER_ASSET__${hash}__`
return ` return `
import { Worker } from 'node:worker_threads'; import { Worker } from 'node:worker_threads';
export default function (options) { return new Worker(new URL(${assetRefId}, import.meta.url), options); }` export default function (options) { return new Worker(new URL(${assetRefId}, import.meta.url), options); }`
}
} }
}, },
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null { renderChunk(code, chunk, { sourcemap }): { code: string; map: SourceMapInput } | null {
if (code.match(nodeWorkerAssetUrlRE)) { let match: RegExpExecArray | null
let match: RegExpExecArray | null let s: MagicString | undefined
const s = new MagicString(code)
while ((match = nodeWorkerAssetUrlRE.exec(code))) { nodeWorkerAssetUrlRE.lastIndex = 0
const [full, hash] = match while ((match = nodeWorkerAssetUrlRE.exec(code))) {
const filename = this.getFileName(hash) s ||= new MagicString(code)
const outputFilepath = toRelativePath(filename, chunk.fileName) const [full, hash] = match
const replacement = JSON.stringify(outputFilepath) const filename = this.getFileName(hash)
s.overwrite(match.index, match.index + full.length, replacement, { const outputFilepath = toRelativePath(filename, chunk.fileName)
contentOnly: true const replacement = JSON.stringify(outputFilepath)
}) s.overwrite(match.index, match.index + full.length, replacement, {
} contentOnly: true
})
}
if (s) {
return { return {
code: s.toString(), code: s.toString(),
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null

View File

@ -13,5 +13,5 @@ export async function preview(inlineConfig: InlineConfig = {}, options: { skipBu
startElectron(inlineConfig.root) startElectron(inlineConfig.root)
logger.info(colors.green(`\nstart electron app...\n`)) logger.info(colors.green(`\nstarting electron app...\n`))
} }

View File

@ -31,22 +31,20 @@ export async function createServer(
const mainViteConfig = config.config?.main const mainViteConfig = config.config?.main
if (mainViteConfig && !options.rendererOnly) { if (mainViteConfig && !options.rendererOnly) {
const watchHook = (): void => { const watchHook = (): void => {
logger.info(colors.green(`\nrebuild the electron main process successfully`)) logger.info(colors.green(`\nelectron main process rebuilt successfully`))
if (ps) { if (ps) {
logger.info(colors.cyan(`\n waiting for electron to exit...`))
ps.removeAllListeners() ps.removeAllListeners()
ps.kill() ps.kill()
ps = startElectron(inlineConfig.root) ps = startElectron(inlineConfig.root)
logger.info(colors.green(`\nrestart electron app...`)) logger.info(colors.green(`\nrestarting electron app...\n`))
} }
} }
await doBuild(mainViteConfig, watchHook, errorHook) await doBuild(mainViteConfig, watchHook, errorHook)
logger.info(colors.green(`\nbuild the electron main process successfully`)) logger.info(colors.green(`\nelectron main process built successfully`))
} }
const preloadViteConfig = config.config?.preload const preloadViteConfig = config.config?.preload
@ -54,10 +52,10 @@ export async function createServer(
logger.info(colors.gray(`\n-----\n`)) logger.info(colors.gray(`\n-----\n`))
const watchHook = (): void => { const watchHook = (): void => {
logger.info(colors.green(`\nrebuild the electron preload files successfully`)) logger.info(colors.green(`\nelectron preload scripts rebuilt successfully`))
if (server) { if (server) {
logger.info(colors.cyan(`\n trigger renderer reload`)) logger.info(colors.cyan(`\nreloading electron renderer...\n`))
server.ws.send({ type: 'full-reload' }) server.ws.send({ type: 'full-reload' })
} }
@ -65,14 +63,12 @@ export async function createServer(
await doBuild(preloadViteConfig, watchHook, errorHook) await doBuild(preloadViteConfig, watchHook, errorHook)
logger.info(colors.green(`\nbuild the electron preload files successfully`)) logger.info(colors.green(`\nelectron preload scripts built successfully`))
} }
if (options.rendererOnly) { if (options.rendererOnly) {
logger.warn( logger.warn(
`\n${colors.yellow(colors.bold('warn'))}:${colors.yellow( `\n${colors.yellow(colors.bold('(!)'))} ${colors.yellow('skipped building main process and preload scripts (using previous build)')}`
' you have skipped the main process and preload scripts building'
)}`
) )
} }
@ -106,7 +102,7 @@ export async function createServer(
ps = startElectron(inlineConfig.root) ps = startElectron(inlineConfig.root)
logger.info(colors.green(`\nstart electron app...\n`)) logger.info(colors.green(`\nstarting electron app...\n`))
} }
} }

View File

@ -1,4 +1,4 @@
import { URL, URLSearchParams } from 'node:url' /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
@ -20,16 +20,11 @@ export const hashRE = /#.*$/s
export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '') export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '')
export function parseRequest(id: string): Record<string, string> | null {
const { search } = new URL(id, 'file:')
if (!search) {
return null
}
return Object.fromEntries(new URLSearchParams(search))
}
export function getHash(text: Buffer | string): string { export function getHash(text: Buffer | string): string {
return createHash('sha256').update(text).digest('hex').substring(0, 8) return createHash('sha256')
.update(text as unknown as Uint8Array)
.digest('hex')
.substring(0, 8)
} }
export function toRelativePath(filename: string, importer: string): string { export function toRelativePath(filename: string, importer: string): string {
@ -84,3 +79,44 @@ export function isFilePathESM(filePath: string): boolean {
return pkg?.type === 'module' return pkg?.type === 'module'
} }
} }
type DeepWritable<T> =
T extends ReadonlyArray<unknown>
? { -readonly [P in keyof T]: DeepWritable<T[P]> }
: T extends RegExp
? RegExp
: T[keyof T] extends Function
? T
: { -readonly [P in keyof T]: DeepWritable<T[P]> }
export function deepClone<T>(value: T): DeepWritable<T> {
if (Array.isArray(value)) {
return value.map(v => deepClone(v)) as DeepWritable<T>
}
if (isObject(value)) {
const cloned: Record<string, any> = {}
for (const key in value) {
cloned[key] = deepClone(value[key])
}
return cloned as DeepWritable<T>
}
if (typeof value === 'function') {
return value as DeepWritable<T>
}
if (value instanceof RegExp) {
return new RegExp(value) as DeepWritable<T>
}
if (typeof value === 'object' && value != null) {
throw new Error('Cannot deep clone non-plain object')
}
return value as DeepWritable<T>
}
type AsyncFlatten<T extends unknown[]> = T extends (infer U)[] ? Exclude<Awaited<U>, U[]>[] : never
export async function asyncFlatten<T extends unknown[]>(arr: T): Promise<AsyncFlatten<T>> {
do {
arr = (await Promise.all(arr)).flat(Infinity) as any
} while (arr.some((v: any) => v?.then))
return arr as unknown[] as AsyncFlatten<T>
}

View File

@ -1,13 +1,13 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2023",
"module": "ESNext", "module": "ESNext",
"lib": ["ESNext"], "lib": ["ESNext"],
"sourceMap": false, "sourceMap": false,
"strict": true, "strict": true,
"allowJs": true, "allowJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"moduleResolution": "Node", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"skipLibCheck": true, "skipLibCheck": true,