Compare commits

..

199 Commits

Author SHA1 Message Date
alex8088
23d7bfc8ba release: v6.0.0 2026-10-15 00:01:19 +08:00
alex8088
2ca5e40433 release: v6.0.0-beta.8 2026-10-11 02:23:37 +08:00
alex8088
bffc6c95df perf(swcPlugin): skip redundant minification when using Oxc (#922) 2026-10-11 02:13:14 +08:00
alex8088
b0b33bdd5e fix(asset): use consistent file paths for asset cache lookups 2026-10-11 02:07:32 +08:00
alex8088
3ec79bbf65 perf: skip Babel loading in development and builds without bytecode 2026-10-11 02:01:18 +08:00
alex8088
19050aaec9 refactor: share and normalize build output resolution 2026-10-11 00:59:44 +08:00
alex8088
0655933704 refactor: inline bytecode build plugin name 2026-10-11 00:37:36 +08:00
alex8088
6fc103eb5b perf: short-circuit bytecode detection in builds 2026-10-10 23:00:29 +08:00
alex8088
6b0f85a2a7 chore: simplify bytecode compiler initialization 2026-10-10 02:03:25 +08:00
alex8088
fa199eb372 release: v6.0.0-beta.7 2026-10-08 02:30:56 +08:00
alex8088
b66840e2ec fix(bytecode): unable to obtain the bytecode build session in Node.js 24 (#928) 2026-10-08 02:30:44 +08:00
alex8088
1cbef3fb80 release: v6.0.0-beta.6 2026-10-07 13:41:25 +08:00
alex8088
36a0147a6d refactor!: invoke the bytecode compiler only once to compile all code 2026-10-07 13:36:10 +08:00
alex8088
4c4bdf5248 refactor: use pipe stdio for spawn Electron bytecode compliler and append stderr error 2026-10-01 22:44:18 +08:00
alex8088
395f0dae0c chore: improve jsdoc for bytecode options (#898) 2026-10-01 22:31:29 +08:00
alex8088
22b12bd102 release: v6.0.0-beta.5 2026-09-29 23:47:02 +08:00
alex8088
c104b2d15b fix(bytecode): set the regular expression's lastIndex to 0 to perform a global match for chunkAlias option 2026-09-29 23:45:48 +08:00
Beans
c047b58d5d
feat(bytecode): support RegExp for chunkAlias option (#885)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-29 23:25:21 +08:00
alex8088
55239df0ee fix(dev): improve Electron process signal handling 2026-09-29 23:23:03 +08:00
Iddo Gino
d8e7702dc5
fix(dev): forward termination signals to Electron child (#900)
Spawn the Electron binary directly in dev/preview mode but forward
SIGINT/SIGTERM/SIGHUP from the electron-vite parent to the child,
matching Electron's own CLI wrapper (node_modules/electron/cli.js).

Without this, Ctrl-C sends SIGINT to the foreground process group once:
the Node parent exits immediately (no handler) and the Electron child
gets only a single SIGINT. On macOS, an Electron app whose before-quit
handler calls preventDefault() and later calls app.quit() can stall in
AppKit's event loop in that path. Under a direct 'electron .' launch
the CLI shim forwards SIGINT as well, so Electron gets it twice and
exits cleanly. Matching that behavior here fixes the hang.

Fixes #899
2026-09-29 11:55:31 +08:00
Praveens-Pepper-AI
816c6dac61
fix(dev): exit non-zero when Electron is killed by a signal (#918)
`ChildProcess`'s `close` event fires as `(code, signal)` and `code` is null when the
child was terminated by a signal. `process.exit` ignores its second argument and
coerces null to 0, so passing the listener directly reports every signal death as a
successful exit.

Handle the two arguments explicitly so a signal death exits non-zero.
2026-09-29 10:33:35 +08:00
alex8088
98d30e57e3 release: v6.0.0-beta.4 2026-09-28 22:59:51 +08:00
alex8088
a135566fa5 fix(esmShim)!: use this.parse to find the last static import (#906) 2026-09-28 22:53:13 +08:00
alex8088
08acca3585 release: v6.0.0-beta.3 2026-09-27 18:40:44 +08:00
alex8088
68b80f0ad5 fix(deps)!: downgraded @babel/* to v7 due to Node requirements 2026-09-27 18:39:07 +08:00
alex8088
bc1ef5b023 release: v6.0.0-beta.2 2026-09-27 17:59:44 +08:00
alex8088
53bc35b240 fix(deps)!: update @babel/* to v8 2026-09-27 17:51:29 +08:00
alex8088
a7f76d8d44 fix(deps)!: update magic-string to v1 2026-09-25 01:26:57 +08:00
alex8088
2937574aa4 chore(deps): update lint-staged to v17 2026-09-25 01:12:37 +08:00
alex8088
3680d60b21 chore(deps): update all non-major dependencies 2026-09-25 01:08:22 +08:00
alex8088
8df70754ca perf: build compatibility target for Electron 42, 43, 44 2026-09-25 00:44:14 +08:00
alex8088
e691efbd82 refactor: compile bytecode using the Electron main process or renderer process rather then via the ELECTRON_RUN_AS_NODE 2026-09-25 00:39:47 +08:00
alex8088
ac2cdd1a21 fix: clone config without plugins #902 2026-09-24 19:30:27 +08:00
alex8088
811a9f2a75 refactor: use rolldown instead of esbuild to bundle config file 2026-09-24 19:18:50 +08:00
edenbuilds
608461c04c
fix(swcPlugin): also disable oxc on Vite 8 (#920)
Vite 8 defaults transform to Oxc; esbuild:false alone is ignored and
warns. SWC should disable both. Fixes #916.

Co-authored-by: edenbuilds <279970382+edenbuilds@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 13:10:42 +08:00
alex8088
31965d2972 perf: setup rollupOption compatibility via config factory 2026-04-18 01:03:04 +08:00
alex8088
4bede21c81 fix: remove useless preset config 2026-04-18 00:56:23 +08:00
alex8088
31bfc4886b release: v6.0.0-beta.1 2026-04-12 16:36:22 +08:00
alex8088
371b650d71 fix: chunk filename is wrong in ES mode 2026-04-12 16:24:56 +08:00
alex8088
5333d5216a fix!: compatible with rollupOptions and rolldownOptions 2026-04-12 16:18:24 +08:00
alex8088
2765eea74d refactor!: simplify resolve config and isolate user config for sub-builds 2026-04-12 02:58:42 +08:00
alex8088
d3095d24ab release: v6.0.0-beta.0 2026-04-09 23:26:25 +08:00
alex8088
a041f42e0b fix(asset): isolate asset caches per environment 2026-04-09 22:30:27 +08:00
alex8088
2b7c4c33bc fix: use the environment api in plugins 2026-04-09 21:51:42 +08:00
alex8088
517d703cbf fix: skip TTY terminal manipulations in CI environments 2026-04-09 00:50:27 +08:00
alex8088
fa84b6d6c3 refactor: ?modulePath sub-build output strategy 2026-04-09 00:38:21 +08:00
alex8088
2f1f389776 fix(deps)!: update vite to v8 (#894) 2026-04-08 02:04:25 +08:00
alex8088
a9a496dc1a fix: Typescript compiler option 'outDir' must be located inside Rollup 'dir' option 2026-03-29 15:22:06 +08:00
alex8088
201eb213ac chore(deps): update cac to v7 2026-03-29 15:20:31 +08:00
alex8088
3d2a979fb1 chore(deps): update globals to v17 2026-03-29 15:05:56 +08:00
alex8088
e58369f975 chore(deps): update eslint and @eslint/js to v10 2026-03-29 15:04:39 +08:00
alex8088
e9289f32f7 chore(deps): update all non-major dependencies 2026-03-29 14:55:56 +08:00
alex8088
78a117d5be perf: build compatibility target for Electron 40, 41 2026-03-21 14:36:18 +08:00
Michal Fudala
5a8bbe3d0e
fix(isolateEntries): handle non-TTY stdout in build reporter (#887)
The `clearLine` and `writeLine` helpers use TTY-only methods (`clearLine`, `cursorTo`,
`moveCursor`) without checking `process.stdout.isTTY`, causing builds to crash in
non-interactive environments like CI or piped output.

See error I got in my CI environment:

```
vite v7.3.1 building ssr environment for production...
✗ Build failed in 7ms
✓ 0 modules transformed.
✗ Build failed in 9ms
error during build:
[vite:isolate-entries] Could not load virtual:isolate-entries:
  [vite:transform-reporter] process.stdout.clearLine is not a function
```
2026-02-16 22:25:35 +08:00
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
alex8088
02e0dff9f4 release: v2.3.0 2024-06-23 22:48:15 +08:00
alex8088
0a02ace008 chore(deps): update esbuild to v0.21 2024-06-22 23:22:41 +08:00
alex8088
e638dcae1b chore(deps): update @typescript-eslint/* to v7 2024-06-22 23:20:01 +08:00
alex8088
3264abc70b chore(deps): update all non-major dependencies 2024-06-22 23:09:09 +08:00
alex8088
19b42225f8 perf: improve cjs shim 2024-06-22 22:28:08 +08:00
alex8088
d9aaf24f84 fix: default mode should not overrite user config mode 2024-06-21 01:03:01 +08:00
Krystian Otto
3605aca1e8
fix: not using the mode from the config file (#539) 2024-06-21 00:49:24 +08:00
Justin Carrus
73dfee5a4f
fix: don't handle module ID that begin with \0 (#530) 2024-06-15 22:03:45 +08:00
alex8088
b3185d7fc5 feat: resolve import.meta.[dirname|filename] to support CommonJS format 2024-06-15 21:49:51 +08:00
alex8088
d79de5abb6 perf: build compatilibity target for Electron 31 2024-06-13 23:45:44 +08:00
alex8088
1838bdbf3e release: v2.2.0 2024-04-23 21:39:16 +08:00
alex8088
ff23f7d44d chore: use rollup-plugin-rm to clean dist 2024-04-21 21:46:06 +08:00
alex8088
a48e12a9df fix(types): narrow down the return type of defineConfig 2024-04-21 21:15:15 +08:00
alex8088
1abedce6c2 refactor(config): defineConfig types 2024-04-21 19:59:46 +08:00
alex8088
c2c655367f chore: fix camelcase typo 2024-04-21 19:04:33 +08:00
alex8088
6170705eae perf: build compatilibity target for Electron 30 2024-04-20 23:50:12 +08:00
alex8088
eeaa7de45c feat: export mergeConfig from vite (#471) 2024-04-20 23:42:45 +08:00
alex8088
f42a9d370f release: v2.1.0 2024-03-03 22:00:56 +08:00
alex8088
08ff86f2c4 feat: easy way to fork processes and use workers 2024-03-03 19:08:48 +08:00
alex8088
0ce505a12f perf(bytecodePlugin): warn that strings cannot be protected when minification is enabled (#417) 2024-03-03 16:45:50 +08:00
alex8088
ad891af811 fix: config via build.lib fails when default entry point not found (#393) 2024-03-01 23:06:09 +08:00
alex8088
27ade03acf chore: format 2024-03-01 22:33:56 +08:00
Luke Hagar
52fce25787
perf: allow integrating more complex render solutions (#412) 2024-02-24 01:01:26 +08:00
alex8088
19489a28c8 perf: build compatilibity target for Electron 29 2024-02-24 00:31:41 +08:00
alex8088
7f13ea2a84 release: v2.0.0 2024-01-09 00:05:04 +08:00
alex8088
932fc556f5 release: v2.0.0-beta.4 2024-01-06 23:57:05 +08:00
alex8088
a12646f25e fix: electorn's export subpaths also need to be externalized #372 2024-01-06 11:33:53 +08:00
alex8088
921aa9d4a7 release: v2.0.0-beta.3 2024-01-04 00:05:06 +08:00
alex8088
694e134a52 feat: config file supports "type": "module" in package.json 2024-01-04 00:00:57 +08:00
alex8088
40a1b64639 release: v2.0.0-beta.2 2023-12-19 21:04:29 +08:00
alex8088
476ef54498 chore: format 2023-12-16 17:31:13 +08:00
Mikael Finstad
9b04362e12
feat: support for passing arguments to electron in dev and preview commands (#339) 2023-12-16 17:11:27 +08:00
alex8088
a8f5182d25 perf: loadEnv api also needs to load shared env variables prefixed with VITE_ 2023-12-15 22:14:58 +08:00
alex8088
0c98f33573 perf(externalizeDepsPlugin): use cached package data to improve performance 2023-12-15 21:40:24 +08:00
alex8088
db1128089a release: v2.0.0-beta.1 2023-12-14 21:19:04 +08:00
alex8088
0b265442a1 feat: env variables prefixed with VITE_ will be shared in main process and renderer 2023-12-14 21:14:49 +08:00
alex8088
7b27e684d6 perf: dev error message 2023-12-14 20:49:22 +08:00
alex8088
dbd7b0137b fix: externalizeDepsPlugin not work 2023-12-14 20:46:00 +08:00
alex8088
73f2562172 release: v2.0.0-beta.0 2023-12-13 21:32:06 +08:00
alex8088
cc0df8a8cc perf: package version 2023-12-13 21:21:15 +08:00
alex8088
93e84f6bce perf(worker): ESM syntax 2023-12-13 21:12:43 +08:00
alex8088
619a337c6d perf: resolve import meta url in CommonJS format 2023-12-13 21:10:53 +08:00
alex8088
963f8aba51 fix: emit assets when ssr is enabled 2023-12-13 21:06:40 +08:00
alex8088
082b1331ff chore: ignore .DS_store 2023-12-12 23:19:18 +08:00
alex8088
b578f61f1c refactor: use dynamic import directly 2023-12-12 23:11:36 +08:00
alex8088
5de87d681b refactor: remove Electron 11, 12 build compatilibity target 2023-12-12 23:00:03 +08:00
alex8088
a7b3c65576 perf: build compatilibity target for Electron 28 2023-12-12 22:58:50 +08:00
alex8088
4924e36d74 perf: use magic-string hires boundary for sourcemaps 2023-12-12 22:55:55 +08:00
alex8088
61eb32aa04 chore: update homepage 2023-12-12 22:44:41 +08:00
alex8088
c1e1ca0471 feat: support ESM in Electron 2023-12-12 22:40:37 +08:00
alex8088
cb58ef8649 perf: improve package.json resolve 2023-12-11 22:06:22 +08:00
alex8088
93763597fe refactor: file hashes use url-safe base64 encoded hashes in vite 5 (rollup 4) 2023-12-11 21:59:31 +08:00
alex8088
dc87a798ab feat: add package.json to export map 2023-12-11 21:39:11 +08:00
alex8088
4f12196e54 feat: support vite 5 2023-12-08 22:09:49 +08:00
alex8088
b93a1878c1 feat: migrate to ESM 2023-12-08 22:03:47 +08:00
alex8088
8bb03f8b24 feat: bump minimum node version to 18 2023-12-08 21:35:06 +08:00
alex8088
f951edf492 chore: improve prettier config 2023-12-07 22:35:38 +08:00
alex8088
7c6020e11b refactor: build 2023-12-07 22:32:59 +08:00
alex8088
4e48537b39 chore(deps): update typescript to 5.3.3 2023-12-07 21:46:19 +08:00
alex8088
10099eced0 build: use rollup-plugin-dts 2023-12-07 21:43:19 +08:00
alex8088
fbc45f696b chore(deps): update esbuild to v0.19 2023-12-07 21:24:19 +08:00
alex8088
d30d3765eb chore(deps): update vite to v5 2023-12-07 21:22:47 +08:00
alex8088
5c4c28ca0a chore(deps): update rollup to v4 2023-12-07 21:20:27 +08:00
alex8088
5e31794c2d chore(deps): update @rollup/plugin-typescript to v11 2023-12-07 21:10:48 +08:00
alex8088
588b39b0b4 chore(deps): update eslint-config-prettier to v9 2023-12-07 21:03:24 +08:00
alex8088
ce56ecd4dc chore(deps): update lint-staged to v15 2023-12-07 21:01:04 +08:00
alex8088
5021496f74 chore(deps): update all non-major dependencies 2023-12-07 20:58:42 +08:00
42 changed files with 4761 additions and 3538 deletions

View File

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

View File

@ -1,38 +0,0 @@
module.exports = {
root: true,
env: {
commonjs: true,
es6: true,
node: true
},
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaVersion: 2021
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:prettier/recommended'
],
rules: {
'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

3
.gitignore vendored
View File

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

11
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View File

@ -1,3 +1,277 @@
### v6.0.0 (_2026-10-15_)
- feat(bytecode): support RegExp for chunkAlias option ([#885](https://github.com/alex8088/electron-vite/issues/885))
- refactor: share and normalize build output resolution
- refactor: inline bytecode build plugin name
- refactor!: invoke the bytecode compiler only once to compile all code
- refactor: use pipe stdio for spawn Electron bytecode compliler and append stderr error
- refactor: use rolldown instead of esbuild to bundle config file
- refactor: compile bytecode using the Electron main process or renderer process rather then via the `ELECTRON_RUN_AS_NODE`
- refactor!: simplify resolve config and isolate user config for sub-builds
- refactor: `?modulePath` sub-build output strategy
- perf(swcPlugin): skip redundant minification when using Oxc ([#922](https://github.com/alex8088/electron-vite/issues/922))
- perf: skip Babel loading in development and builds without bytecode
- perf: short-circuit bytecode detection in builds
- perf: setup rollupOption compatibility via config factory
- perf: build compatibility target for Electron 42, 43, 44
- fix(asset): use consistent file paths for asset cache lookups
- fix(bytecode): unable to obtain the bytecode build session in Node.js 24 ([#928](https://github.com/alex8088/electron-vite/issues/928))
- fix(dev): exit non-zero when Electron is killed by a signal ([#918](https://github.com/alex8088/electron-vite/issues/918))
- fix(dev): forward termination signals to Electron child ([#900](https://github.com/alex8088/electron-vite/issues/900))
- fix(dev): improve Electron process signal handling
- fix(bytecode): set the regular expression's `lastIndex` to 0 to perform a global match for chunkAlias option
- fix(esmShim)!: use `this.parse` to find the last static import ([#906](https://github.com/alex8088/electron-vite/issues/906))
- fix(deps)!: downgraded @babel/* to v7 due to Node requirements
- fix: remove useless preset config
- fix(swcPlugin): also disable oxc on Vite 8 ([#920](https://github.com/alex8088/electron-vite/issues/920))
- fix: clone config without plugins [#902](https://github.com/alex8088/electron-vite/issues/902)
- fix(deps)!: update magic-string to v1
- fix(deps)!: update @babel/* to v8
- fix!: compatible with rollupOptions and rolldownOptions
- fix: chunk filename is wrong in ES mode
- fix(asset): isolate asset caches per environment
- fix: use the environment api in plugins
- fix: skip TTY terminal manipulations in CI environments
- fix(deps)!: update vite to v8 ([#894](https://github.com/alex8088/electron-vite/issues/894))
- fix: Typescript compiler option 'outDir' must be located inside Rollup 'dir' option
- fix(isolateEntries): handle non-TTY stdout in build reporter (#887)
- chore: simplify bytecode compiler initialization
- chore: improve jsdoc for bytecode options ([#898](https://github.com/alex8088/electron-vite/issues/898))
- chore(deps): update all non-major dependencies
- chore(deps): update lint-staged to v17
- chore(deps): update cac to v7
- chore(deps): update globals to v17
- chore(deps): update eslint and @eslint/js to v10
- chore(deps): update all non-major dependencies
### v6.0.0-beta.8 (_2026-10-11_)
See [v6.0.0-beta.8 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.8/CHANGELOG.md)
### v6.0.0-beta.7 (_2026-10-08_)
See [v6.0.0-beta.7 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.7/CHANGELOG.md)
### v6.0.0-beta.6 (_2026-10-07_)
See [v6.0.0-beta.6 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.6/CHANGELOG.md)
### v6.0.0-beta.5 (_2026-09-29_)
See [v6.0.0-beta.5 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.5/CHANGELOG.md)
### v6.0.0-beta.4 (_2026-09-28_)
See [v6.0.0-beta.4 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.4/CHANGELOG.md)
### v6.0.0-beta.3 (_2026-09-27_)
See [v6.0.0-beta.3 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.3/CHANGELOG.md)
### v6.0.0-beta.2 (_2026-09-27_)
See [v6.0.0-beta.2 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.2/CHANGELOG.md)
### v6.0.0-beta.1 (_2026-04-12_)
See [v6.0.0-beta.1 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.1/CHANGELOG.md)
### v6.0.0-beta.0 (_2026-04-09_)
See [v6.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.0/CHANGELOG.md)
### 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_)
- feat: resolve import.meta.\[dirname|filename\] to support CommonJS format
- fix: don't handle module ID that begin with \0 ([#530](https://github.com/alex8088/electron-vite/pull/530))
- fix: not using the mode from the config file ([#539](https://github.com/alex8088/electron-vite/pull/539))
- fix: default mode should not overrite user config mode
- perf: build compatilibity target for Electron 31
- perf: improve cjs shim
- chore(deps): update all non-major dependencies
- chore(deps): update @typescript-eslint/\* to v7
- chore(deps): update esbuild to v0.21
### v2.2.0 (_2024-04-21_)
- feat: export mergeConfig from vite ([#471](https://github.com/alex8088/electron-vite/issues/471))
- fix(types): narrow down the return type of defineConfig
- perf: build compatilibity target for Electron 30
- refactor(config): defineConfig types
- chore: fix camelcase typo
- chore: use rollup-plugin-rm to clean dist
### v2.1.0 (_2024-03-03_)
- feat: easy way to fork processes and use workers
- fix: config via build.lib fails when default entry point not found ([#393](https://github.com/alex8088/electron-vite/issues/393))
- perf: build compatilibity target for Electron 29
- perf: allow integrating more complex render solutions ([#412](https://github.com/alex8088/electron-vite/pull/412))
- perf(bytecodePlugin): warn that strings cannot be protected when minification is enabled ([#417](https://github.com/alex8088/electron-vite/issues/417))
### v2.0.0 (_2024-01-09_)
- feat: bump minimum node version to 18
- feat: migrate to ESM
- feat: support vite 5
- feat: add package.json to export map
- feat: support ESM in Electron
- feat: env variables prefixed with VITE\_ will be shared in main process and renderer
- feat: support for passing arguments to electron in dev and preview commands ([#339](https://github.com/alex8088/electron-vite/pull/339))
- feat: config file supports "type": "module" in package.json
- fix: emit assets when ssr is enabled
- fix: externalizeDepPlugin not work
- fix: electron's export subpaths also need to be externalized ([#372](https://github.com/alex8088/electron-vite/issues/372))
- perf: improve package.json resolve
- perf: use magic-string hires boundary for sourcemaps
- perf: build compatilibity target for Electron 28
- pref: resolve import meta url in CommonJS format
- perf(worker): ESM syntax
- perf: package version
- perf: dev error message
- perf(externalizeDepsPlugin): use cached package data to improve performance
- perf: loadEnv api also needs to load shared env variables prefixed with VITE\_
- refactor: build
- refactor: file hashes use url-safe base64 encoded hashes in vite 5 (rollup 4)
- refactor: remove Electron 11, 12 build compatilibity target
- refactor: use dynamic import directly
- build: use rollup-plugin-dts
- chore(deps): update all non-major dependencies
- chore(deps): update lint-staged to v15
- chore(deps): update eslint-config-prettier to v9
- chore(deps): update @rollup/plugin-typescript to v11
- chore(deps): update rollup to v4
- chore(deps): update vite to v5
- chore(deps): update esbuild to v0.19
- chore(deps): update typescript to 5.3.3
- chore: improve prettier config
- chore: update homepage
### v2.0.0-beta.4 (_2024-01-06_)
See [v2.0.0-beta.4 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.4/CHANGELOG.md)
### v2.0.0-beta.3 (_2024-01-04_)
See [v2.0.0-beta.3 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.3/CHANGELOG.md)
### v2.0.0-beta.2 (_2023-12-19_)
See [v2.0.0-beta.2 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.2/CHANGELOG.md)
### v2.0.0-beta.1 (_2023-12-14_)
See [v2.0.0-beta.1 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.1/CHANGELOG.md)
### v2.0.0-beta.0 (_2023-12-13_)
See [v2.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.0/CHANGELOG.md)
### v1.0.29 (_2023-11-17_) ### v1.0.29 (_2023-11-17_)
- feat(cli): support --noSandbox option for dev and preview command - feat(cli): support --noSandbox option for dev and preview command

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

@ -1,52 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "./dist/types/index.d.ts",
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "",
"publicTrimmedFilePath": "./dist/index.d.ts"
},
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": false
},
"tsdocMetadata": {
"enabled": false
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "warning",
"addToApiReportFile": true
},
"ae-missing-release-tag": {
"logLevel": "none"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
},
"tsdoc-undefined-tag": {
"logLevel": "none"
}
}
}
}

15
bin/bytecode-compiler.cjs Normal file
View File

@ -0,0 +1,15 @@
const vm = require('node:vm')
const v8 = require('node:v8')
v8.setFlagsFromString('--no-lazy')
v8.setFlagsFromString('--no-flush-bytecode')
const params = ['exports', 'require', 'module', '__filename', '__dirname']
module.exports = function compile(code) {
if (typeof code !== 'string') throw new TypeError('Compilation source must be a string')
const { cachedData } = vm.compileFunction(code, params, { produceCachedData: true })
if (!Buffer.isBuffer(cachedData)) throw new Error('V8 did not produce cached data')
// Explicit wire format works across both Node IPC and Electron structured clone.
return { sourceLength: code.length, cachedData: cachedData.toString('base64') }
}

View File

@ -0,0 +1,109 @@
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')
const RpcPeer = require('./rpc-peer.cjs')
const compile = require('./bytecode-compiler.cjs')
if (process.platform === 'darwin') {
app.dock.hide()
}
const channel = 'electron-vite:bytecode:rpc'
const readyChannel = 'electron-vite:bytecode:ready'
let win
let rendererRPC
let rendererStarting
let stopping = false
function fail(error) {
console.error(error)
parentRPC.close(error)
rendererRPC?.close(error)
app.exit(1)
}
const parentRPC = new RpcPeer(
message =>
new Promise((resolve, reject) => {
if (!process.connected || !process.send) return reject(new Error('Parent IPC is disconnected'))
process.send(message, error => (error ? reject(error) : resolve()))
})
)
function createRenderer() {
return new Promise((resolve, reject) => {
let ready = false
const timer = setTimeout(() => finish(new Error('Compiler preload readiness timed out')), 30000)
function finish(error) {
clearTimeout(timer)
ipcMain.removeListener(readyChannel, onReady)
if (error) reject(error)
else {
ready = true
resolve()
}
}
function onReady(event) {
if (event.sender === win.webContents) finish()
}
function onFailure(error) {
if (stopping) return
if (ready) fail(error)
else finish(error)
}
ipcMain.on(readyChannel, onReady)
win = new BrowserWindow({
show: false,
webPreferences: {
preload: path.join(__dirname, 'electron-bytecode-preload.cjs'),
sandbox: false,
contextIsolation: true
}
})
rendererRPC = new RpcPeer(message => {
if (win.isDestroyed()) throw new Error('Compiler window is closed')
win.webContents.send(channel, message)
})
ipcMain.on(channel, (event, message) => {
if (!win.isDestroyed() && event.sender === win.webContents) void rendererRPC.receive(message)
})
win.webContents.on('preload-error', (_event, _path, error) => onFailure(error))
win.webContents.on('render-process-gone', (_event, details) => {
onFailure(new Error(`Compiler renderer exited: ${details.reason}`))
})
win.on('closed', () => onFailure(new Error('Compiler window closed unexpectedly')))
win.loadURL('data:text/html,<!doctype html><html></html>').catch(onFailure)
})
}
const initialized = app.whenReady().then(() => true)
parentRPC.register('ready', () => initialized)
parentRPC.register('compile', async (target, code) => {
await initialized
if (target === 'main') return compile(code)
if (target === 'preload') {
rendererStarting ||= createRenderer()
await rendererStarting
return rendererRPC.request('compile', code)
}
throw new Error(`Invalid bytecode target: ${target}`)
})
initialized.catch(fail)
process.on('message', message => {
if (message?.type === 'shutdown') {
stopping = true
app.quit()
} else {
void parentRPC.receive(message)
}
})
process.on('disconnect', () => {
stopping = true
app.quit()
})
app.on('before-quit', () => {
stopping = true
parentRPC.close()
rendererRPC?.close()
})
if (!process.send) fail(new Error('Bytecode compiler must be launched with an IPC channel'))

View File

@ -0,0 +1,10 @@
const { ipcRenderer } = require('electron')
const RpcPeer = require('./rpc-peer.cjs')
const compile = require('./bytecode-compiler.cjs')
const channel = 'electron-vite:bytecode:rpc'
const rpc = new RpcPeer(message => ipcRenderer.send(channel, message))
rpc.register('ready', () => true)
rpc.register('compile', compile)
ipcRenderer.on(channel, (_event, message) => void rpc.receive(message))
ipcRenderer.send('electron-vite:bytecode:ready')

View File

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

89
bin/rpc-peer.cjs Normal file
View File

@ -0,0 +1,89 @@
// Transport-independent peer. Messages must be JSON serializable.
class RpcPeer {
constructor(send, timeout = 30000) {
this.send = send
this.timeout = timeout
this.methods = new Map()
this.pending = new Map()
this.nextId = 0
this.closed = false
}
register(cmd, method) {
this.methods.set(cmd, method)
return this
}
request(cmd, ...args) {
if (this.closed) return Promise.reject(new Error('RPC connection is closed'))
const invocationId = this.nextId++
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.delete(invocationId)
reject(new Error(`RPC request timed out: ${cmd}`))
}, this.timeout)
this.pending.set(invocationId, { resolve, reject, timer })
Promise.resolve()
.then(() => this.send({ type: 'request', invocationId, cmd, args }))
.catch(error => {
const pending = this.pending.get(invocationId)
if (!pending) return
clearTimeout(timer)
this.pending.delete(invocationId)
reject(error)
})
})
}
async receive(message) {
if (this.closed || !message || !Number.isSafeInteger(message.invocationId)) return
const { type, invocationId } = message
if (type === 'response') {
const pending = this.pending.get(invocationId)
if (!pending) return
this.pending.delete(invocationId)
clearTimeout(pending.timer)
if (message.error) {
const error = new Error(message.error.message)
error.name = message.error.name || 'Error'
if (message.error.stack) error.stack = message.error.stack
pending.reject(error)
} else {
pending.resolve(message.result)
}
return
}
if (type !== 'request' || typeof message.cmd !== 'string' || !Array.isArray(message.args)) return
const response = { type: 'response', invocationId }
try {
const method = this.methods.get(message.cmd)
if (!method) throw new Error(`Invalid method: ${message.cmd}`)
response.result = await method(...message.args)
} catch (error) {
response.error = {
name: error instanceof Error ? error.name : 'Error',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
}
}
if (!this.closed) {
try {
await this.send(response)
} catch (error) {
this.close(error)
}
}
}
close(error = new Error('RPC connection is closed')) {
this.closed = true
for (const pending of this.pending.values()) {
clearTimeout(pending.timer)
pending.reject(error)
}
this.pending.clear()
this.methods.clear()
}
}
module.exports = RpcPeer

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'
}
}
)

8
node.d.ts vendored
View File

@ -1,9 +1,15 @@
// 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
} }
// module path
declare module '*?modulePath' {
const src: string
export default src
}
// node asset // node asset
declare module '*?asset' { declare module '*?asset' {
const src: string const src: string

View File

@ -1,9 +1,17 @@
{ {
"name": "electron-vite", "name": "electron-vite",
"version": "1.0.29", "version": "6.0.0",
"description": "Electron build tooling based on Vite", "description": "Electron build tooling based on Vite",
"main": "dist/index.js", "type": "module",
"types": "dist/index.d.ts", "main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./node": {
"types": "./node.d.ts"
},
"./package.json": "./package.json"
},
"bin": { "bin": {
"electron-vite": "bin/electron-vite.js" "electron-vite": "bin/electron-vite.js"
}, },
@ -13,9 +21,9 @@
"node.d.ts" "node.d.ts"
], ],
"engines": { "engines": {
"node": "^14.18.0 || >=16.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": {
@ -25,7 +33,7 @@
"bugs": { "bugs": {
"url": "https://github.com/alex8088/electron-vite/issues" "url": "https://github.com/alex8088/electron-vite/issues"
}, },
"homepage": "https://github.com/alex8088/electron-vite#readme", "homepage": "https://electron-vite.org",
"keywords": [ "keywords": [
"electron", "electron",
"vite", "vite",
@ -34,9 +42,9 @@
], ],
"scripts": { "scripts": {
"format": "prettier --write .", "format": "prettier --write .",
"lint": "eslint --ext .ts src/**", "lint": "eslint --cache .",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"build": "npm run lint && node scripts/build.js" "build": "pnpm run lint && rollup -c rollup.config.ts --configPlugin typescript"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"pre-commit": "npx lint-staged", "pre-commit": "npx lint-staged",
@ -53,7 +61,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@swc/core": "^1.0.0", "@swc/core": "^1.0.0",
"vite": "^3.0.0 || ^4.0.0" "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@swc/core": { "@swc/core": {
@ -61,31 +69,40 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@microsoft/api-extractor": "^7.36.1", "@eslint/js": "^10.0.1",
"@rollup/plugin-node-resolve": "^15.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-typescript": "^9.0.2", "@rollup/plugin-node-resolve": "^16.0.3",
"@swc/core": "^1.3.68", "@rollup/plugin-typescript": "^12.3.0",
"@types/node": "^18.16.19", "@swc/core": "^1.16.2",
"@typescript-eslint/eslint-plugin": "^6.2.0", "@types/babel__core": "^7.20.5",
"@typescript-eslint/parser": "^6.2.0", "@types/node": "^22.20.4",
"eslint": "^8.44.0", "eslint": "^10.11.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.5.6",
"fs-extra": "^11.1.1", "globals": "^17.12.0",
"lint-staged": "^13.2.3", "lint-staged": "^17.5.1",
"prettier": "^3.0.0", "prettier": "^3.9.9",
"rollup": "^3.26.2", "rollup": "^4.63.5",
"simple-git-hooks": "^2.8.1", "rollup-plugin-dts": "^6.5.1",
"tslib": "^2.6.0", "rollup-plugin-rm": "^1.0.2",
"typescript": "5.0.4", "simple-git-hooks": "^2.14.0",
"vite": "^4.4.2" "tslib": "^2.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.70.1",
"vite": "^8.3.1"
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.22.8", "@babel/core": "^7.29.7",
"@babel/plugin-transform-arrow-functions": "^7.22.5", "@babel/plugin-transform-arrow-functions": "^7.29.7",
"cac": "^6.7.14", "cac": "^7.0.0",
"esbuild": "^0.18.11", "magic-string": "^1.4.2",
"magic-string": "^0.30.1", "picocolors": "^1.1.1",
"picocolors": "^1.0.0" "rolldown": "~1.2.10"
},
"pnpm": {
"onlyBuiltDependencies": [
"@swc/core",
"simple-git-hooks"
]
} }
} }

4312
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

42
rollup.config.ts Normal file
View File

@ -0,0 +1,42 @@
import { createRequire } from 'node:module'
import { defineConfig } from 'rollup'
import ts from '@rollup/plugin-typescript'
import resolve from '@rollup/plugin-node-resolve'
import json from '@rollup/plugin-json'
import dts from 'rollup-plugin-dts'
import rm from 'rollup-plugin-rm'
const require = createRequire(import.meta.url)
const pkg = require('./package.json')
const external = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]
export default defineConfig([
{
input: ['src/index.ts', 'src/cli.ts'],
output: [
{
dir: 'dist',
entryFileNames: '[name].js',
chunkFileNames: 'chunks/lib-[hash].js',
format: 'es'
}
],
external,
plugins: [
rm('dist', 'buildStart'),
json(),
ts({ compilerOptions: { rootDir: 'src', outDir: 'dist', declaration: true, declarationDir: 'dist/types' } }),
resolve()
],
treeshake: {
moduleSideEffects: false
}
},
{
input: 'dist/types/index.d.ts',
external: ['vite'],
output: [{ file: pkg.types, format: 'es' }],
plugins: [dts(), rm('dist/types', 'buildEnd')]
}
])

View File

@ -1,67 +0,0 @@
const path = require('path')
const colors = require('picocolors')
const fs = require('fs-extra')
const rollup = require('rollup')
const typescript = require('@rollup/plugin-typescript')
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const { Extractor, ExtractorConfig } = require('@microsoft/api-extractor')
;(async () => {
const dist = path.resolve(__dirname, '../dist')
await fs.remove(dist)
console.log()
console.log(colors.bold(colors.yellow(`Rolling up ts code...`)))
const pkg = require('../package.json')
const external = ['esbuild', ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]
const bundle = await rollup.rollup({
input: {
index: path.resolve(__dirname, '../src/index.ts'),
cli: path.resolve(__dirname, '../src/cli.ts')
},
external,
plugins: [
typescript({
tsconfig: path.resolve(__dirname, '../tsconfig.json')
}),
nodeResolve()
],
treeshake: {
moduleSideEffects: false
}
})
await bundle.write({
dir: dist,
entryFileNames: '[name].js',
chunkFileNames: 'chunks/lib-[hash].js',
format: 'cjs'
})
console.log(colors.bold(colors.yellow(`Rolling up type definitions...`)))
if (pkg.types) {
const extractorConfig = ExtractorConfig.loadFileAndPrepare(path.resolve(__dirname, '../api-extractor.json'))
const extractorResult = Extractor.invoke(extractorConfig, {
localBuild: true,
showVerboseMessages: true
})
if (extractorResult.succeeded) {
console.log(colors.green('API Extractor completed successfully'))
} else {
console.error(
`API Extractor completed with ${extractorResult.errorCount} errors` +
` and ${extractorResult.warningCount} warnings`
)
process.exitCode = 1
}
}
await fs.remove(path.resolve(dist, 'types'))
console.log(colors.green(`Build ${pkg.name}@${pkg.version} successfully`))
})()

View File

@ -1,13 +1,13 @@
// Invoked on the commit-msg git hook by simple-git-hooks. // Invoked on the commit-msg git hook by simple-git-hooks.
const colors = require('picocolors') import colors from 'picocolors'
const fs = require('fs') import fs from 'node:fs'
const msgPath = process.argv[2] 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,33 +1,56 @@
import { build as viteBuild } from 'vite' import { build as viteBuild, mergeConfig } from 'vite'
import { InlineConfig, resolveConfig } from './config' import { type InlineConfig, type MainViteConfig, type PreloadViteConfig, resolveConfig } from './config'
import { BytecodeCompiler } from './bytecodeCompiler'
import { asyncFlatten } from './utils'
async function usesBytecode(config?: MainViteConfig | PreloadViteConfig): Promise<boolean> {
if (!config) return false
if (config.build?.bytecode) return true
const plugins = await asyncFlatten(config.plugins || [])
return plugins.some(plugin => plugin && typeof plugin === 'object' && plugin.name === 'vite:bytecode')
}
/** /**
* Bundles the electron app for production. * Bundles the electron app for production.
*/ */
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')
if (config.config) { const bytecodeCompiler = new BytecodeCompiler()
const mainViteConfig = config.config?.main const config = await resolveConfig(
if (mainViteConfig) { mergeConfig(inlineConfig, {
if (mainViteConfig.build?.watch) { plugins: [{ name: 'vite:electron-bytecode-build', api: { compiler: bytecodeCompiler } }]
mainViteConfig.build.watch = null }),
'build',
'production'
)
if (!config.config) {
return
}
const hasBytecode = (await usesBytecode(config.config.main)) || (await usesBytecode(config.config.preload))
const buildTargets = async (): Promise<void> => {
// Build targets in order: main -> preload -> renderer
for (const target of ['main', 'preload', 'renderer'] as const) {
const viteConfig = config.config?.[target]
if (viteConfig) {
// Disable watch mode in production builds
if (viteConfig.build?.watch) {
viteConfig.build.watch = null
}
await viteBuild(viteConfig)
} }
await viteBuild(mainViteConfig)
}
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)
} }
} }
if (!hasBytecode) return buildTargets()
try {
await bytecodeCompiler.start()
await buildTargets()
} finally {
await bytecodeCompiler.stop()
}
} }

188
src/bytecodeCompiler.ts Normal file
View File

@ -0,0 +1,188 @@
import { type ChildProcess, spawn } from 'node:child_process'
import path from 'node:path'
import { createRequire } from 'node:module'
import { getElectronPath } from './electron'
const require = createRequire(import.meta.url)
const RPC_TIMEOUT = 30_000
const SHUTDOWN_TIMEOUT = 3_000
export type BytecodeCompileResult = {
sourceLength: number
/** Base64-encoded V8 cachedData; decode with Buffer.from(cachedData, 'base64'). */
cachedData: string
}
export type BytecodeTarget = 'main' | 'preload'
type CompilerResponse = {
type: 'response'
invocationId: number
result?: unknown
error?: { name: string; message: string; stack?: string }
}
type PendingRequest = {
resolve: (value: unknown) => void
reject: (reason: Error) => void
timer: ReturnType<typeof setTimeout>
}
export class BytecodeCompiler {
private ps?: ChildProcess
private rpcMap = new Map<number, PendingRequest>()
private invocationId = 0
private starting?: Promise<void>
private stopping?: Promise<void>
private ready = false
private stderr = ''
start(): Promise<void> {
if (this.stopping) return Promise.reject(new Error('BytecodeCompiler is stopping'))
if (this.starting) return this.starting
if (this.ready) return Promise.resolve()
if (this.ps) return Promise.reject(new Error('Previous Electron process has not closed; await stop() first'))
this.starting = this.initialize().finally(() => {
this.starting = undefined
})
return this.starting
}
private async initialize(): Promise<void> {
const env: NodeJS.ProcessEnv = { ...process.env, ELECTRON_VITE_RPC: 'true' }
delete env.ELECTRON_RUN_AS_NODE
this.stderr = ''
const compilerPath = path.join(
path.dirname(require.resolve('electron-vite/package.json')),
'bin',
'electron-bytecode-main.cjs'
)
const ps = spawn(getElectronPath(), [compilerPath], {
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
windowsHide: true,
env
})
this.ps = ps
if (ps.stderr) {
ps.stderr.setEncoding('utf8')
ps.stderr.on('data', (chunk: string) => {
if (this.ps === ps) this.stderr = (this.stderr + chunk).slice(-64 * 1024)
})
}
ps.on('message', (message: CompilerResponse) => {
if (this.ps !== ps || !message || message.type !== 'response') return
const pending = this.rpcMap.get(message.invocationId)
if (!pending) return
clearTimeout(pending.timer)
this.rpcMap.delete(message.invocationId)
if (message.error) {
const error = new Error(message.error.message)
error.name = message.error.name
if (message.error.stack) error.stack = message.error.stack
pending.reject(error)
} else pending.resolve(message.result)
})
ps.on('error', error => {
if (this.ps === ps) this.rejectPending(error)
})
ps.on('disconnect', () => {
if (this.ps === ps) this.rejectPending(new Error('Electron IPC disconnected'))
})
ps.on('exit', (code, signal) => {
if (this.ps === ps) this.rejectPending(new Error('Electron exited (code ' + code + ', signal ' + signal + ')'))
})
ps.on('close', () => {
if (this.ps === ps) {
this.rejectPending(new Error('Electron process closed'))
this.ps = undefined
}
})
try {
await this.sendRequest('ready', [])
if (!ps.connected || this.stopping) throw new Error('Electron stopped during initialization')
this.ready = true
} catch (error) {
await this.stop()
throw error
}
}
compile(target: BytecodeTarget, code: string): Promise<BytecodeCompileResult> {
return this.request<BytecodeCompileResult>('compile', target, code)
}
async request<T = unknown>(cmd: string, ...args: unknown[]): Promise<T> {
if (!this.ready || this.stopping) throw new Error('BytecodeCompiler is not ready; await start() first')
return this.sendRequest(cmd, args) as Promise<T>
}
private sendRequest(cmd: string, args: unknown[]): Promise<unknown> {
const ps = this.ps
if (!ps?.connected) return Promise.reject(new Error('Electron IPC is not connected'))
const invocationId = this.invocationId++
return new Promise((resolve, reject) => {
const fail = (error: Error): void => {
const pending = this.rpcMap.get(invocationId)
if (!pending) return
clearTimeout(pending.timer)
this.rpcMap.delete(invocationId)
reject(this.withStderr(error))
}
const timer = setTimeout(() => fail(new Error('Electron RPC timed out: ' + cmd)), RPC_TIMEOUT)
this.rpcMap.set(invocationId, { resolve, reject, timer })
try {
ps.send({ type: 'request', invocationId, cmd, args }, error => {
if (error) fail(error)
})
} catch (error) {
fail(error instanceof Error ? error : new Error(String(error)))
}
})
}
private rejectPending(error: Error): void {
this.ready = false
if (this.rpcMap.size) this.withStderr(error)
for (const pending of this.rpcMap.values()) {
clearTimeout(pending.timer)
pending.reject(error)
}
this.rpcMap.clear()
}
private withStderr(error: Error): Error {
const stderr = this.stderr.trim()
if (stderr) {
const stack = error.stack
error.message += '\n' + stderr
if (stack) error.stack = stack + '\n' + stderr
}
return error
}
stop(): Promise<void> {
if (this.stopping) return this.stopping
this.rejectPending(new Error('BytecodeCompiler stopped'))
const ps = this.ps
if (!ps) return Promise.resolve()
this.stopping = new Promise<void>(resolve => {
const timer = setTimeout(() => ps.kill('SIGKILL'), SHUTDOWN_TIMEOUT)
ps.once('close', () => {
clearTimeout(timer)
resolve()
})
if (ps.connected) {
try {
ps.send({ type: 'shutdown' }, error => {
if (error) ps.kill()
})
} catch {
ps.kill()
}
} else ps.kill()
}).finally(() => {
this.stopping = undefined
})
return this.stopping
}
}

View File

@ -1,7 +1,8 @@
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'
const cli = cac('electron-vite') const cli = cac('electron-vite')
@ -96,6 +97,10 @@ cli
process.env.NO_SANDBOX = '1' process.env.NO_SANDBOX = '1'
} }
if (options['--']) {
process.env.ELECTRON_CLI_ARGS = JSON.stringify(options['--'])
}
if (options.entry) { if (options.entry) {
process.env.ELECTRON_ENTRY = options.entry process.env.ELECTRON_ENTRY = options.entry
} }
@ -150,6 +155,10 @@ cli
process.env.ELECTRON_ENTRY = options.entry process.env.ELECTRON_ENTRY = options.entry
} }
if (options['--']) {
process.env.ELECTRON_CLI_ARGS = JSON.stringify(options['--'])
}
try { try {
await preview(inlineConfig, { skipBuild: options.skipBuild }) await preview(inlineConfig, { skipBuild: options.skipBuild })
} catch (e) { } catch (e) {
@ -160,6 +169,6 @@ cli
}) })
cli.help() cli.help()
cli.version(require('../package.json').version) cli.version(version)
cli.parse() cli.parse()

View File

@ -1,77 +1,124 @@
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs'
import { pathToFileURL } from 'node:url'
import { createRequire } from 'node:module'
import colors from 'picocolors' import colors from 'picocolors'
import { import {
type UserConfig as ViteConfig, type UserConfig as ViteConfig,
type UserConfigExport as UserViteConfigExport,
type ConfigEnv, type ConfigEnv,
type PluginOption,
type Plugin, type Plugin,
type BuildEnvironmentOptions as ViteBuildOptions,
type LogLevel, type LogLevel,
createLogger, createLogger,
mergeConfig, mergeConfig,
normalizePath normalizePath
} from 'vite' } from 'vite'
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 { isObject, dynamicImport } from './utils' import importMetaPlugin from './plugins/importMeta'
import esmShimPlugin from './plugins/esmShim'
import modulePathPlugin from './plugins/modulePath'
import isolateEntriesPlugin from './plugins/isolateEntries'
import { type ExternalOptions, externalizeDepsPlugin } from './plugins/externalizeDeps'
import { type BytecodeOptions, bytecodePlugin } from './plugins/bytecode'
import { isObject, isFilePathESM, cloneConfig, asyncFlatten } from './utils'
import { findConfigFile, bundleConfigFile, loadConfigFormBundledFile } from './load'
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
*
* Compiles source code to v8 bytecode and works only in the `build` command.
*/
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 UserConfigSchema { 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?: UserViteConfigExport
/**
* Vite config options for electron renderer process
*
* https://vitejs.dev/config/
*/
renderer?: UserViteConfigExport
/**
* Vite config options for electron preload files
*
* https://vitejs.dev/config/
*/
preload?: UserViteConfigExport
}
export type InlineConfig = Omit<ViteConfig, 'base'> & { export type ElectronViteConfigExport =
configFile?: string | false UserConfig | Promise<UserConfig> | ElectronViteConfigFnObject | ElectronViteConfigFnPromise | ElectronViteConfigFn
envFile?: false
ignoreConfigWarning?: boolean
}
export type UserConfigFn = (env: ConfigEnv) => UserConfigSchema | Promise<UserConfigSchema>
export type UserConfigExport = UserConfigSchema | Promise<UserConfigSchema> | UserConfigFn
/** /**
* Type helper to make it easier to use `electron.vite.config.*` * Type helper to make it easier to use `electron.vite.config.*`
@ -79,10 +126,21 @@ export type UserConfigExport = UserConfigSchema | Promise<UserConfigSchema> | Us
* 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: UserConfigExport): UserConfigExport { export function defineConfig(config: UserConfig): UserConfig
export function defineConfig(config: Promise<UserConfig>): Promise<UserConfig>
export function defineConfig(config: ElectronViteConfigFnObject): ElectronViteConfigFnObject
export function defineConfig(config: ElectronViteConfigFnPromise): ElectronViteConfigFnPromise
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
@ -97,11 +155,10 @@ export async function resolveConfig(
const config = inlineConfig const config = inlineConfig
const mode = inlineConfig.mode || defaultMode const mode = inlineConfig.mode || defaultMode
config.mode = mode
process.env.NODE_ENV = defaultMode process.env.NODE_ENV = defaultMode
let userConfig: UserConfig | undefined const userConfig: UserConfig | undefined = {}
let configFileDependencies: string[] = [] let configFileDependencies: string[] = []
let { configFile } = config let { configFile } = config
@ -110,6 +167,7 @@ export async function resolveConfig(
mode, mode,
command command
} }
const loadResult = await loadConfigFromFile( const loadResult = await loadConfigFromFile(
configEnv, configEnv,
configFile, configFile,
@ -117,52 +175,30 @@ 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) { const { main, preload, renderer } = loadResult.config
const mainViteConfig: ViteConfig = mergeConfig(loadResult.config.main, deepClone(config))
if (outDir) { if (main) {
resetOutDir(mainViteConfig, outDir, 'main') userConfig.main = await new MainConfigFactory(main, config, { outDir, root }).build()
}
mergePlugins(mainViteConfig, [...electronMainVitePlugin({ root }), assetPlugin(), workerPlugin()])
loadResult.config.main = mainViteConfig
loadResult.config.main.configFile = false
} }
if (loadResult.config.preload) { if (preload) {
const preloadViteConfig: ViteConfig = mergeConfig(loadResult.config.preload, deepClone(config)) userConfig.preload = await new PreloadConfigFactory(preload, config, { outDir, root }).build()
if (outDir) {
resetOutDir(preloadViteConfig, outDir, 'preload')
}
mergePlugins(preloadViteConfig, [...electronPreloadVitePlugin({ root }), assetPlugin()])
loadResult.config.preload = preloadViteConfig
loadResult.config.preload.configFile = false
} }
if (loadResult.config.renderer) { if (renderer) {
const rendererViteConfig: ViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config)) userConfig.renderer = await new RendererConfigFactory(renderer, config, { outDir, root }).build()
if (outDir) {
resetOutDir(rendererViteConfig, outDir, 'renderer')
}
mergePlugins(rendererViteConfig, electronRendererVitePlugin({ root }))
loadResult.config.renderer = rendererViteConfig
loadResult.config.renderer.configFile = false
} }
userConfig = loadResult.config
configFile = loadResult.path configFile = loadResult.path
configFileDependencies = loadResult.dependencies configFileDependencies = loadResult.dependencies
} }
@ -177,8 +213,109 @@ export async function resolveConfig(
return resolved return resolved
} }
function deepClone<T>(data: T): T { export abstract class ConfigFactory<T extends MainViteConfig | PreloadViteConfig | RendererViteConfig> {
return JSON.parse(JSON.stringify(data)) constructor(
protected readonly baseConfig: T,
protected readonly inlineConfig: InlineConfig,
protected readonly options: { outDir?: string; root?: string }
) {
baseConfig.build ??= {}
baseConfig.build.rolldownOptions ??= baseConfig.build.rollupOptions
}
async build(cleanMode?: boolean): Promise<T> {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const config = mergeConfig(cloneConfig(this.baseConfig) as any, cloneConfig(this.inlineConfig)) as T
config.mode = this.inlineConfig.mode || config.mode || process.env.NODE_ENV
if (this.options.outDir) {
resetOutDir(config, this.options.outDir, this.processType())
}
const builtinPlugins = await this.resolveBuiltinPlugins(config, cleanMode)
config.plugins = builtinPlugins.concat(config.plugins || [])
return config
}
protected abstract processType(): 'main' | 'preload' | 'renderer'
protected abstract resolveBuiltinPlugins(config: T, cleanMode?: boolean): Promise<PluginOption[]>
}
export class MainConfigFactory extends ConfigFactory<MainViteConfig> {
protected processType(): 'main' {
return 'main'
}
protected async resolveBuiltinPlugins(config: MainViteConfig, cleanMode?: boolean): Promise<PluginOption[]> {
const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(config)
return cleanMode
? [
electronMainConfigPresetPlugin({ root: this.options.root }),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
: [
electronMainConfigPresetPlugin({ root: this.options.root }),
electronMainConfigValidatorPlugin(),
assetPlugin(),
workerPlugin(),
modulePathPlugin(this),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
}
}
export class PreloadConfigFactory extends ConfigFactory<PreloadViteConfig> {
protected processType(): 'preload' {
return 'preload'
}
protected async resolveBuiltinPlugins(config: PreloadViteConfig, cleanMode?: boolean): Promise<PluginOption[]> {
const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(config)
return cleanMode
? [
electronPreloadConfigPresetPlugin({ root: this.options.root }),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
: [
electronPreloadConfigPresetPlugin({ root: this.options.root }),
electronPreloadConfigValidatorPlugin(),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins,
...(config.build?.isolatedEntries ? [isolateEntriesPlugin(this)] : [])
]
}
}
export class RendererConfigFactory extends ConfigFactory<RendererViteConfig> {
protected processType(): 'renderer' {
return 'renderer'
}
protected async resolveBuiltinPlugins(config: RendererViteConfig, cleanMode?: boolean): Promise<PluginOption[]> {
return cleanMode
? [electronRendererConfigPresetPlugin({ root: this.options.root })]
: [
electronRendererConfigPresetPlugin({ root: this.options.root }),
electronRendererConfigValidatorPlugin(),
...(config.build?.isolatedEntries ? [isolateEntriesPlugin(this)] : [])
]
}
} }
function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void { function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void {
@ -193,12 +330,37 @@ 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
} }
const CONFIG_FILE_NAME = 'electron.vite.config' function isOptions<T extends object>(value: boolean | T): value is T {
return typeof value === 'object' && value !== null
}
export async function loadConfigFromFile( export async function loadConfigFromFile(
configEnv: ConfigEnv, configEnv: ConfigEnv,
@ -215,9 +377,7 @@ export async function loadConfigFromFile(
throw new Error(`config file cannot be named ${configFile}.`) throw new Error(`config file cannot be named ${configFile}.`)
} }
const resolvedPath = configFile const resolvedPath = configFile ? path.resolve(configFile) : findConfigFile(configRoot)
? path.resolve(configFile)
: findConfigFile(configRoot, ['js', 'ts', 'mjs', 'cjs', 'mts', 'cts'])
if (!resolvedPath) { if (!resolvedPath) {
return { return {
@ -227,186 +387,33 @@ export async function loadConfigFromFile(
} }
} }
// electron does not support adding type: "module" to package.json const isESM = isFilePathESM(resolvedPath)
let isESM = false
if (/\.m[jt]s$/.test(resolvedPath) || resolvedPath.endsWith('.ts')) {
isESM = true
}
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<ElectronViteConfigExport>(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 })
throw e throw e
} }
} }
function findConfigFile(configRoot: string, extensions: string[]): string {
for (const ext of extensions) {
const configFile = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${ext}`)
if (fs.existsSync(configFile)) {
return configFile
}
}
return ''
}
async function bundleConfigFile(fileName: string, isESM: boolean): Promise<{ code: string; dependencies: string[] }> {
const dirnameVarName = '__electron_vite_injected_dirname'
const filenameVarName = '__electron_vite_injected_filename'
const importMetaUrlVarName = '__electron_vite_injected_import_meta_url'
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: [fileName],
write: false,
target: ['node14.18', 'node16'],
platform: 'node',
bundle: true,
format: isESM ? 'esm' : 'cjs',
sourcemap: false,
metafile: true,
define: {
__dirname: dirnameVarName,
__filename: filenameVarName,
'import.meta.url': importMetaUrlVarName
},
plugins: [
{
name: 'externalize-deps',
setup(build): void {
build.onResolve({ filter: /.*/ }, args => {
const id = args.path
if (id[0] !== '.' && !path.isAbsolute(id)) {
return {
external: true
}
}
return null
})
}
},
{
name: 'replace-import-meta',
setup(build): void {
build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async args => {
const contents = await fs.promises.readFile(args.path, 'utf8')
const injectValues =
`const ${dirnameVarName} = ${JSON.stringify(path.dirname(args.path))};` +
`const ${filenameVarName} = ${JSON.stringify(args.path)};` +
`const ${importMetaUrlVarName} = ${JSON.stringify(pathToFileURL(args.path).href)};`
return {
loader: args.path.endsWith('ts') ? 'ts' : 'js',
contents: injectValues + contents
}
})
}
}
]
})
const { text } = result.outputFiles[0]
return {
code: text,
dependencies: result.metafile ? Object.keys(result.metafile.inputs) : []
}
}
interface NodeModuleWithCompile extends NodeModule {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
_compile(code: string, filename: string): any
}
const _require = createRequire(import.meta.url)
async function loadConfigFormBundledFile(
configRoot: string,
configFile: string,
bundledCode: string,
isESM: boolean
): Promise<UserConfigExport> {
if (isESM) {
const fileNameTmp = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${Date.now()}.mjs`)
fs.writeFileSync(fileNameTmp, bundledCode)
const fileUrl = pathToFileURL(fileNameTmp)
try {
return (await dynamicImport(fileUrl)).default
} finally {
try {
fs.unlinkSync(fileNameTmp)
} catch {}
}
} else {
const extension = path.extname(configFile)
const realFileName = fs.realpathSync(configFile)
const loaderExt = extension in _require.extensions ? extension : '.js'
const defaultLoader = _require.extensions[loaderExt]!
_require.extensions[loaderExt] = (module: NodeModule, filename: string): void => {
if (filename === realFileName) {
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
} else {
defaultLoader(module, filename)
}
}
delete _require.cache[_require.resolve(configFile)]
const raw = _require(configFile)
_require.extensions[loaderExt] = defaultLoader
return raw.__esModule ? raw.default : raw
}
}

View File

@ -2,18 +2,18 @@ import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { type ChildProcess, spawn } from 'node:child_process' import { type ChildProcess, spawn } from 'node:child_process'
import { loadPackageData } from './utils'
const _require = createRequire(import.meta.url) const _require = createRequire(import.meta.url)
const ensureElectronEntryFile = (root = process.cwd()): void => { const ensureElectronEntryFile = (root = process.cwd()): void => {
if (process.env.ELECTRON_ENTRY) return if (process.env.ELECTRON_ENTRY) return
const pkg = path.join(root, 'package.json') const pkg = loadPackageData()
if (fs.existsSync(pkg)) { if (pkg) {
const main = require(pkg).main if (!pkg.main) {
if (!main) {
throw new Error('No entry point found for electron app, please add a "main" field to package.json') throw new Error('No entry point found for electron app, please add a "main" field to package.json')
} else { } else {
const entryPath = path.resolve(root, main) const entryPath = path.resolve(root, pkg.main)
if (!fs.existsSync(entryPath)) { if (!fs.existsSync(entryPath)) {
throw new Error(`No electron app entry file found: ${entryPath}`) throw new Error(`No electron app entry file found: ${entryPath}`)
} }
@ -36,6 +36,16 @@ const getElectronMajorVer = (): string => {
return majorVer return majorVer
} }
export function supportESM(): boolean {
const majorVer = getElectronMajorVer()
return parseInt(majorVer) >= 28
}
export function supportImportMetaPaths(): boolean {
const majorVer = getElectronMajorVer()
return parseInt(majorVer) >= 30
}
export function getElectronPath(): string { export function getElectronPath(): string {
let electronExecPath = process.env.ELECTRON_EXEC_PATH || '' let electronExecPath = process.env.ELECTRON_EXEC_PATH || ''
if (!electronExecPath) { if (!electronExecPath) {
@ -59,23 +69,29 @@ export function getElectronNodeTarget(): string {
const electronVer = getElectronMajorVer() const electronVer = getElectronMajorVer()
const nodeVer = { const nodeVer = {
'44': '24.18',
'43': '24.17',
'42': '24.15',
'41': '24.14',
'40': '24.14',
'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',
'30': '20.11',
'29': '20.9',
'28': '18.18',
'27': '18.17', '27': '18.17',
'26': '18.16', '26': '18.16',
'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',
'12': '14.16',
'11': '12.18'
} }
if (electronVer && parseInt(electronVer) > 10) { if (electronVer && parseInt(electronVer) > 10) {
let target = nodeVer[electronVer] let target = nodeVer[electronVer]
@ -89,23 +105,29 @@ export function getElectronChromeTarget(): string {
const electronVer = getElectronMajorVer() const electronVer = getElectronMajorVer()
const chromeVer = { const chromeVer = {
'44': '152',
'43': '150',
'42': '148',
'41': '146',
'40': '144',
'39': '142',
'38': '140',
'37': '138',
'36': '136',
'35': '134',
'34': '132',
'33': '130',
'32': '128',
'31': '126',
'30': '124',
'29': '122',
'28': '120',
'27': '118', '27': '118',
'26': '116', '26': '116',
'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',
'12': '89',
'11': '87'
} }
if (electronVer && parseInt(electronVer) > 10) { if (electronVer && parseInt(electronVer) > 10) {
let target = chromeVer[electronVer] let target = chromeVer[electronVer]
@ -122,7 +144,7 @@ export function startElectron(root: string | undefined): ChildProcess {
const isDev = process.env.NODE_ENV_ELECTRON_VITE === 'development' const isDev = process.env.NODE_ENV_ELECTRON_VITE === 'development'
const args: string[] = [] const args: string[] = process.env.ELECTRON_CLI_ARGS ? JSON.parse(process.env.ELECTRON_CLI_ARGS) : []
if (!!process.env.REMOTE_DEBUGGING_PORT && isDev) { if (!!process.env.REMOTE_DEBUGGING_PORT && isDev) {
args.push(`--remote-debugging-port=${process.env.REMOTE_DEBUGGING_PORT}`) args.push(`--remote-debugging-port=${process.env.REMOTE_DEBUGGING_PORT}`)
@ -143,7 +165,36 @@ export function startElectron(root: string | undefined): ChildProcess {
const entry = process.env.ELECTRON_ENTRY || '.' const entry = process.env.ELECTRON_ENTRY || '.'
const ps = spawn(electronPath, [entry].concat(args), { stdio: 'inherit' }) const ps = spawn(electronPath, [entry].concat(args), { stdio: 'inherit' })
ps.on('close', process.exit) // `code` is null when the child was terminated by a signal, and `process.exit` ignores
// its second argument, so passing the listener directly would report every crash as a
// successful exit. Map a signal death to a non-zero status instead.
ps.on('close', (code, signal) => process.exit(code ?? (signal ? 1 : 0)))
setupElectronSignalHandlers(ps)
return ps return ps
} }
let electronPs: ChildProcess | undefined
let signalHandlersRegistered = false
function setupElectronSignalHandlers(child: ChildProcess): void {
electronPs = child
if (signalHandlersRegistered) return
const signals: NodeJS.Signals[] =
process.platform === 'win32' ? ['SIGINT', 'SIGTERM'] : ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGUSR2']
for (const signal of signals) {
process.on(signal, () => {
const child = electronPs
if (!child?.pid || child.exitCode !== null || child.signalCode !== null) return
// A previously sent signal does not mean the child has exited.
child.kill(signal)
})
}
signalHandlersRegistered = true
}

View File

@ -1,4 +1,4 @@
export { type LogLevel, createLogger, 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'

130
src/load.ts Normal file
View File

@ -0,0 +1,130 @@
import path from 'node:path'
import fs from 'node:fs'
import { pathToFileURL } from 'node:url'
import { createRequire } from 'node:module'
import { rolldown } from 'rolldown'
import type { RolldownOutput, OutputChunk } from 'rolldown'
const CONFIG_FILE_NAME = 'electron.vite.config'
export function findConfigFile(
configRoot: string,
extensions: string[] = ['js', 'ts', 'mjs', 'cjs', 'mts', 'cts']
): string {
for (const ext of extensions) {
const configFile = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${ext}`)
if (fs.existsSync(configFile)) {
return configFile
}
}
return ''
}
export async function bundleConfigFile(
fileName: string,
isESM: boolean
): Promise<{ code: string; dependencies: string[] }> {
const dirnameVarName = '__electron_vite_injected_dirname'
const filenameVarName = '__electron_vite_injected_filename'
const importMetaUrlVarName = '__electron_vite_injected_import_meta_url'
const bundle = await rolldown({
input: path.resolve(fileName),
platform: 'node',
transform: {
target: 'node20',
define: {
__dirname: dirnameVarName,
__filename: filenameVarName,
'import.meta.url': importMetaUrlVarName
}
},
plugins: [
{
name: 'externalize-deps',
resolveId(id) {
if (id[0] !== '.' && !path.isAbsolute(id)) {
return { id, external: true }
}
return null
}
},
{
name: 'inject-file-scope-variables',
transform: {
filter: { id: /\.[cm]?[jt]s$/ },
handler(code, id) {
const injectValues =
`const ${dirnameVarName} = ${JSON.stringify(path.dirname(id))};` +
`const ${filenameVarName} = ${JSON.stringify(id)};` +
`const ${importMetaUrlVarName} = ${JSON.stringify(pathToFileURL(id).href)};`
return { code: injectValues + code, map: null }
}
}
}
]
})
let result: RolldownOutput
try {
result = await bundle.generate({
format: isESM ? 'esm' : 'cjs',
sourcemap: 'inline',
sourcemapPathTransform(relative, sourcemapPath) {
return path.resolve(path.dirname(sourcemapPath), relative)
},
codeSplitting: false
})
} finally {
await bundle.close()
}
const entryChunk = result.output.find((chunk): chunk is OutputChunk => chunk.type === 'chunk' && chunk.isEntry)
if (!entryChunk) {
throw new Error(`Failed to bundle config file: ${fileName}`)
}
return {
code: entryChunk.code,
dependencies: entryChunk.moduleIds.filter(id => !id.startsWith('\0'))
}
}
interface NodeModuleWithCompile extends NodeModule {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
_compile(code: string, filename: string): any
}
const _require = createRequire(import.meta.url)
export async function loadConfigFormBundledFile<T>(fileName: string, bundledCode: string, isESM: boolean): Promise<T> {
if (isESM) {
const fileNameTmp = `${fileName}.${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`
fs.writeFileSync(fileNameTmp, bundledCode)
const fileUrl = pathToFileURL(fileNameTmp)
try {
return (await import(fileUrl.href)).default
} finally {
try {
fs.unlinkSync(fileNameTmp)
} catch {}
}
} else {
const extension = path.extname(fileName)
const realFileName = fs.realpathSync(fileName)
const loaderExt = extension in _require.extensions ? extension : '.js'
const defaultLoader = _require.extensions[loaderExt]!
_require.extensions[loaderExt] = (module: NodeModule, filename: string): void => {
if (filename === realFileName) {
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
} else {
defaultLoader(module, filename)
}
}
delete _require.cache[_require.resolve(fileName)]
const raw = _require(fileName)
_require.extensions[loaderExt] = defaultLoader
return raw.__esModule ? raw.default : raw
}
}

View File

@ -1,50 +1,17 @@
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import type { SourceMapInput } from 'rollup' import { type Plugin, type Environment, 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 { const nodeAssetRE = /__VITE_NODE_ASSET__([\w$]+)__/g
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__([a-z\d]{8})__/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 = `
@ -58,24 +25,18 @@ export default async function loadWasm(file, importObject = {}) {
} }
` `
const assetCache = new WeakMap<Environment, Map<string, string>>()
const publicAssetPathCache = new WeakMap<Environment, Map<string, string>>()
export default function assetPlugin(): Plugin { export default function assetPlugin(): Plugin {
let sourcemap: boolean | 'inline' | 'hidden' = false const isImportMetaPathSupported = supportImportMetaPaths()
let publicDir = ''
let outDir = ''
const publicAssetPathCache = new Map<string, string>()
const assetCache = new Map<string, string>()
return { return {
name: 'vite:node-asset', name: 'vite:node-asset',
apply: 'build', apply: 'build',
enforce: 'pre', enforce: 'pre',
buildStart(): void { buildStart(): void {
publicAssetPathCache.clear() publicAssetPathCache.set(this.environment, new Map())
assetCache.clear() assetCache.set(this.environment, new Map())
},
configResolved(config): void {
sourcemap = config.build.sourcemap
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,21 +48,26 @@ export default function assetPlugin(): Plugin {
return wasmHelperCode return wasmHelperCode
} }
const assetResolved = resolveAsset(id) if (id.startsWith('\0') || !assetImportRE.test(id)) {
if (!assetResolved) {
return return
} }
let referenceId: string let referenceId: string
const file = assetResolved.file
const file = cleanUrl(id)
const { publicDir } = this.environment.config
if (publicDir && file.startsWith(publicDir)) { if (publicDir && file.startsWith(publicDir)) {
const hash = getHash(file) const hash = getHash(file)
if (!publicAssetPathCache.get(hash)) { const publicAssetPathMap = publicAssetPathCache.get(this.environment)!
publicAssetPathCache.set(hash, file) if (!publicAssetPathMap.get(hash)) {
publicAssetPathMap.set(hash, file)
} }
referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__` referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__`
} else { } else {
const cached = assetCache.get(file) const cache = assetCache.get(this.environment)!
const cached = cache.get(file)
if (cached) { if (cached) {
referenceId = cached referenceId = cached
} else { } else {
@ -109,75 +75,74 @@ 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) cache.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 }) {
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 })
})
}
} }
const publicAssetPathMap = publicAssetPathCache.get(this.environment)!
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 = publicAssetPathMap.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) {
return { return {
code: s.toString(), code: s.toString(),
map: sourcemap ? s.generateMap({ hires: true }) : null map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
} }
} else {
return null
} }
return null
} }
} }
} }

View File

@ -1,155 +1,89 @@
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs' import { randomBytes } from 'node:crypto'
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 Rolldown, normalizePath } from 'vite'
import * as babel from '@babel/core' import type * as Babel from '@babel/core'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import type { SourceMapInput, OutputChunk } from 'rollup' import { toRelativePath, resolveBuildOutputs } from '../utils'
import { getElectronPath } from '../electron' import type { BytecodeCompiler, BytecodeTarget } from '../bytecodeCompiler'
import { toRelativePath } from '../utils'
// Inspired by https://github.com/bytenode/bytenode // Inspired by https://github.com/bytenode/bytenode
const _require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
let babel: typeof Babel | undefined
function getBytecodeCompilerPath(): string { function getBytecodeCompiler(plugins: readonly Plugin[]): BytecodeCompiler | undefined {
return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', 'electron-bytecode.js') const plugin = plugins.find(plugin => plugin.name === 'vite:electron-bytecode-build')
return (plugin?.api as { compiler?: BytecodeCompiler } | undefined)?.compiler
} }
function compileToBytecode(code: string): Promise<Buffer> { // Versioned envelope: magic/version (8), UTF-16 source length (4), module ID (16), V8 cache.
return new Promise((resolve, reject) => { // Keep our metadata separate from the private V8 cache header.
let data = Buffer.from([]) async function compileToBytecode(compiler: BytecodeCompiler, target: BytecodeTarget, code: string): Promise<Buffer> {
const id = randomBytes(16)
const electronPath = getElectronPath() // Reserve enough source space for an untruncated, per-artifact placeholder identity.
const bytecodePath = getBytecodeCompilerPath() const source = code + '\n/*' + id.toString('hex') + '*/'
const result = await compiler.compile(target, source)
const proc = spawn(electronPath, [bytecodePath], { const header = Buffer.alloc(28)
env: { ELECTRON_RUN_AS_NODE: '1' }, header.write('EVBC0001', 0, 'ascii')
stdio: ['pipe', 'pipe', 'pipe', 'ipc'] header.writeUInt32LE(result.sourceLength, 8)
}) id.copy(header, 12)
return Buffer.concat([header, Buffer.from(result.cachedData, 'base64')])
if (proc.stdin) {
proc.stdin.write(code)
proc.stdin.end()
}
if (proc.stdout) {
proc.stdout.on('data', chunk => {
data = Buffer.concat([data, chunk])
})
proc.stdout.on('error', err => {
console.error(err)
})
proc.stdout.on('end', () => {
resolve(data)
})
}
if (proc.stderr) {
proc.stderr.on('data', chunk => {
console.error('Error: ', chunk.toString())
})
proc.stderr.on('error', err => {
console.error('Error: ', err)
})
}
proc.addListener('message', message => console.log(message))
proc.addListener('error', err => console.error(err))
proc.on('error', err => reject(err))
proc.on('exit', () => {
resolve(data)
})
})
} }
const bytecodeModuleLoaderCode = [ const bytecodeModuleLoaderCode = [
`"use strict";`, '"use strict";',
`const fs = require("fs");`, 'const fs = require("node:fs");',
`const path = require("path");`, 'const path = require("node:path");',
`const vm = require("vm");`, 'const vm = require("node:vm");',
`const v8 = require("v8");`, 'const v8 = require("node:v8");',
`const Module = require("module");`, 'const Module = require("node:module");',
`v8.setFlagsFromString("--no-lazy");`, 'v8.setFlagsFromString("--no-lazy");',
`v8.setFlagsFromString("--no-flush-bytecode");`, 'v8.setFlagsFromString("--no-flush-bytecode");',
`const FLAG_HASH_OFFSET = 12;`, 'const params = ["exports", "require", "module", "__filename", "__dirname"];',
`const SOURCE_HASH_OFFSET = 8;`, 'Module._extensions[".jsc"] = Module._extensions[".cjsc"] = function (module, filename) {',
`let dummyBytecode;`, ' const data = fs.readFileSync(filename);',
`function setFlagHashHeader(bytecodeBuffer) {`, ' if (data.length <= 28 || data.toString("ascii", 0, 8) !== "EVBC0001") {',
` if (!dummyBytecode) {`, ' throw new Error("Invalid electron-vite bytecode format; rebuild the application: " + filename);',
` const script = new vm.Script("", {`, ' }',
` produceCachedData: true`, ' const length = data.readUInt32LE(8);',
` });`, ' const tag = "/*" + data.subarray(12, 28).toString("hex") + "*/";',
` dummyBytecode = script.createCachedData();`, ' if (length < tag.length || length > 0x1fffffff) {',
` }`, ' throw new Error("Invalid bytecode source length: " + filename);',
` dummyBytecode.slice(FLAG_HASH_OFFSET, FLAG_HASH_OFFSET + 4).copy(bytecodeBuffer, FLAG_HASH_OFFSET);`, ' }',
`};`, ' const placeholder = tag + " ".repeat(length - tag.length);',
`function getSourceHashHeader(bytecodeBuffer) {`, ' const compiledWrapper = vm.compileFunction(placeholder, params, {',
` return bytecodeBuffer.slice(SOURCE_HASH_OFFSET, SOURCE_HASH_OFFSET + 4);`, ' filename,',
`};`, ' cachedData: data.subarray(28)',
`function buffer2Number(buffer) {`, ' });',
` let ret = 0;`, ' if (compiledWrapper.cachedDataRejected) {',
` ret |= buffer[3] << 24;`, ' throw new Error("Invalid or incompatible cached data (cachedDataRejected): " + filename);',
` ret |= buffer[2] << 16;`, ' }',
` ret |= buffer[1] << 8;`, ' const require = function (id) { return module.require(id); };',
` ret |= buffer[0];`, ' require.resolve = function (request, options) {',
` return ret;`, ' return Module._resolveFilename(request, module, false, options);',
`};`, ' };',
`Module._extensions[".jsc"] = function (module, filename) {`, ' if (process.mainModule) require.main = process.mainModule;',
` const bytecodeBuffer = fs.readFileSync(filename);`, ' require.extensions = Module._extensions;',
` if (!Buffer.isBuffer(bytecodeBuffer)) {`, ' require.cache = Module._cache;',
` throw new Error("BytecodeBuffer must be a buffer object.");`, ' return compiledWrapper.call(module.exports, module.exports, require, module, filename, path.dirname(filename));',
` }`, '};'
` setFlagHashHeader(bytecodeBuffer);`,
` const length = buffer2Number(getSourceHashHeader(bytecodeBuffer));`,
` let dummyCode = "";`,
` if (length > 1) {`,
` dummyCode = "\\"" + "\\u200b".repeat(length - 2) + "\\"";`,
` }`,
` const script = new vm.Script(dummyCode, {`,
` filename: filename,`,
` lineOffset: 0,`,
` displayErrors: true,`,
` cachedData: bytecodeBuffer`,
` });`,
` if (script.cachedDataRejected) {`,
` throw new Error("Invalid or incompatible cached data (cachedDataRejected)");`,
` }`,
` const require = function (id) {`,
` return module.require(id);`,
` };`,
` require.resolve = function (request, options) {`,
` return Module._resolveFilename(request, module, false, options);`,
` };`,
` if (process.mainModule) {`,
` require.main = process.mainModule;`,
` }`,
` require.extensions = Module._extensions;`,
` require.cache = Module._cache;`,
` const compiledWrapper = script.runInThisContext({`,
` filename: filename,`,
` lineOffset: 0,`,
` columnOffset: 0,`,
` displayErrors: true`,
` });`,
` const dirname = path.dirname(filename);`,
` const args = [module.exports, require, module, filename, dirname, process, global];`,
` return compiledWrapper.apply(module.exports, args);`,
`};`
] ]
const bytecodeChunkExtensionRE = /.(jsc|cjsc)$/
export interface BytecodeOptions { export interface BytecodeOptions {
chunkAlias?: string | string[] chunkAlias?: string | string[] | RegExp
transformArrowFunctions?: boolean transformArrowFunctions?: boolean
removeBundleJS?: boolean removeBundleJS?: boolean
protectedStrings?: string[] protectedStrings?: string[]
} }
/** /**
* 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') {
@ -157,91 +91,182 @@ 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 = chunkAlias instanceof RegExp ? 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 isBytecodeChunk = (chunkName: string): boolean => { const isBytecodeChunk = (chunkName: string): boolean => {
return transformAllChunks || _chunkAlias.some(alias => alias === chunkName) if (_chunkAlias instanceof RegExp) {
_chunkAlias.lastIndex = 0
return _chunkAlias.test(chunkName)
}
return _chunkAlias.length === 0 || _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 as Babel.PluginTarget, { protectedStrings: new Set(protectedStrings) }])
}
const shouldTransformBytecodeChunk = plugins.length !== 0
const _transform = (
code: string,
sourceMaps: boolean = false
): { code: string; map?: Rolldown.SourceMapInput } | null => {
babel ??= require('@babel/core') as typeof Babel
const re = babel.transformSync(code, { plugins, sourceMaps })
return re ? { code: re.code || '', map: re.map as Rolldown.SourceMapInput } : null
} }
const useStrict = '"use strict";' const useStrict = '"use strict";'
const bytecodeModuleLoader = 'bytecode-loader.js' const bytecodeModuleLoader = 'bytecode-loader.cjs'
let config: ResolvedConfig let supported = false
let useInRenderer = false let isPreload = false
let bytecodeRequired = false let bytecodeCompiler: BytecodeCompiler | undefined
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 bytecodeCompiler = getBytecodeCompiler(config.plugins)
useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config') if (supported) {
return
}
isPreload = config.plugins.some(p => p.name === 'vite:electron-preload-config-preset')
const useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-config-preset')
if (useInRenderer) { if (useInRenderer) {
config.logger.warn(colors.yellow('bytecodePlugin is not support renderers')) config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.'))
return
}
const build = config.build
const outputs = resolveBuildOutputs(build.rolldownOptions.output, build.lib)
const output = outputs[0]
if (output) {
if (output.format === 'es') {
config.logger.warn(
colors.yellow(
'bytecodePlugin does not support ES module, please remove "type": "module" ' +
'in package.json or set build.rollupOptions.output.format (or build.rolldownOptions.output.format) to "cjs".'
)
)
}
supported = output.format === 'cjs' && !useInRenderer
} }
}, },
transform(code, id): void | { code: string; map: SourceMapInput } { renderChunk(code, chunk, { sourcemap }): { code: string; map?: Rolldown.SourceMapInput } | null {
if (protectedStrings.length === 0 || !filter(id)) return if (supported && isBytecodeChunk(chunk.name) && shouldTransformBytecodeChunk) {
return _transform(code, !!sourcemap)
let match: RegExpExecArray | null
let s: MagicString | undefined
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: true }) : null
}
}
},
renderChunk(code, chunk): { code: string } | null {
if (useInRenderer) {
return null
}
if (chunk.type === 'chunk' && isBytecodeChunk(chunk.name)) {
bytecodeRequired = true
if (transformArrowFunctions) {
return {
code: _transform(code)
}
}
} }
return null return null
}, },
generateBundle(): void { async generateBundle(_, output): Promise<void> {
if (!useInRenderer && bytecodeRequired) { if (!supported) {
return
}
const _chunks = Object.values(output)
const chunks = _chunks.filter(
chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name)
) as Rolldown.OutputChunk[]
if (chunks.length === 0) {
return
}
const bytecodeChunks = chunks.map(chunk => chunk.fileName)
const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName))
const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|')
const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null
const getBytecodeLoaderBlock = (chunkFileName: string): string => {
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
}
let bytecodeChunkCount = 0
const bundles = Object.keys(output)
if (!bytecodeCompiler) {
throw new Error('Bytecode compilation requires an electron-vite build')
}
for (const name of bundles) {
const chunk = output[name]
if (chunk.type === 'chunk') {
let _code = chunk.code
if (bytecodeRE) {
let match: RegExpExecArray | null
let s: MagicString | undefined
while ((match = bytecodeRE.exec(_code))) {
s ||= new MagicString(_code)
const [prefix, chunkName] = match
const len = prefix.length + chunkName.length
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
contentOnly: true
})
}
if (s) {
_code = s.toString()
}
}
if (bytecodeChunks.includes(name)) {
const bytecodeBuffer = await compileToBytecode(bytecodeCompiler, isPreload ? 'preload' : 'main', _code)
this.emitFile({
type: 'asset',
fileName: name + 'c',
source: bytecodeBuffer
})
if (!removeBundleJS) {
this.emitFile({
type: 'asset',
fileName: '_' + chunk.fileName,
source: chunk.code
})
}
if (chunk.isEntry) {
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
chunk.code = code
} else {
delete output[chunk.fileName]
}
bytecodeChunkCount += 1
} else {
if (chunk.isEntry) {
let hasBytecodeMoudle = false
const idsToHandle = new Set([...chunk.imports, ...chunk.dynamicImports])
for (const moduleId of idsToHandle) {
if (bytecodeChunks.includes(moduleId)) {
hasBytecodeMoudle = true
break
}
const moduleInfo = this.getModuleInfo(moduleId)
if (moduleInfo) {
const { importers, dynamicImporters } = moduleInfo
for (const importerId of importers) idsToHandle.add(importerId)
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
}
}
if (hasBytecodeMoudle) {
const loader = getBytecodeLoaderBlock(chunk.fileName)
const strictRE = /^(#![^\n]*\n)?(["'])use strict\2;/
_code = strictRE.test(_code)
? _code.replace(strictRE, `$&\n${loader}`)
: _code.replace(/^(#![^\n]*\n)?/, `$&${loader}\n`)
}
}
chunk.code = _code
}
}
}
if (bytecodeChunkCount && !_chunks.some(ass => ass.type === 'asset' && ass.fileName === bytecodeModuleLoader)) {
this.emitFile({ this.emitFile({
type: 'asset', type: 'asset',
source: bytecodeModuleLoaderCode.join('\n') + '\n', source: bytecodeModuleLoaderCode.join('\n') + '\n',
@ -250,115 +275,85 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
}) })
} }
}, },
async writeBundle(options, output): Promise<void> { writeBundle(_, output): void {
if (useInRenderer || !bytecodeRequired) { if (supported) {
return const bytecodeChunkCount = Object.keys(output).filter(chunk => bytecodeChunkExtensionRE.test(chunk)).length
} this.environment.logger.info(`${colors.green(`✓`)} ${bytecodeChunkCount} chunks compiled into bytecode.`)
}
const outDir = options.dir! }
}
bytecodeFiles = [] }
const bundles = Object.keys(output) interface ProtectStringsPluginState extends Babel.PluginPass {
const chunks = Object.values(output).filter( opts: { protectedStrings: Set<string> }
chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name) && chunk.fileName !== bytecodeModuleLoader }
) as OutputChunk[]
const bytecodeChunks = chunks.map(chunk => chunk.fileName) function protectStringsPlugin(api: typeof Babel & Babel.ConfigAPI): Babel.PluginObj<ProtectStringsPluginState> {
const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName)) const { types: t } = api
const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|') function createFromCharCodeFunction(value: string): Babel.types.CallExpression {
const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null const charCodes = Array.from(value).map(s => s.charCodeAt(0))
const charCodeLiterals = charCodes.map(code => t.numericLiteral(code))
const keepBundle = (chunkFileName: string): void => {
const newFileName = path.resolve(path.dirname(chunkFileName), `_${path.basename(chunkFileName)}`) // String.fromCharCode
fs.renameSync(chunkFileName, newFileName) const memberExpression = t.memberExpression(t.identifier('String'), t.identifier('fromCharCode'))
} // String.fromCharCode(...arr)
const callExpression = t.callExpression(memberExpression, [t.spreadElement(t.identifier('arr'))])
const getBytecodeLoaderBlock = (chunkFileName: string): string => { // return String.fromCharCode(...arr)
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");` const returnStatement = t.returnStatement(callExpression)
} // function (arr) { return ... }
const functionExpression = t.functionExpression(null, [t.identifier('arr')], t.blockStatement([returnStatement]))
await Promise.all(
bundles.map(async name => { // (function(...) { ... })([x, x, x])
const chunk = output[name] return t.callExpression(functionExpression, [t.arrayExpression(charCodeLiterals)])
if (chunk.type === 'chunk') { }
let _code = chunk.code
if (bytecodeRE && _code.match(bytecodeRE)) { return {
let match: RegExpExecArray | null name: 'protect-strings-plugin',
const s = new MagicString(_code) visitor: {
while ((match = bytecodeRE.exec(_code))) { StringLiteral(path, state) {
const [prefix, chunkName] = match // obj['property']
const len = prefix.length + chunkName.length if (path.parentPath.isMemberExpression({ property: path.node, computed: true })) {
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', { return
contentOnly: true }
})
} // { 'key': value }
_code = s.toString() if (path.parentPath.isObjectProperty({ key: path.node, computed: false })) {
} return
const chunkFileName = path.resolve(outDir, name) }
if (bytecodeChunks.includes(name)) {
const bytecodeBuffer = await compileToBytecode(_code) // require('fs')
fs.writeFileSync(path.resolve(outDir, name + 'c'), bytecodeBuffer) if (
if (chunk.isEntry) { path.parentPath.isCallExpression() &&
if (!removeBundleJS) { t.isIdentifier(path.parentPath.node.callee) &&
keepBundle(chunkFileName) path.parentPath.node.callee.name === 'require' &&
} path.parentPath.node.arguments[0] === path.node
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName) ) {
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");` return
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n` }
fs.writeFileSync(chunkFileName, code)
} else { // Only CommonJS is supported, import declaration and export declaration checks are ignored
if (removeBundleJS) {
fs.unlinkSync(chunkFileName) const { value } = path.node
} else { if (state.opts.protectedStrings.has(value)) {
keepBundle(chunkFileName) path.replaceWith(createFromCharCodeFunction(value))
} }
} },
bytecodeFiles.push({ name: name + 'c', size: bytecodeBuffer.length }) TemplateLiteral(path, state) {
} else { // Must be a pure static template literal
if (chunk.isEntry) { // expressions must be empty (no ${variables})
let hasBytecodeMoudle = false // quasis must have only one element (meaning the entire string is a single static part).
const idsToHandle = new Set([...chunk.imports, ...chunk.dynamicImports]) if (path.node.expressions.length > 0 || path.node.quasis.length !== 1) {
for (const moduleId of idsToHandle) { return
if (bytecodeChunks.includes(moduleId)) { }
hasBytecodeMoudle = true
break // Extract the raw value of the template literal
} // path.node.quasis[0].value.raw is used to get the raw string, including escape sequences
const moduleInfo = this.getModuleInfo(moduleId) // path.node.quasis[0].value.cooked is used to get the processed/cooked string (with escape sequences handled)
if (moduleInfo && !moduleInfo.isExternal) { const value = path.node.quasis[0].value.cooked
const { importers, dynamicImporters } = moduleInfo if (value && state.opts.protectedStrings.has(value)) {
for (const importerId of importers) idsToHandle.add(importerId) path.replaceWith(createFromCharCodeFunction(value))
for (const importerId of dynamicImporters) idsToHandle.add(importerId) }
}
}
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
_code = hasBytecodeMoudle ? _code.replace(useStrict, `${useStrict}\n${bytecodeLoaderBlock}`) : _code
}
fs.writeFileSync(chunkFileName, _code)
}
}
})
)
},
closeBundle(): void {
if (!useInRenderer) {
const chunkLimit = config.build.chunkSizeWarningLimit
const outDir = normalizePath(path.relative(config.root, path.resolve(config.root, config.build.outDir))) + '/'
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
})
bytecodeFiles.forEach(file => {
const kbs = file.size / 1000
config.logger.info(
`${colors.gray(colors.white(colors.dim(outDir)))}${colors.green(file.name.padEnd(longest + 2))} ${
kbs > chunkLimit ? colors.yellow(`${kbs.toFixed(2)} kB`) : colors.dim(`${kbs.toFixed(2)} kB`)
}`
)
})
bytecodeFiles = []
} }
} }
} }

View File

@ -2,14 +2,15 @@ import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import { builtinModules } from 'node:module' import { builtinModules } from 'node:module'
import colors from 'picocolors' import colors from 'picocolors'
import { type Plugin, mergeConfig, normalizePath } from 'vite' import { type Plugin, type UserConfig, mergeConfig, normalizePath } from 'vite'
import { getElectronNodeTarget, getElectronChromeTarget } from '../electron' import { getElectronNodeTarget, getElectronChromeTarget, supportESM } from '../electron'
import { loadPackageData, resolveBuildOutputs } from '../utils'
export interface ElectronPluginOptions { export interface ElectronPluginOptions {
root?: string root?: string
} }
function findLibEntry(root: string, scope: string): string { function findLibEntry(root: string, scope: string): string | undefined {
for (const name of ['index', scope]) { for (const name of ['index', scope]) {
for (const ext of ['js', 'ts', 'mjs', 'cjs']) { for (const ext of ['js', 'ts', 'mjs', 'cjs']) {
const entryFile = path.resolve(root, 'src', scope, `${name}.${ext}`) const entryFile = path.resolve(root, 'src', scope, `${name}.${ext}`)
@ -18,7 +19,7 @@ function findLibEntry(root: string, scope: string): string {
} }
} }
} }
return '' return undefined
} }
function findInput(root: string, scope = 'renderer'): string { function findInput(root: string, scope = 'renderer'): string {
@ -37,293 +38,362 @@ function processEnvDefine(): Record<string, string> {
} }
} }
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 defaultConfig = { const pkg = loadPackageData() || { type: 'commonjs' }
resolve: {
browserField: false, const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
mainFields: ['module', 'jsnext:main', 'jsnext'],
conditions: ['node'] const defaultConfig = {
build: {
outDir: path.resolve(root, 'out', 'main'),
target: nodeTarget,
assetsDir: 'chunks',
rolldownOptions: {
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', ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {
entryFileNames: '[name].js'
}
},
reportCompressedSize: false,
minify: false
}
} }
} satisfies UserConfig
const build = config.build || {} const build = config.build || {}
const rollupOptions = build.rollupOptions || {} const rolldownOptions = build.rolldownOptions || {}
if (!rollupOptions.input) { if (!rolldownOptions.input) {
defaultConfig.build['lib'] = { const libOptions = build.lib
entry: findLibEntry(root, 'main'), const outputOptions = rolldownOptions.output
formats: ['cjs'] defaultConfig.build['lib'] = {
} entry: findLibEntry(root, 'main'),
} else { formats:
defaultConfig.build.rollupOptions.output['format'] = 'cjs' libOptions && libOptions.formats && libOptions.formats.length > 0
? []
: [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
} }
} else {
defaultConfig.build.rolldownOptions.output['format'] = format
}
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join( defaultConfig.build.rolldownOptions.output['assetFileNames'] = path.posix.join(
build.assetsDir || defaultConfig.build.assetsDir, build.assetsDir || defaultConfig.build.assetsDir,
'[name]-[hash].[ext]' '[name]-[hash].[ext]'
)
const buildConfig = mergeConfig(defaultConfig.build, build)
config.build = buildConfig
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 } }
config.build.rollupOptions = config.build.rolldownOptions
}
}
}
export function electronMainConfigValidatorPlugin(): Plugin {
return {
name: 'vite:electron-main-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 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 libOptions = build.lib
const rolldownOptions = build.rolldownOptions
if (!(libOptions && libOptions.entry) && !rolldownOptions?.input) {
throw new Error(
'An entry point is required in the electron vite main config, ' +
'which can be specified using build.lib.entry, build.rollupOptions.input, ' +
'or build.rolldownOptions.input.'
) )
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_'
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
} }
},
{
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 required for 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 must be node')
}
}
const lib = build.lib const outputs = resolveBuildOutputs(rolldownOptions.output, libOptions)
if (!lib) {
const rollupOptions = build.rollupOptions if (outputs.length > 0) {
if (!rollupOptions?.input) { if (outputs.length > 1) {
throw new Error('build lib field required for the electron vite main config') throw new Error('The electron vite main config does not support multiple outputs.')
} else {
const output = rollupOptions?.output
if (output) {
const formats = Array.isArray(output) ? output : [output]
if (formats.some(f => f.format !== 'cjs')) {
throw new Error('the electron vite main config output format must be cjs')
}
}
}
} else { } else {
if (!lib.entry) { const output = outputs[0]
throw new Error('build entry field required for the electron vite main config') if (['es', 'cjs'].includes(output.format || '')) {
} if (output.format === 'es' && !supportESM()) {
if (!lib.formats) { throw new Error(
throw new Error('build format field required for the electron vite main config') 'The electron vite main config output format does not support "es", ' +
} else if (!lib.formats.includes('cjs')) { 'you can upgrade electron to the latest version or switch to "cjs" format.'
throw new Error('the electron vite main config build lib format must be cjs') )
}
} else {
throw new Error(
`The electron vite main config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
)
} }
} }
} }
} }
] }
} }
export function electronPreloadVitePlugin(options?: ElectronPluginOptions): Plugin[] { export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
return [ return {
{ name: 'vite:electron-preload-config-preset',
name: 'vite:electron-preload-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 defaultConfig = { const pkg = loadPackageData() || { type: 'commonjs' }
build: {
outDir: path.resolve(root, 'out', 'preload'), const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
target: nodeTarget,
assetsDir: 'chunks', const defaultConfig = {
rollupOptions: { ssr: {
external: ['electron', ...builtinModules.flatMap(m => [m, `node:${m}`])], resolve: {
output: { conditions: ['module', 'browser', 'development|production'],
entryFileNames: '[name].js' mainFields: ['browser', 'module', 'jsnext:main', 'jsnext']
}
},
reportCompressedSize: false,
minify: false
} }
},
build: {
outDir: path.resolve(root, 'out', 'preload'),
target: nodeTarget,
assetsDir: 'chunks',
rolldownOptions: {
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {}
},
reportCompressedSize: false,
minify: false
} }
} satisfies UserConfig
const build = config.build || {} const build = config.build || {}
const rollupOptions = build.rollupOptions || {} const rolldownOptions = build.rolldownOptions || {}
if (!rollupOptions.input) { if (!rolldownOptions.input) {
defaultConfig.build['lib'] = { const libOptions = build.lib
entry: findLibEntry(root, 'preload'), const outputOptions = rolldownOptions.output
formats: ['cjs'] 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.rolldownOptions.output['format'] = format
}
defaultConfig.build.rolldownOptions.output['assetFileNames'] = path.posix.join(
build.assetsDir || defaultConfig.build.assetsDir,
'[name]-[hash].[ext]'
)
const buildConfig = mergeConfig(defaultConfig.build, build)
config.build = buildConfig
const outputs = resolveBuildOutputs(config.build.rolldownOptions!.output, config.build.lib)
if (outputs.some(({ format }) => format === 'es')) {
if (Array.isArray(config.build.rolldownOptions!.output)) {
config.build.rolldownOptions!.output.forEach(output => {
if (output.format === 'es') {
output['entryFileNames'] = '[name].mjs'
output['chunkFileNames'] = '[name]-[hash].mjs'
}
})
} else { } else {
defaultConfig.build.rollupOptions.output['format'] = 'cjs' config.build.rolldownOptions!.output!['entryFileNames'] = '[name].mjs'
config.build.rolldownOptions!.output!['chunkFileNames'] = config.build.lib
? '[name]-[hash].mjs'
: path.posix.join(build.assetsDir || defaultConfig.build.assetsDir, '[name]-[hash].mjs')
} }
}
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join( config.define = config.define || {}
build.assetsDir || defaultConfig.build.assetsDir, config.define = { ...processEnvDefine(), ...config.define }
'[name]-[hash].[ext]'
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
config.build.rollupOptions = config.build.rolldownOptions
}
}
}
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 rolldownOptions = build.rolldownOptions
if (!(libOptions && libOptions.entry) && !rolldownOptions?.input) {
throw new Error(
'An entry point is required in the electron vite preload config, ' +
'which can be specified using build.lib.entry, build.rollupOptions.input, ' +
' or build.rolldownOptions.input.'
) )
const buildConfig = mergeConfig(defaultConfig.build, build)
config.build = buildConfig
config.define = config.define || {}
config.define = { ...processEnvDefine(), ...config.define }
config.envPrefix = config.envPrefix || 'PRELOAD_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
} }
},
{
name: 'vite:electron-preload-resolved-config',
apply: 'build',
enforce: 'post',
configResolved(config): void {
const build = config.build
if (!build.target) {
throw new Error('build target required for 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 lib = build.lib const outputs = resolveBuildOutputs(rolldownOptions.output, libOptions)
if (!lib) {
const rollupOptions = build.rollupOptions if (outputs.length > 0) {
if (!rollupOptions?.input) { if (outputs.length > 1) {
throw new Error('build lib field required for the electron vite preload config') throw new Error('The electron vite preload config does not support multiple outputs.')
} else {
const output = outputs[0]
if (['es', 'cjs'].includes(output.format || '')) {
if (output.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 { } else {
const output = rollupOptions?.output throw new Error(
if (output) { `The electron vite preload config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
const formats = Array.isArray(output) ? output : [output] )
if (formats.some(f => f.format !== 'cjs')) {
throw new Error('the electron vite preload config output format must be cjs')
}
}
}
} else {
if (!lib.entry) {
throw new Error('build entry field required for the electron vite preload config')
}
if (!lib.formats) {
throw new Error('build format field required for the electron vite preload config')
} else if (!lib.formats.includes('cjs')) {
throw new Error('the electron vite preload config lib format must be cjs')
} }
} }
} }
} }
] }
} }
export function electronRendererVitePlugin(options?: ElectronPluginOptions): Plugin[] { export function electronRendererConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
return [ return {
{ name: 'vite:electron-renderer-config-preset',
name: 'vite:electron-renderer-preset-config', enforce: 'pre',
enforce: 'pre', config(config): void {
config(config): void { const root = options?.root || process.cwd()
const root = options?.root || process.cwd()
config.base = config.base =
config.mode === 'production' || process.env.NODE_ENV_ELECTRON_VITE === 'production' ? './' : config.base config.mode === 'production' || process.env.NODE_ENV_ELECTRON_VITE === 'production' ? './' : config.base
config.root = config.root || './src/renderer' config.root = config.root || './src/renderer'
const chromeTarget = getElectronChromeTarget() const chromeTarget = getElectronChromeTarget()
const emptyOutDir = (): boolean => { const emptyOutDir = (): boolean => {
let outDir = config.build?.outDir let outDir = config.build?.outDir
if (outDir) { if (outDir) {
if (!path.isAbsolute(outDir)) { if (!path.isAbsolute(outDir)) {
outDir = path.resolve(root, outDir) outDir = path.resolve(root, outDir)
}
const resolvedRoot = normalizePath(path.resolve(root))
return normalizePath(outDir).startsWith(resolvedRoot + '/')
} }
return true 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_'
} }
},
{
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 field for the electron vite renderer config'))
}
const build = config.build const defaultConfig = {
if (!build.target) { build: {
throw new Error('build target required for the electron vite renderer config') outDir: path.resolve(root, 'out', 'renderer'),
} else { target: chromeTarget,
const targets = Array.isArray(build.target) ? build.target : [build.target] modulePreload: { polyfill: false },
if (targets.some(t => !t.startsWith('chrome') && !/^es((202\d{1})|next)$/.test(t))) { rolldownOptions: {
throw new Error('the electron vite renderer config build target must be chrome? or es?') input: findInput(root)
} },
reportCompressedSize: false,
minify: false,
emptyOutDir: emptyOutDir()
} }
} satisfies UserConfig
const rollupOptions = build.rollupOptions if (config.build?.outDir) {
if (!rollupOptions.input) { config.build.outDir = path.resolve(root, config.build.outDir)
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory`)) }
throw new Error('build rollupOptions input field required for the electron vite renderer config')
const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
config.build = buildConfig
config.envDir = config.envDir || path.resolve(root)
config.envPrefix = config.envPrefix || ['RENDERER_VITE_', 'VITE_']
config.build.rollupOptions = config.build.rolldownOptions
}
}
}
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 rolldownOptions = build.rolldownOptions
if (!rolldownOptions.input) {
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory.`))
throw new Error(
'build.rollupOptions.input or build.rolldownOptions.input option is required in the electron vite renderer config.'
)
}
} }
] }
} }

55
src/plugins/esmShim.ts Normal file
View File

@ -0,0 +1,55 @@
import MagicString from 'magic-string'
import type { Plugin } from 'vite'
import { supportImportMetaPaths } from '../electron'
const CJSyntaxRe = /__filename|__dirname|require\(|require\.resolve\(/
const CJSShim_normal = `
// -- CommonJS Shims --
import __shims_url__ from 'node:url';
import __shims_path__ from 'node:path';
import __shims_module__ from 'node:module';
const __filename = __shims_url__.fileURLToPath(import.meta.url);
const __dirname = __shims_path__.dirname(__filename);
const require = __shims_module__.createRequire(import.meta.url);
`
const CJSShim_node_20_11 = `
// -- CommonJS Shims --
import __shims_module__ from 'node:module';
const __filename = import.meta.filename;
const __dirname = import.meta.dirname;
const require = __shims_module__.createRequire(import.meta.url);
`
export default function esmShimPlugin(): Plugin {
const CJSShim = supportImportMetaPaths() ? CJSShim_node_20_11 : CJSShim_normal
return {
name: 'vite:esm-shim',
apply: 'build',
enforce: 'post',
renderChunk(code, _chunk, { format, sourcemap }) {
if (format === 'es') {
// Rolldown may remove unused shim bindings after this hook; referenced bindings remain available.
if (code.includes(CJSShim) || !CJSyntaxRe.test(code)) {
return null
}
const lastESMImport = this.parse(code)
.body.filter(node => node.type === 'ImportDeclaration')
.pop()
const indexToAppend = lastESMImport ? lastESMImport.end : 0
const s = new MagicString(code)
s.appendRight(indexToAppend, CJSShim)
return {
code: s.toString(),
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
}
}
return null
}
}
}

View File

@ -1,6 +1,5 @@
import path from 'node:path'
import { createRequire } from 'node:module'
import { type Plugin, mergeConfig } from 'vite' import { type Plugin, mergeConfig } from 'vite'
import { loadPackageData } from '../utils'
export interface ExternalOptions { export interface ExternalOptions {
exclude?: string[] exclude?: string[]
@ -8,14 +7,14 @@ 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
const packagePath = path.resolve(process.cwd(), 'package.json') const pkg = loadPackageData() || {}
const require = createRequire(import.meta.url)
const pkg = require(packagePath)
let deps = Object.keys(pkg.dependencies || {}) let deps = Object.keys(pkg.dependencies || {})
if (include.length) { if (include.length) {
@ -34,13 +33,15 @@ export function externalizeDepsPlugin(options: ExternalOptions = {}): Plugin | n
config(config): void { config(config): void {
const defaultConfig = { const defaultConfig = {
build: { build: {
rollupOptions: { rolldownOptions: {
external: deps.length > 0 ? [...deps, new RegExp(`^(${deps.join('|')})/.+`)] : [] external: deps.length > 0 ? [...deps, new RegExp(`^(${deps.join('|')})/.+`)] : []
} }
} }
} }
const buildConfig = mergeConfig(defaultConfig.build, config.build || {}) const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
config.build = buildConfig config.build = buildConfig
config.build.rollupOptions = config.build.rolldownOptions
} }
} }
} }

34
src/plugins/importMeta.ts Normal file
View File

@ -0,0 +1,34 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type Plugin, version } from 'vite'
/**
* @deprecated Vite 8 (Rolldown) now provides native support for these
* `import.meta` properties. These properties are polyfilled when the output
* format is CJS.
*
* @see https://rolldown.rs/in-depth/non-esm-output-formats#well-known-import-meta-properties
*/
export default function importMetaPlugin(): Plugin | null {
const currentVersion = version.split('.')
const major = parseInt(currentVersion[0], 10)
if (major >= 8) {
return null
}
return {
name: 'vite:import-meta',
apply: 'build',
enforce: 'pre',
resolveImportMeta(property, { format }): string | null {
if (property === 'url' && format === 'cjs') {
return `require("url").pathToFileURL(__filename).href`
}
if (property === 'filename' && format === 'cjs') {
return `__filename`
}
if (property === 'dirname' && format === 'cjs') {
return `__dirname`
}
return null
}
} as any
}

View File

@ -0,0 +1,199 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */
import path from 'node:path'
import { type InlineConfig, type Plugin, type LogLevel, type Rolldown, build as viteBuild, mergeConfig } from 'vite'
import colors from 'picocolors'
import { cleanUrl } from '../utils'
import type { ConfigFactory, PreloadViteConfig, RendererViteConfig } from '../config'
const VIRTUAL_ENTRY_ID = '\0virtual:isolate-entries'
const LogLevels: Record<LogLevel, number> = {
silent: 0,
error: 1,
warn: 2,
info: 3
}
export default function isolateEntriesPlugin(factory: ConfigFactory<PreloadViteConfig | RendererViteConfig>): Plugin {
let entries: string[] | { [x: string]: string }[]
let transformedCount = 0
const assetCache = new Set<string>()
return {
name: 'vite:isolate-entries',
apply: 'build',
options(opts): Rolldown.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 userConfig = await factory.build(true)
const shouldLog = LogLevels[userConfig.logLevel || 'info'] >= LogLevels.info
const watchFiles = new Set<string>()
for (const entry of entries) {
const re = await bundleEntryFile(entry, userConfig, 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)
}
if (this.meta.watchMode) {
const moduleIds = new Set<string>()
for (const chunk of re.bundles.output) {
if (chunk.type === 'chunk') {
for (const id of chunk.moduleIds) {
if (!id.startsWith('\0')) {
moduleIds.add(cleanUrl(id))
}
}
}
}
for (const id of moduleIds) {
this.addWatchFile(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)
this.environment.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,
shouldLog: boolean,
preTransformedCount: number
): Promise<{ bundles: Rolldown.RolldownOutput; transformedCount: number }> {
const transformReporter = transformReporterPlugin(preTransformedCount, shouldLog)
const viteConfig = mergeConfig(config, {
build: {
write: false,
watch: false
},
plugins: [transformReporter],
logLevel: 'warn',
configFile: false
}) as InlineConfig
// rewrite the input instead of merging
viteConfig.build!.rolldownOptions!.input = input
const bundles = (await viteBuild(viteConfig)) as Rolldown.RolldownOutput
return {
bundles: bundles,
transformedCount: transformReporter?.api?.getTransformedCount() || 0
}
}
function transformReporterPlugin(
preTransformedCount = 0,
shouldLog = true
): Plugin<{ getTransformedCount: () => number }> {
let transformedCount = 0
const log = throttle((id, root) => {
writeLine(`transforming (${preTransformedCount + transformedCount}) ${colors.dim(path.relative(root, id))}`)
})
return {
name: 'vite:transform-reporter',
transform(_, id) {
transformedCount++
if (!shouldLog) return
if (id.includes('?')) return
log(id, this.environment.config.root)
},
api: {
getTransformedCount() {
return transformedCount
}
}
}
}
function writeLine(output: string): void {
if (!process.stdout.isTTY || process.env.CI) return
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 (!process.stdout.isTTY || process.env.CI) return
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)
}
}

126
src/plugins/modulePath.ts Normal file
View File

@ -0,0 +1,126 @@
import { type Plugin, type InlineConfig, type Rolldown, build as viteBuild, mergeConfig } from 'vite'
import MagicString from 'magic-string'
import { cleanUrl, toRelativePath } from '../utils'
import { supportImportMetaPaths } from '../electron'
import type { ConfigFactory, MainViteConfig } from '../config'
const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g
/**
* Resolve `?modulePath` import and return the module bundle path.
*/
export default function modulePathPlugin(factory: ConfigFactory<MainViteConfig>): Plugin {
const isImportMetaPathSupported = supportImportMetaPaths()
const assetCache = new Set<string>()
return {
name: 'vite:module-path',
apply: 'build',
enforce: 'pre',
buildStart(): void {
assetCache.clear()
},
async load(id): Promise<string | void> {
if (id.endsWith('?modulePath')) {
// id resolved by Vite resolve plugin
const config = await factory.build(true)
const bundles = await bundleEntryFile(cleanUrl(id), config)
const [outputChunk, ...outputChunks] = bundles.output
const hash = this.emitFile({
type: 'asset',
fileName: outputChunk.fileName,
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)
}
if (this.meta.watchMode) {
const moduleIds = new Set<string>()
for (const chunk of bundles.output) {
if (chunk.type === 'chunk') {
for (const id of chunk.moduleIds) {
if (!id.startsWith('\0')) {
moduleIds.add(cleanUrl(id))
}
}
}
}
for (const id of moduleIds) {
this.addWatchFile(id)
}
}
const refId = `__VITE_MODULE_PATH__${hash}__`
const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname'
return `
import { join } from 'path'
export default join(${dirnameExpr}, ${refId})`
}
},
renderChunk(code, chunk, { sourcemap }): { code: string; map: Rolldown.SourceMapInput } | null {
let match: RegExpExecArray | null
let s: MagicString | undefined
modulePathRE.lastIndex = 0
while ((match = modulePathRE.exec(code))) {
s ||= new MagicString(code)
const [full, hash] = match
const filename = this.getFileName(hash)
const outputFilepath = toRelativePath(filename, chunk.fileName)
const replacement = JSON.stringify(outputFilepath)
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true
})
}
if (s) {
return {
code: s.toString(),
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
}
}
return null
}
}
}
async function bundleEntryFile(input: string, config: InlineConfig): Promise<Rolldown.RolldownOutput> {
const viteConfig = mergeConfig(config, {
build: {
write: false,
watch: false
},
plugins: [
{
name: 'vite:entry-file-name',
outputOptions(output): Rolldown.OutputOptions {
// Prevent output conflicts
output.hashCharacters = 'hex'
// Assign chunkFileNames to entryFileNames to treat sub-entries as internal chunks
output.entryFileNames = output.chunkFileNames
return output
}
}
] as Plugin[],
logLevel: 'warn',
configFile: false
}) as InlineConfig
// rewrite the input instead of merging
const buildOptions = viteConfig.build!
buildOptions.rolldownOptions = {
...buildOptions.rolldownOptions,
input
}
const bundles = await viteBuild(viteConfig)
return bundles as Rolldown.RolldownOutput
}

View File

@ -79,12 +79,16 @@ export type SwcOptions = {
export function swcPlugin(options: SwcOptions = {}): Plugin { export function swcPlugin(options: SwcOptions = {}): Plugin {
const filter = createFilter(options.include || /\.(m?ts|[jt]sx)$/, options.exclude || /\.js$/) const filter = createFilter(options.include || /\.(m?ts|[jt]sx)$/, options.exclude || /\.js$/)
let sourcemap: boolean | 'inline' = false let sourcemap: boolean | 'inline' = false
let minify: boolean | 'esbuild' | 'terser' = false let minify: boolean | 'esbuild' | 'oxc' | 'terser' = false
return { return {
name: 'vite:swc', name: 'vite:swc',
config(): UserConfig { config(): UserConfig {
// Vite 8 defaults to Oxc for transform; `esbuild: false` is ignored and
// warns. SWC replaces both paths when this plugin is active.
// See https://github.com/alex8088/electron-vite/issues/916
return { return {
esbuild: false esbuild: false,
oxc: false
} }
}, },
async configResolved(resolvedConfig): Promise<void> { async configResolved(resolvedConfig): Promise<void> {
@ -101,7 +105,7 @@ export function swcPlugin(options: SwcOptions = {}): Plugin {
} }
}, },
async renderChunk(code, chunk): Promise<null | { code: string; map: SourceMap }> { async renderChunk(code, chunk): Promise<null | { code: string; map: SourceMap }> {
if (!minify || minify === 'terser') { if (!minify || minify === 'terser' || minify === 'oxc') {
return null return null
} }
const result = await transformWithSWC(code, chunk.fileName, { const result = await transformWithSWC(code, chunk.fileName, {

View File

@ -1,61 +1,60 @@
import type { Plugin } from 'vite' import type { Plugin } from 'vite'
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__([a-z\d]{8})__/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(require.resolve(${assetRefId}), 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 }) {
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: true }) : 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

@ -2,7 +2,7 @@ import type { ChildProcess } from 'node:child_process'
import { import {
type UserConfig as ViteConfig, type UserConfig as ViteConfig,
type ViteDevServer, type ViteDevServer,
createServer as ViteCreateServer, createServer as viteCreateServer,
build as viteBuild, build as viteBuild,
createLogger, createLogger,
mergeConfig mergeConfig
@ -24,25 +24,27 @@ export async function createServer(
let server: ViteDevServer | undefined let server: ViteDevServer | undefined
let ps: ChildProcess | undefined let ps: ChildProcess | undefined
const errorHook = (e): void => {
logger.error(`${colors.bgRed(colors.white(' ERROR '))} ${colors.red(e.message)}`)
}
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) 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
@ -50,25 +52,23 @@ 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' })
} }
} }
await doBuild(preloadViteConfig, watchHook) 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'
)}`
) )
} }
@ -76,7 +76,7 @@ export async function createServer(
if (rendererViteConfig) { if (rendererViteConfig) {
logger.info(colors.gray(`\n-----\n`)) logger.info(colors.gray(`\n-----\n`))
server = await ViteCreateServer(rendererViteConfig) server = await viteCreateServer(rendererViteConfig)
if (!server.httpServer) { if (!server.httpServer) {
throw new Error('HTTP server not available') throw new Error('HTTP server not available')
@ -102,13 +102,13 @@ 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`))
} }
} }
type UserConfig = ViteConfig & { configFile?: string | false } type UserConfig = ViteConfig & { configFile?: string | false }
async function doBuild(config: UserConfig, watchHook: () => void): Promise<void> { async function doBuild(config: UserConfig, watchHook: () => void, errorHook: (e: Error) => void): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
if (config.build?.watch) { if (config.build?.watch) {
let firstBundle = true let firstBundle = true
@ -131,10 +131,12 @@ async function doBuild(config: UserConfig, watchHook: () => void): Promise<void>
}) })
} }
viteBuild(config).then(() => { viteBuild(config)
if (!config.build?.watch) { .then(() => {
resolve() if (!config.build?.watch) {
} resolve()
}) }
})
.catch(e => errorHook(e))
}) })
} }

View File

@ -1,14 +1,14 @@
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 { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { loadEnv as viteLoadEnv } from 'vite' import { createRequire } from 'node:module'
import { type LibraryOptions, type Rolldown, loadEnv as viteLoadEnv } from 'vite'
export function isObject(value: unknown): value is Record<string, unknown> { export function isObject(value: unknown): value is Record<string, unknown> {
return Object.prototype.toString.call(value) === '[object Object]' return Object.prototype.toString.call(value) === '[object Object]'
} }
export const dynamicImport = new Function('file', 'return import(file)')
export const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000']) export const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000'])
export function resolveHostname(optionsHost: string | boolean | undefined): string { export function resolveHostname(optionsHost: string | boolean | undefined): string {
@ -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 {
@ -38,14 +33,160 @@ export function toRelativePath(filename: string, importer: string): string {
} }
/** /**
* Load `.env` files within the `envDir`(default: `process.cwd()`). * Resolve configured build outputs into an array.
* By default, only env variables prefixed with `MAIN_VITE_`, `PRELOAD_VITE_` and */
export function resolveBuildOutputs(
outputs: Rolldown.OutputOptions | Rolldown.OutputOptions[] | undefined,
libOptions: LibraryOptions | false | undefined
): Rolldown.OutputOptions[] {
if (Array.isArray(outputs)) {
return outputs
}
if (libOptions) {
const libFormats = libOptions.formats || []
return libFormats.map(format => ({ ...outputs, format }))
}
return outputs ? [outputs] : []
}
/**
* Load `.env` files within the `envDir` (default: `process.cwd()`) .
* By default, only env variables prefixed with `VITE_`, `MAIN_VITE_`, `PRELOAD_VITE_` and
* `RENDERER_VITE_` are loaded, unless `prefixes` is changed. * `RENDERER_VITE_` are loaded, unless `prefixes` is changed.
*/ */
export function loadEnv( export function loadEnv(
mode: string, mode: string,
envDir: string = process.cwd(), envDir: string = process.cwd(),
prefixes: string | string[] = ['MAIN_VITE_', 'PRELOAD_VITE_', 'RENDERER_VITE_'] prefixes: string | string[] = ['VITE_', 'MAIN_VITE_', 'PRELOAD_VITE_', 'RENDERER_VITE_']
): Record<string, string> { ): Record<string, string> {
return viteLoadEnv(mode, envDir, prefixes) return viteLoadEnv(mode, envDir, prefixes)
} }
interface PackageData {
main?: string
type?: 'module' | 'commonjs'
dependencies?: Record<string, string>
}
let packageCached: PackageData | null = null
export function loadPackageData(root = process.cwd()): PackageData | null {
if (packageCached) return packageCached
const pkg = path.join(root, 'package.json')
if (fs.existsSync(pkg)) {
const _require = createRequire(import.meta.url)
const data = _require(pkg)
packageCached = {
main: data.main,
type: data.type,
dependencies: data.dependencies
}
return packageCached
}
return null
}
export function isFilePathESM(filePath: string): boolean {
if (/\.m[jt]s$/.test(filePath) || filePath.endsWith('.ts')) {
return true
} else if (/\.c[jt]s$/.test(filePath)) {
return false
} else {
const pkg = loadPackageData()
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>
}
/**
* Isolate configuration data without cloning plugins or runtime instances.
* Plugins may capture their own object in hooks, so their identity must survive.
*/
export function cloneConfig<T>(config: T): T {
const seen = new WeakMap<object, unknown>()
const pluginArrays = new WeakMap<object, unknown>()
function clone(value: unknown, plugins = false): unknown {
if (value === null || typeof value !== 'object') return value
const cache = plugins ? pluginArrays : seen
if (cache.has(value)) return cache.get(value)
if (Array.isArray(value)) {
const result: unknown[] = new Array(value.length)
cache.set(value, result)
value.forEach((item, index) => {
result[index] = clone(item, plugins)
})
return result
}
// Preserve plugin objects, promises and other opaque plugin options.
if (plugins) return value
if (value instanceof RegExp) {
const result = new RegExp(value)
result.lastIndex = value.lastIndex
cache.set(value, result)
return result
}
const prototype = Object.getPrototypeOf(value)
if (prototype !== Object.prototype && prototype !== null) return value
const result = Object.create(prototype) as Record<PropertyKey, unknown>
cache.set(value, result)
for (const key of Reflect.ownKeys(value)) {
if (!Object.prototype.propertyIsEnumerable.call(value, key)) continue
Object.defineProperty(result, key, {
value: clone((value as Record<PropertyKey, unknown>)[key], key === 'plugins'),
enumerable: true,
configurable: true,
writable: true
})
}
return result
}
return clone(config) as 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,23 +1,20 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2019", "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,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noImplicitAny": false, "noImplicitAny": false,
"noImplicitReturns": true, "noImplicitReturns": true
"declaration": true,
"declarationDir": "dist/types",
"outDir": "dist"
}, },
"include": ["src"] "include": ["src", "rollup.config.ts"]
} }