Compare commits

...

287 Commits

Author SHA1 Message Date
alex8088
91368b6655 release: v5.0.0 2025-12-07 22:26:56 +08:00
alex8088
b1fd596afe docs: update 2025-11-09 23:31:08 +08:00
alex8088
438e9e7672 chore: fix jsdoc 2025-11-09 23:03:06 +08:00
alex8088
9eba4df577 release: v5.0.0-beta.3 2025-11-01 17:11:12 +08:00
alex8088
465690ab0d refactor(config)!: remove function resolution for nested config fields 2025-10-31 23:28:40 +08:00
alex8088
6aae37833e perf(buildReport): exclude node_modules from watch list 2025-10-31 21:59:15 +08:00
alex8088
9152dfc943 fix(modulePath): rewrite the build input instead of merging 2025-10-31 01:16:10 +08:00
alex8088
0276407b5b refactor: deprecated externalizeDepsPlugin and bytecodePlugin 2025-10-31 01:16:02 +08:00
alex8088
fe7e631f47 refactor(config): move the isolateEntries options to the build option 2025-10-31 01:09:44 +08:00
alex8088
08be346407 feat(config): add build.externalizeDeps and build.bytecode config options to replace externalizeDepsPlugin and bytecodePlugin 2025-10-31 01:03:34 +08:00
alex8088
3e9ded666c release: v5.0.0-beta.2 2025-10-29 23:42:04 +08:00
alex8088
1bba6766e8 perf(isolateEntries): optimize entries transformation 2025-10-29 23:35:38 +08:00
alex8088
4edffe3b9a perf(isolateEntries): transform log 2025-10-29 23:25:28 +08:00
alex8088
cfd9812a91 feat: reporter plugin for isolated builds 2025-10-29 22:32:35 +08:00
alex8088
7c7f31b2a3 fix: avoid duplicate chunk emission 2025-10-29 21:22:12 +08:00
alex8088
ae57b2489a fix(asset): normalize imported public asset chunk path 2025-10-29 21:12:13 +08:00
alex8088
397b02e384 release: v5.0.0-beta.1 2025-10-29 00:19:49 +08:00
alex8088
a4f7693712 perf: build compatibility target for Electron 39 2025-10-29 00:08:03 +08:00
alex8088
56fb519092 refactor: replace JSON.parse/stringify with manual deep clone 2025-10-28 23:18:45 +08:00
alex8088
eb0a7e3ffe refactor(build): simplify build logic 2025-10-28 22:53:03 +08:00
alex8088
de70dfe1dc refactor(config)!: restructure Electron Vite config interfaces 2025-10-28 22:36:25 +08:00
alex8088
8892bf3679 fix(modulePath): support watch mode 2025-10-28 21:50:54 +08:00
alex8088
2576484604 chore: improve logging message clarity and consistency 2025-10-28 21:50:47 +08:00
alex8088
88f6db2239 refactor: split electron plugin into preset and validator plugins 2025-10-28 21:50:12 +08:00
alex8088
0a79da03db feat: add isolatedEntries option for preload and renderer to build entries as standalone bundles #154 2025-10-27 23:40:01 +08:00
alex8088
c3939ade45 refactor(asset): remove redundant path normalization 2025-10-24 23:57:12 +08:00
alex8088
38228f9b3f fix(modulePath): prevent duplicate asset emission 2025-10-24 22:10:03 +08:00
alex8088
8b193864fd feat: enhanced string protection 2025-10-20 23:35:21 +08:00
alex8088
f264d41d7f release: v5.0.0-beta.0 2025-10-19 20:09:04 +08:00
alex8088
5debb6f83b refactor: remove Electron 18, 19, 20, 21 build compatilibity target 2025-10-19 16:00:01 +08:00
alex8088
d530597339 chore: rename the file esm to esmShim 2025-10-19 15:53:19 +08:00
alex8088
a9b5326544 perf(plugin): enhance path resolution using import.meta.dirname for ES modules 2025-10-19 15:47:35 +08:00
alex8088
7587d2c674 perf(bytecodePlugin): better way to count bytecode chunks 2025-10-19 14:49:09 +08:00
alex8088
0fb8918090 perf(plugin): lazily initialize MagicString and remove the redundant pre-check 2025-10-19 14:42:35 +08:00
alex8088
c7955aa6fd perf(plugin): no need to cache sourcemap option 2025-10-19 12:11:29 +08:00
alex8088
70e027d38a perf(plugin): more efficient module filtering via regular expressions 2025-10-19 11:57:23 +08:00
alex8088
28bb22b353 refactor(modulePath): better support for tree-shaking and code-splitting 2025-10-18 17:10:04 +08:00
alex8088
327adc23df refactor(bytecodePlugin): improved bytecode bundle generation and made a new string protection plugin 2025-10-18 16:33:49 +08:00
alex8088
4a6aea3704 chore: remove redundant external id 2025-10-17 21:53:35 +08:00
alex8088
3f7f65bf57 chore: replace tseslint.config with defineConfig 2025-10-17 21:29:48 +08:00
alex8088
0badfc493f chore: update eslint config 2025-10-17 21:28:47 +08:00
alex8088
cbb039c3e9 chore(deps): update all non-major dependencies 2025-10-17 21:07:21 +08:00
alex8088
ad50cba495 release: 4.0.1 2025-09-21 17:03:52 +08:00
alex8088
b55eb725c4 perf: build compatibility target for Electron 38 2025-09-21 16:52:57 +08:00
alex8088
1eeb15ea77 release: v4.0.0 2025-07-06 23:12:09 +08:00
alex8088
52abc48abf release: v4.0.0-beta.0 2025-06-28 13:29:56 +08:00
alex8088
cbee52c8bb perf: build compatibility target for Electron 37 2025-06-28 13:12:31 +08:00
alex8088
48e6f4f570 fix(deps)!: update Vite to v7 and remove cjs build 2025-06-28 13:08:38 +08:00
alex8088
4071778f07 fix: use import type for type-only imports 2025-06-28 00:01:48 +08:00
alex8088
3fd16d0c23 refactor!: bump required node version to 20.19+, 22.12+ 2025-06-27 23:10:23 +08:00
alex8088
d2e8b1271b chore: allow breaking commit message 2025-06-27 23:07:33 +08:00
alex8088
28d7df6e91 chore(deps): update lint-staged to v16 2025-06-27 22:02:26 +08:00
alex8088
b7763a7f77 chore(deps): update all non-major dependencies 2025-06-27 21:59:22 +08:00
alex8088
1c084cc090 chore(deps): update pnpm to v10 2025-06-27 21:50:18 +08:00
alex8088
9ec164d33e chore: add funding 2025-05-10 17:58:35 +08:00
alex8088
a9197f5cc9 chore: remove deprecated rules and adjust rules 2025-05-07 22:03:59 +08:00
jonz94
3d5c9f68a1
perf: build compatibility target for Electron 36 (#766) 2025-05-06 10:12:44 +08:00
alex8088
1b411d3633 release: v3.1.0 2025-03-25 21:30:53 +08:00
alex8088
b56d3c2d21 fix(bytecodePlugin): optimize 'use strict' directive replacement (#681) 2025-03-16 19:24:22 +08:00
alex8088
f2eff25268 release: v3.1.0-beta.0 2025-03-12 22:05:32 +08:00
alex8088
2d8e513e07 chore(deps): update esbuild to v0.25 2025-03-12 21:52:14 +08:00
alex8088
d8063320dc chore(deps): update globals to v16 2025-03-12 21:43:32 +08:00
alex8088
f33c5b2abe chore(deps): update all non-major dependencies 2025-03-12 21:40:35 +08:00
alex.wei
e91e70c105
Merge pull request #729 from jonz94/electron-35
perf: build compatibility target for Electron 35
2025-03-11 10:10:09 +08:00
jonz94
ea144aef19
perf: build compatibility target for Electron 35 2025-03-09 04:16:52 +08:00
alex8088
987c55ee8b release: v3.0.0 2025-02-16 20:37:18 +08:00
alex8088
8064bd81ff release: v3.0.0-beta.0 2025-01-22 23:05:02 +08:00
alex8088
6e8572d9b7 feat: resolve conditions for preload 2025-01-22 22:24:04 +08:00
alex8088
4b47ef0bd4 perf: build compatilibity target for Electron 34 2025-01-22 21:16:13 +08:00
alex8088
5a5af050b2 chore(deps): update @type/node to v22 2025-01-22 21:13:05 +08:00
alex8088
96ae3c5cd9 chore(deps): update vite to v6 2025-01-22 01:24:58 +08:00
alex8088
79ac91dee2 chore(deps): update esbuild to v0.24 2025-01-22 01:15:46 +08:00
alex8088
1599d730f6 chore(deps): update @rollup/plugin-typescript to v12 2025-01-22 01:12:57 +08:00
alex8088
3c6e08b2f2 chore(deps): update @rollup/plugin-node-resolve to v16 2025-01-22 01:10:54 +08:00
alex8088
dfe6a3e3f8 chore(deps): update all non-major dependencies 2025-01-22 01:04:31 +08:00
alex8088
6c01417909 chore: move to eslint flat config 2025-01-22 00:47:48 +08:00
kye shimizu
5ffd49eddc
perf: build compatilibity target for Electron 33 (#651) 2024-11-11 23:49:02 +08:00
alex8088
bf1220875f perf: build compatilibity target for Electron 32 2024-08-21 21:49:44 +08:00
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
alex8088
8e1f9f6845 release: v1.0.29 2023-11-17 22:27:06 +08:00
alex8088
7369960b99 feat(cli): support --noSandbox option for dev and preview command 2023-11-08 23:03:50 +08:00
alex8088
ac47dacc1b perf: build compatilibity target for Electron 27 2023-10-17 21:57:45 +08:00
alex8088
2360d502a4 release: v1.0.28 2023-09-18 22:08:46 +08:00
alex8088
01163866fb feat(cli): supports specifying electron entry file (#270) 2023-09-08 00:32:43 +08:00
alex8088
8b839872f8 chore(types): add .json?commonjs-external&asset typing 2023-09-08 00:04:02 +08:00
alex8088
670c23b2e1 perf: build compatilibity target for Electron 26 2023-09-07 22:14:02 +08:00
alex8088
566f88960e fix(externalizeDepsPlugin): supports subpath 2023-09-07 22:06:47 +08:00
alex.wei
8a774ef76d
Merge pull request #254 from yoni-rapoport/master
Support subpath imports from externalized dependencies
2023-08-17 23:58:43 +08:00
Yoni Rapoport
27acfb7c1d
Support subpath imports from externalized dependencies
Using an array of regular expressions for rollupOptions.external, I've fixed an issue causing subpath imports or external dependencies (e.g. import {} from 'pkg/subpath') to be bundled.
2023-08-15 14:37:48 +03:00
alex8088
5c440a8a76 release: v1.0.27 2023-08-01 21:45:21 +08:00
alex8088
ed64873434 chore: remove preinstall script 2023-08-01 21:37:14 +08:00
alex8088
254ce0b5b6 release: v1.0.26 2023-07-30 18:32:51 +08:00
alex8088
0c46c1064b chore(types): add Vite importMeta types 2023-07-30 17:27:04 +08:00
alex8088
6c590ad65b chore(types): add process.env.ELECTRON_RENDERER_URL type 2023-07-30 17:23:59 +08:00
alex8088
d8307e8fd4 chore: update user config interface jsdoc 2023-07-30 17:00:41 +08:00
alex8088
532ba52714 chore(deps): update @typescript-eslint/* to v6 2023-07-29 17:17:07 +08:00
alex8088
8914878a53 chore(deps): update prettier to v3 2023-07-29 17:11:49 +08:00
alex8088
d7b4ebc54a chore(deps): update pnpm to v8 2023-07-29 16:45:45 +08:00
alex8088
342a0556cd perf: spawn Electron process using parent's stdios (#236) 2023-07-29 01:44:26 +08:00
alex8088
9d76799072 feat(cli): add CLI --inspect[-brk] to support debugging without IDEs (#231) 2023-07-27 00:33:28 +08:00
alex8088
f0f21db904 chore: improve issue templates 2023-07-26 21:54:03 +08:00
alex8088
11b6f3c39d release: v1.0.25 2023-07-11 21:09:38 +08:00
alex8088
ed13174829 fix(asset): asset handling error when hot reloading 2023-07-11 00:00:14 +08:00
alex8088
583c318af1 chore(deps): update rollup to 3.26.2 2023-07-10 23:40:26 +08:00
alex8088
3507149b6e chore(deps): update esbuild to v0.18 2023-07-10 23:31:57 +08:00
alex8088
81117c6809 chore(deps): update vite to 4.4.2 2023-07-10 23:29:56 +08:00
alex8088
3567f26f02 chore(deps): update typescript to 5.0.4 2023-07-10 23:26:08 +08:00
alex8088
9f5ca0d92d chore(deps): update @types/node to v18 2023-07-07 23:58:41 +08:00
alex8088
8b445b67c5 chore(deps): update fs-extra to v11 2023-07-07 23:55:06 +08:00
alex8088
c43d1832d2 chore(deps): update all non-major dependencies 2023-07-07 23:53:12 +08:00
alex8088
0367f73a95 fix: remove node resolve condition for preload (#204) 2023-07-07 21:35:12 +08:00
alex8088
785b32f2b0 release: v1.0.24 2023-06-25 21:09:29 +08:00
alex8088
4f796e4c8a perf: ignore browser field and additional node condition for main config 2023-06-18 16:33:08 +08:00
alex8088
3def0ed84b fix(bytecodePlugin): bytecode loader relative path is incorrect 2023-06-18 16:21:38 +08:00
alex8088
b71bf82f02 docs: electron-vite.org is online 2023-06-10 23:45:59 +08:00
alex8088
587efabac5 release: v1.0.23 2023-06-04 20:03:01 +08:00
alex8088
bac8c04816 chore(deps): update all non-major dependencies 2023-06-04 00:28:05 +08:00
alex8088
6dff3475b4 feat: supports ES build target for renderer #174 2023-06-03 22:18:48 +08:00
alex8088
c26a9ff71c perf: build compatilibity target for Electron 25 2023-06-03 21:43:17 +08:00
alex8088
388b590808 docs: update 2023-05-15 23:14:33 +08:00
alex8088
8c924188de revert: chore: remove process env define (#159) 2023-05-14 17:49:04 +08:00
alex8088
6627e81f66 release: v1.0.22 2023-04-24 00:11:52 +08:00
alex8088
f443c18056 feat: add --rendererOnly flag to dev command 2023-04-23 21:28:58 +08:00
alex8088
a736af835e perf: build compatilibity target for Electron 24 2023-04-18 23:46:29 +08:00
alex8088
3b2ba0ae25 chore: remove process env define 2023-04-18 23:11:33 +08:00
alex.wei
72d37a70ca
Merge pull request #139 from gknapp/config-error-messages
fix(dev): Rephrase config error messages, fix typo 'electorn'
2023-04-03 09:51:47 +08:00
Greg Knapp
c0d9ad3fd5
Rephase certain error messages, fix typo 'electorn' 2023-03-31 16:37:15 +01:00
alex8088
3a69fbf489 release: v1.0.21 2023-03-27 00:01:41 +08:00
alex8088
6c6db628f6 fix(bytecodePlugin): sub-chunks are not compliled in vite 4 2023-03-26 23:08:52 +08:00
alex8088
f33c86383e fix(bytecodePlugin): bytecode loader is not referenced correctly in the chunks 2023-03-26 21:17:29 +08:00
alex8088
1cb7d419a2 perf: alway disable build.modulePreload in main and preload config 2023-03-25 13:21:33 +08:00
alex8088
103beec87b chore(deps): update vite to 4.2.1 2023-03-25 12:26:28 +08:00
alex8088
f0c1a431b0 chore(deps): update esbuild to 0.17 2023-03-25 12:21:58 +08:00
alex8088
ee4d400fb0 release: v1.0.20 2023-03-12 00:39:48 +08:00
alex8088
5ad0747f32 feat: support for renderer debugging (#130) 2023-03-11 20:29:06 +08:00
alex8088
f7f19f9649 fix(bytecodePlugin): not work in monorepo (#128) 2023-03-11 00:15:19 +08:00
alex8088
05e0cc6db1 fix: specified renderer outDir is not parsed correctly 2023-03-10 23:47:17 +08:00
alex8088
55be651207 fix(asset): asset path is not resolved correctly when outDir is specified #117 2023-03-10 23:46:57 +08:00
alex8088
a84ae9e7e0 perf: print log 2023-03-05 18:21:31 +08:00
alex8088
0d837e4356 chore(deps): update magic-string to 0.30.0 2023-03-05 16:18:23 +08:00
alex8088
8dd87715e7 chore(deps): update rollup to 3.18 2023-03-05 16:16:45 +08:00
alex8088
9c34b6c884 chore(deps): update vite to 4.1.4 2023-03-05 16:09:19 +08:00
alex8088
678bfa616d chore(deps): update all non-major dependencies 2023-03-05 16:08:19 +08:00
alex8088
b0fc657db4 perf: build compatilibity target for Electron 23 2023-02-08 21:35:49 +08:00
alex8088
5ed5fe9352 release: v1.0.19 2023-02-06 21:01:27 +08:00
alex8088
c3567edba9 fix(bytecodePlugin): escape protected strings 2023-02-05 21:41:01 +08:00
alex8088
32d78f2f42 feat(bytecodePlugin): protect strings #91 2023-01-30 22:00:33 +08:00
alex8088
bc6448df9b release: v1.0.18 2023-01-16 20:54:23 +08:00
alex8088
b85cd5ecf2 docs: update 2023-01-16 20:52:19 +08:00
alex8088
44af7960a2 fix(asset): wasm must be suffixed with ?loader 2023-01-13 21:32:54 +08:00
alex8088
b3208d3dae feat(asset): support for WebAssembly in the main process 2023-01-10 21:53:38 +08:00
alex8088
4989595464 release: v1.0.17 2023-01-08 00:05:59 +08:00
alex8088
127b09ebd8 docs: update 2023-01-07 13:23:15 +08:00
alex8088
efd21c0433 chore(deps): update all non-major dependencies 2023-01-07 13:12:40 +08:00
alex8088
9883399bf3 chore(deps): update vite to 4.0.4 2023-01-07 13:07:24 +08:00
alex8088
62e8be1c34 chore(worker): use toRelativePath helper 2023-01-07 12:57:09 +08:00
alex8088
336b4292eb feat: static asset handling 2023-01-04 22:53:22 +08:00
alex8088
f7b4146c56 fix: output duplicate log in vscode debugging #75 2022-12-28 21:54:20 +08:00
alex8088
a5de5b36f5 chore(bytecodePlugin): KiB to kB 2022-12-28 21:27:53 +08:00
alex8088
edd9ccb286 release: v1.0.16 2022-12-12 00:03:33 +08:00
alex8088
ad1ca5dcd7 fix: output format check 2022-12-11 21:57:31 +08:00
alex8088
95736032f2 fix: invalid output format check 2022-12-11 21:16:23 +08:00
alex8088
3365e87041 chore(deps): update vite to 4.0.0 2022-12-11 20:54:19 +08:00
alex8088
b4d1e7466b chore(deps): update esbuild and magic-string 2022-12-11 20:52:19 +08:00
alex8088
9d5cd15a3f chore(deps): update all non-major dependencies 2022-12-11 20:48:31 +08:00
alex8088
d086e0d51a fix: NODE_ENV is incorrect in vite 4.x 2022-12-10 23:26:10 +08:00
alex8088
6676f60aae feat: vite 4.x support (#69) 2022-12-10 21:52:07 +08:00
alex8088
09705f7099 release: v1.0.15 2022-12-05 00:19:30 +08:00
alex8088
16d9932bac feat: specify env prefixes for vite's loadEnv and export it 2022-12-04 01:59:03 +08:00
alex8088
ffb2426e75 feat: support mode and command conditional config 2022-12-04 01:24:25 +08:00
alex8088
767aee7464 perf: build compatilibity target for Electron 22 2022-12-01 21:33:05 +08:00
alex8088
067c333aeb perf: do not externalize node builtin modules for the renderer (#61) 2022-11-19 14:24:12 +08:00
alex8088
e72e1ea056 release: v1.0.14 2022-11-13 01:28:47 +08:00
alex8088
23ae7636cd fix(bytecodePlugin): replace bytecode module regex (#54) 2022-11-13 01:19:58 +08:00
alex8088
21e10c023a docs: update 2022-11-12 00:18:46 +08:00
alex8088
230d09c2a0 release: v1.0.13 2022-11-11 21:23:16 +08:00
alex8088
baf538cfe4 fix(bytecodePlugin): bytecode loader injection and chunk module parsing errors (#49) 2022-11-11 20:48:23 +08:00
alex8088
41ff7372c2 fix: output format error under multiple entries 2022-11-11 20:40:37 +08:00
alex8088
cf1151ef5f fix: incorrect replace __dirname/_filename 2022-11-08 20:29:44 +08:00
alex8088
10f5ae64a3 refactor: plugins 2022-11-08 20:17:22 +08:00
alex8088
28d069c453 feat: support for node worker (#51) 2022-11-08 00:36:22 +08:00
alex8088
bf0c83835a fix: unreachable code 2022-11-02 23:51:42 +08:00
alex8088
f81f57efbf release: v1.0.12 2022-11-02 02:00:30 +08:00
alex8088
1668547dbf docs: update 2022-11-02 01:56:45 +08:00
alex8088
97ad5fa726 feat: make a SWC plugin to emit decorator metadata (#48) 2022-11-02 01:34:19 +08:00
alex8088
6bae6ac6b4 fix: use modulePreload.polyfill instead polyfillModulePreload 2022-11-01 23:06:22 +08:00
alex8088
39f548a30a chore: update deps 2022-11-01 22:58:32 +08:00
alex8088
caafa1355d feat: add --skipBuild flag to preview command (#44) 2022-10-29 13:10:20 +08:00
alex8088
97a62b75f2 feat: monorepo support 2022-10-23 21:23:27 +08:00
alex.wei
a62b20359b
Merge pull request #39 from ianstormtaylor/patch-1
perf: use `require.resolve` to find electron package
2022-10-23 01:39:20 +08:00
Ian Storm Taylor
d0476dd91e
Use require.resolve to find electron package
Fixes #38
2022-10-21 13:22:01 -04:00
alex8088
229c398235 release: v1.0.11 2022-10-16 14:35:07 +08:00
alex8088
f09a482c66 feat: externalize deps plugin 2022-10-15 14:53:33 +08:00
alex8088
f8749646ee chore: update issue template docs link 2022-10-08 00:19:32 +08:00
alex8088
811e6a1e1e release: v1.0.10 2022-10-07 13:49:51 +08:00
alex8088
59e1d93166 fix: compatible with the latest version of Electron 2022-10-07 13:36:50 +08:00
alex8088
c49068258d perf: build compatilibity target for Electron 21 2022-09-27 20:40:32 +08:00
alex8088
cf08073f08 perf: bytecode compilation log print format 2022-09-26 19:44:54 +08:00
alex8088
969dc30e50 perf: disable gzip-compressed size reporting, increase build performance 2022-09-26 19:35:09 +08:00
alex8088
fab097e367 feat: export splitVendorChunk from vite 2022-09-25 00:33:44 +08:00
alex8088
51b9f74457 refactor: load config file 2022-09-25 00:18:13 +08:00
alex8088
dd6322f9c8 chore: function style 2022-09-24 23:58:10 +08:00
alex8088
8556a45cab perf: the bytecodePlugin transform arrow function by default 2022-09-22 18:54:29 +08:00
alex8088
6e04db0a14 docs: update 2022-09-19 20:56:51 +08:00
alex8088
555d4b43a9 release: v1.0.9 2022-09-19 18:36:51 +08:00
alex8088
19bfb2a829 fix: in the specified non-production mode, the base path is wrong 2022-09-19 18:33:56 +08:00
alex8088
556fe95f20 fix: specify a config file error 2022-09-19 18:32:39 +08:00
alex8088
da50aa9246 docs: update 2022-09-19 03:09:30 +08:00
alex8088
9132b24751 feat: source code protection 2022-09-19 03:06:37 +08:00
alex8088
4779bcf911 chore: add electron-vite mode node env 2022-09-18 17:06:06 +08:00
alex8088
cd0bbb6cfb perf: cache electron executable path 2022-09-18 01:46:02 +08:00
alex8088
c6bade23b5 chore: use node prefix 2022-09-17 23:47:48 +08:00
alex8088
4e53a28650 docs: update 2022-09-11 17:41:06 +08:00
alex8088
5db96e2a36 docs: update 2022-09-11 17:37:48 +08:00
alex8088
bf27b464d9 release: v1.0.8 2022-09-11 17:00:07 +08:00
alex8088
2132eccb28 docs: update 2022-09-11 16:54:48 +08:00
alex8088
714798fd56 chore: disable build.watch in production 2022-09-09 01:28:36 +08:00
alex8088
1bd15486ea feat: main process and preload scripts support hot reloading (#7) 2022-09-08 23:49:30 +08:00
alex8088
006418ab14 release: v1.0.7 2022-08-29 18:57:43 +08:00
alex8088
ab6d089aa6 docs: update 2022-08-29 18:35:46 +08:00
alex8088
b62b8ec0bb feat: add sourcemap cli options for debugging 2022-08-29 18:35:21 +08:00
41 changed files with 5498 additions and 2480 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

View File

@ -1,6 +1,5 @@
name: "\U0001F41E Bug Report"
description: Report an issue with electron-vite
labels: ['bug', 'triage']
body:
- type: markdown
attributes:
@ -45,7 +44,7 @@ body:
required: true
- label: Read the [Contributing Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md).
required: true
- label: Read the [docs](https://github.com/alex8088/electron-vite#readme).
- label: Read the [docs](https://electron-vite.org).
required: true
- label: Check that there isn't [already an issue](https://github.com/alex8088/electron-vite/issues) that reports the same bug to avoid creating a duplicate.
required: true

View File

@ -1 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions & Discussions
url: https://github.com/alex8088/electron-vite/discussions
about: Use GitHub discussions for message-board style questions and discussions.

View File

@ -40,7 +40,7 @@ body:
required: true
- label: Read the [Contributing Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md).
required: true
- label: Read the [docs](https://github.com/alex8088/electron-vite#readme).
- label: Read the [docs](https://electron-vite.org).
required: true
- label: Check that there isn't [already an issue](https://github.com/alex8088/electron-vite/issues) that reports the same bug to avoid creating a duplicate.
- label: Check that there isn't [already an issue](https://github.com/alex8088/electron-vite/issues) that requests the same feature to avoid creating a duplicate.
required: true

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
node_modules
dist
.DS_Store
.eslintcache
*.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,362 @@
### 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_)
- feat(cli): support --noSandbox option for dev and preview command
- perf: build compatilibity target for Electron 27
### v1.0.28 (_2023-09-18_)
- feat(cli): supports specifying electron entry file ([#270](https://github.com/alex8088/electron-vite/issues/270))
- fix(externalizeDepsPlugin): supports subpath
- perf: build compatilibity target for Electron 26
- chore(types): add .json?commonjs-external&asset typing
### v1.0.27 (_2023-08-01_)
- chore: remove preinstall script
### v1.0.26 (_2023-07-30_)
- feat(cli): add CLI `--inspect[-brk]` to support debugging without IDEs ([#231](https://github.com/alex8088/electron-vite/issues/231))
- feat(types): add process.env.ELECTRON_RENDERER_URL type
- feat(types): add Vite importMeta types
- perf: spawn Electron process using parent's stdios ([#236](https://github.com/alex8088/electron-vite/issues/236))
- chore: update user config interface jsdoc
- chore(deps): update pnpm to v8
- chore(deps): update prettier to v3
- chore(deps): update @typescript-eslint/\* to v6
### v1.0.25 (_2023-07-11_)
- fix: remove node resolve condition for preload [#204](https://github.com/alex8088/electron-vite/issues/204)
- fix(asset): asset handling error when hot reloading
- chore(deps): update all non-major dependencies
- chore(deps): update fs-extra to v11
- chore(deps): update @types/node to v18
- chore(deps): update typescript to 5.0.4
- chore(deps): update vite to 4.4.2
- chore(deps): update esbuild to v0.18
- chore(deps): update rollup to 3.26.2
### v1.0.24 (_2023-06-25_)
- fix(bytecodePlugin): bytecode loader relative path is incorrect
- perf: ignore `browser` field and additional `node` condition for main config
### v1.0.23 (_2023-06-04_)
- feat: supports ES build target for renderer [#174](https://github.com/alex8088/electron-vite/issues/174)
- revert: chore: remove process env define [#159](https://github.com/alex8088/electron-vite/issues/174)
- perf: build compatilibity target for Electron 25
- chore(deps): update all non-major dependencies
### v1.0.22 (_2023-04-23_)
- feat(cli): add --rendererOnly flag to dev command
- perf: build compatilibity target for Electron 24
- chore: remove process env define
- chore: typo error messages
### v1.0.21 (_2023-03-27_)
- fix(bytecodePlugin): bytecode loader is not referenced correctly in the chunks
- fix(bytecodePlugin): sub-chunks are not compliled in vite 4
- perf: always disable build.modulePreload in main and preload config
- chore(deps): update esbuild to 0.17
- chore(deps): update vite to 4.2.1
### v1.0.20 (_2023-03-12_)
- feat: support for renderer debugging [#130](https://github.com/alex8088/electron-vite/issues/130)
- fix(asset): asset path is not resolved correctly when outDir is specified [#117](https://github.com/alex8088/electron-vite/issues/117)
- fix: specified renderer outDir is not parsed correctly
- fix(bytecodePlugin): not work in monorepo [#128](https://github.com/alex8088/electron-vite/issues/128)
- perf: build compatilibity target for Electron 23
- perf: print log
- chore(deps): update all non-major dependencies
- chore(deps): update vite to 4.1.4
- chore(deps): update rollup to 3.18
- chore(deps): update magic-string to 0.30.0
### v1.0.19 (_2023-02-06_)
- feat(bytecodePlugin): protect strings [#91](https://github.com/alex8088/electron-vite/issues/91)
- fix(bytecodePlugin): escape protected strings (thanks to [@jeremyben](https://github.com/jeremyben))
### v1.0.18 (_2023-01-16_)
- feat(asset): support for WebAssembly in the main process
- fix(asset): wasm must be suffixed with `?loader`
### v1.0.17 (_2023-01-08_)
- feat: static asset handling
- fix: output duplicate log in vscode debugging [#75](https://github.com/alex8088/electron-vite/issues/75)
- chore(bytecodePlugin): KiB to kB
- chore(worker): use toRelativePath helper
- chore(deps): update all non-major dependencies
- chore(deps): update vite to 4.0.4
### v1.0.16 (_2022-12-12_)
- feat: vite 4.x support [#69](https://github.com/alex8088/electron-vite/issues/69)
- fix: `NODE_ENV` is incorrect in vite 4.x [#70](https://github.com/alex8088/electron-vite/issues/70)
- fix: invalid output format check
- fix: output format check
- chore(deps): update all non-major dependencies
- chore(deps): update esbuild and magic-string
- chore(deps): update vite to 4.0.0
### v1.0.15 (_2022-12-05_)
- feat: support mode and command conditional config
- feat: specify env prefixes for vite's loadEnv and export it
- perf: build compatilibity target for Electron 22
- perf: do not externalize node builtin modules for the renderer [#61](https://github.com/alex8088/electron-vite/issues/61)
### v1.0.14 (_2022-11-13_)
- fix(bytecodePlugin): replace bytecode module regex
### v1.0.13 (_2022-11-11_)
- feat: support for node worker
- refactor: plugins
- fix(swcPlugin): unreachable code
- fix(bytecodePlugin): bytecode loader injection and chunk module parsing errors [#49](https://github.com/alex8088/electron-vite/issues/49)
- fix: incorrect replace `__dirname`/`__filename` in config file
- fix: output format error under multiple entries
### v1.0.12 (_2022-11-02_)
- feat: support monorepo (by @ianstormtaylor)
- feat: add `--skipBuild` flag to preview command
- feat: make a SWC plugin to support TypeScript decorators (`emitDecoratorMetadata`)
- fix: use `modulePreload.polyfill` instead `polyfillModulePreload`
- chore: update deps
### v1.0.11 (_2022-10-16_)
- feat: externalize deps plugin
### v1.0.10 (_2022-10-07_)
- feat: export splitVendorChunk from vite
- fix: compatible with the latest version of Electron
- refactor: load config file
- perf: the bytecodePlugin transform arrow function by default
- perf: disable gzip-compressed size reporting, increase build performance
- perf: bytecode compilation log print format
- perf: build compatilibity target for Electron 21
### v1.0.9 (_2022-09-19_)
- feat: source code protection
- fix: specify a config file error
- fix: in the specified non-production mode, the `base` path is wrong
- perf: cache electron executable path
- chore: add electron-vite mode node env
- chore: use node prefix
### v1.0.8 (_2022-09-11_)
- feat: the main process and preload scripts support hot reloading [#7](https://github.com/alex8088/electron-vite/issues/7)
### v1.0.7 (_2022-08-29_)
- feat: add sourcemap cli options for debugging
### v1.0.6 (_2022-08-25_)
- chore: update deps

View File

@ -9,17 +9,17 @@
<p align="center">
<img src="https://img.shields.io/npm/v/electron-vite?color=6988e6&label=version">
<img src="https://img.shields.io/github/license/alex8088/wx-vue-next?color=blue" alt="license" />
<img src="https://img.shields.io/github/license/alex8088/electron-vite?color=blue" alt="license" />
</p>
<p align="center">
<a href="https://evite.netlify.app/">Documentation</a> |
<a href="https://evite.netlify.app/guide/">Getting Started</a> |
<a href="https://electron-vite.org">Documentation</a> |
<a href="https://electron-vite.org/guide">Getting Started</a> |
<a href="https://github.com/alex8088/quick-start/tree/master/packages/create-electron">create-electron</a>
</p>
<p align="center">
<a href="https://cn-evite.netlify.app/">中文文档</a>
<a href="https://cn.electron-vite.org">中文文档</a>
</p>
<br />
@ -27,11 +27,15 @@
## Features
- ⚡️ Inherit all the benefits of Vite and use the same way as [Vite](https://vitejs.dev).
- 📦The main process, renderers and preload scripts are all built with Vite.
- 🛠The main process, renderers and preload scripts Vite configuration combined into one file.
- 💡Pre-configured for Electron, don't worry about configuration.
- 🚀HMR for renderer processes.
- ⚡️ [Vite](https://vitejs.dev) powered and use the same way.
- 🛠 Pre-configure with sensible defaults optimized for Electron.
- 💡 Optimize asset handling for Electron main process.
- 🚀 Fast HMR & hot reloading.
- 🔥 Isolated build for multi-entry application development.
- ✨ Simplify multi-threading development.
- 🔒 Compile code to v8 bytecode to protect source code.
- 🔌 Easy to debug in IDEs such as VSCode or WebStorm.
- 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more.
## Usage
@ -55,7 +59,7 @@ In a project where `electron-vite` is installed, you can use `electron-vite` bin
}
```
### Configuring
### Configuration
When running `electron-vite` from the command line, electron-vite will automatically try to resolve a config file named `electron.vite.config.js` inside project root. The most basic config file looks like this:
@ -74,38 +78,24 @@ export default {
}
```
### Use HMR in Renderer
In order to use the renderer process HMR, you need to use the `environment variables` to determine whether the window browser loads a local html file or a local URL.
```js
function createWindow() {
// Create the browser window
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js')
}
})
// Load the remote URL for development or the local html file for production
if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}
}
```
### Getting Started
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
npm init @quick-start/electron
npm create @quick-start/electron@latest
```
Currently supported template presets include:
| JavaScript | TypeScript |
| :--------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------: |
| [vanilla](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vanilla) | [vanilla-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vanilla-ts) |
| [vue](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vue) | [vue-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vue-ts) |
| [react](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/react) | [react-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/react-ts) |
| [svelte](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/svelte) | [svelte-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/svelte-ts) |
| [solid](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/solid) | [solid-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/solid-ts) |
## Contribution
See [Contributing Guide](CONTRIBUTING.md).

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"
}
}
}
}

32
bin/electron-bytecode.cjs Normal file
View File

@ -0,0 +1,32 @@
const vm = require('vm')
const v8 = require('v8')
const wrap = require('module').wrap
v8.setFlagsFromString('--no-lazy')
v8.setFlagsFromString('--no-flush-bytecode')
let code = ''
process.stdin.setEncoding('utf-8')
process.stdin.on('readable', () => {
const data = process.stdin.read()
if (data !== null) {
code += data
}
})
process.stdin.on('end', () => {
try {
if (typeof code !== 'string') {
throw new Error(`javascript code must be string. ${typeof code} was given.`)
}
const script = new vm.Script(wrap(code), { produceCachedData: true })
const bytecodeBuffer = script.createCachedData()
process.stdout.write(bytecodeBuffer)
} catch (error) {
console.error(error)
}
})

View File

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

69
eslint.config.js Normal file
View File

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

112
node.d.ts vendored Normal file
View File

@ -0,0 +1,112 @@
// node worker
declare module '*?nodeWorker' {
import type { Worker, WorkerOptions } from 'node:worker_threads'
export default function (options: WorkerOptions): Worker
}
// module path
declare module '*?modulePath' {
const src: string
export default src
}
// node asset
declare module '*?asset' {
const src: string
export default src
}
declare module '*?asset&asarUnpack' {
const src: string
export default src
}
declare module '*.json?commonjs-external&asset' {
const src: string
export default src
}
// native node module
declare module '*.node' {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const node: any
export default node
}
// node wasm
declare module '*.wasm?loader' {
const loadWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>
export default loadWasm
}
// build-in process env
declare namespace NodeJS {
interface ProcessEnv {
/**
* Vite's dev server address for Electron renderers.
*/
readonly ELECTRON_RENDERER_URL?: string
}
}
// Refer to Vite's ImportMeta type declarations
// <https://github.com/vitejs/vite/blob/main/packages/vite/types/importMeta.d.ts>
interface ImportMetaEnv {
MODE: string
DEV: boolean
PROD: boolean
}
interface ImportGlobOptions<Eager extends boolean, AsType extends string> {
/**
* Import type for the import url.
*/
as?: AsType
/**
* Import as static or dynamic
*
* @default false
*/
eager?: Eager
/**
* Import only the specific named export. Set to `default` to import the default export.
*/
import?: string
/**
* Custom queries
*/
query?: string | Record<string, string | number | boolean>
/**
* Search files also inside `node_modules/` and hidden directories (e.g. `.git/`). This might have impact on performance.
*
* @default false
*/
exhaustive?: boolean
}
interface KnownAsTypeMap {
raw: string
url: string
worker: Worker
}
interface ImportGlobFunction {
/**
* Import a list of files with a glob pattern.
*
* https://vitejs.dev/guide/features.html#glob-import
*/
<Eager extends boolean, As extends string, T = As extends keyof KnownAsTypeMap ? KnownAsTypeMap[As] : unknown>(
glob: string | string[],
options?: ImportGlobOptions<Eager, As>
): (Eager extends true ? true : false) extends true ? Record<string, T> : Record<string, () => Promise<T>>
<M>(glob: string | string[], options?: ImportGlobOptions<false, string>): Record<string, () => Promise<M>>
<M>(glob: string | string[], options: ImportGlobOptions<true, string>): Record<string, M>
}
interface ImportMeta {
url: string
readonly env: ImportMetaEnv
glob: ImportGlobFunction
}

View File

@ -1,19 +1,29 @@
{
"name": "electron-vite",
"version": "1.0.6",
"version": "5.0.0",
"description": "Electron build tooling based on Vite",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./node": {
"types": "./node.d.ts"
},
"./package.json": "./package.json"
},
"bin": {
"electron-vite": "bin/electron-vite.js"
},
"files": [
"bin",
"dist"
"dist",
"node.d.ts"
],
"engines": {
"node": ">=12.2.0"
"node": "^20.19.0 || >=22.12.0"
},
"packageManager": "pnpm@10.12.4",
"author": "Alex Wei<https://github.com/alex8088>",
"license": "MIT",
"repository": {
@ -23,7 +33,7 @@
"bugs": {
"url": "https://github.com/alex8088/electron-vite/issues"
},
"homepage": "https://github.com/alex8088/electron-vite#readme",
"homepage": "https://electron-vite.org",
"keywords": [
"electron",
"vite",
@ -32,9 +42,9 @@
],
"scripts": {
"format": "prettier --write .",
"lint": "eslint --ext .ts src/**",
"lint": "eslint --cache .",
"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": {
"pre-commit": "npx lint-staged",
@ -50,30 +60,50 @@
]
},
"peerDependencies": {
"vite": "^3.0.0"
"@swc/core": "^1.0.0",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
}
},
"devDependencies": {
"@microsoft/api-extractor": "^7.29.5",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-typescript": "^8.4.0",
"@types/node": "16.11.22",
"@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.35.1",
"eslint": "^8.22.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"fs-extra": "^10.1.0",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"rollup": "^2.78.1",
"simple-git-hooks": "^2.8.0",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vite": "^3.0.9"
"@eslint/js": "^9.37.0",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.1.4",
"@swc/core": "^1.13.5",
"@types/babel__core": "^7.20.5",
"@types/node": "^22.18.11",
"eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"globals": "^16.4.0",
"lint-staged": "^16.2.4",
"prettier": "^3.6.2",
"rollup": "^4.52.4",
"rollup-plugin-dts": "^6.2.3",
"rollup-plugin-rm": "^1.0.2",
"simple-git-hooks": "^2.13.1",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.1",
"vite": "^7.1.10"
},
"dependencies": {
"cac": "^6.7.12",
"esbuild": "^0.14.54",
"picocolors": "^1.0.0"
"@babel/core": "^7.28.4",
"@babel/plugin-transform-arrow-functions": "^7.27.1",
"cac": "^6.7.14",
"esbuild": "^0.25.11",
"magic-string": "^0.30.19",
"picocolors": "^1.1.1"
},
"pnpm": {
"onlyBuiltDependencies": [
"@swc/core",
"esbuild",
"simple-git-hooks"
]
}
}

3933
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

41
rollup.config.ts Normal file
View File

@ -0,0 +1,41 @@
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', declaration: true, declarationDir: 'dist/types' } }),
resolve()
],
treeshake: {
moduleSideEffects: false
}
},
{
input: 'dist/types/index.d.ts',
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.
const colors = require('picocolors')
const fs = require('fs')
import colors from 'picocolors'
import fs from 'node:fs'
const msgPath = process.argv[2]
const msg = fs.readFileSync(msgPath, 'utf-8').trim()
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)) {
console.log()

View File

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

View File

@ -1,7 +1,8 @@
import { cac } from 'cac'
import colors from 'picocolors'
import { LogLevel, createLogger } from 'vite'
import { InlineConfig } from './config'
import { type LogLevel, createLogger } from 'vite'
import type { InlineConfig } from './config'
import { version } from '../package.json'
const cli = cac('electron-vite')
@ -20,7 +21,24 @@ interface GlobalCLIOptions {
m?: string
mode?: string
ignoreConfigWarning?: boolean
sourcemap?: boolean
w?: boolean
watch?: boolean
outDir?: string
entry?: string
}
interface DevCLIOptions {
inspect?: boolean | string
inspectBrk?: boolean | string
remoteDebuggingPort?: string
noSandbox?: boolean
rendererOnly?: boolean
}
interface PreviewCLIOptions {
noSandbox?: boolean
skipBuild?: boolean
}
function createInlineConfig(root: string, options: GlobalCLIOptions): InlineConfig {
@ -32,7 +50,9 @@ function createInlineConfig(root: string, options: GlobalCLIOptions): InlineConf
clearScreen: options.clearScreen,
ignoreConfigWarning: options.ignoreConfigWarning,
build: {
outDir: options.outDir
sourcemap: options.sourcemap,
outDir: options.outDir,
...(options.w || options.watch ? { watch: {} } : null)
}
}
}
@ -45,19 +65,51 @@ cli
.option('-f, --filter <filter>', `[string] filter debug logs`)
.option('-m, --mode <mode>', `[string] set env mode`)
.option('--ignoreConfigWarning', `[boolean] ignore config warning`)
.option('--sourcemap', `[boolean] output source maps for debug (default: false)`)
.option('--outDir <dir>', `[string] output directory (default: out)`)
.option('--entry <file>', `[string] specify electron entry file`)
// dev
cli
.command('[root]', 'start dev server and electron app')
.alias('serve')
.alias('dev')
.action(async (root: string, options: GlobalCLIOptions) => {
.option('-w, --watch', `[boolean] rebuilds when main process or preload script modules have changed on disk`)
.option('--inspect [port]', `[boolean | number] enable V8 inspector on the specified port`)
.option('--inspectBrk [port]', `[boolean | number] enable V8 inspector on the specified port`)
.option('--remoteDebuggingPort <port>', `[string] port for remote debugging`)
.option('--noSandbox', `[boolean] forces renderer process to run un-sandboxed`)
.option('--rendererOnly', `[boolean] only dev server for the renderer`)
.action(async (root: string, options: DevCLIOptions & GlobalCLIOptions) => {
if (options.remoteDebuggingPort) {
process.env.REMOTE_DEBUGGING_PORT = options.remoteDebuggingPort
}
if (options.inspect) {
process.env.V8_INSPECTOR_PORT = typeof options.inspect === 'number' ? `${options.inspect}` : '5858'
}
if (options.inspectBrk) {
process.env.V8_INSPECTOR_BRK_PORT = typeof options.inspectBrk === 'number' ? `${options.inspectBrk}` : '5858'
}
if (options.noSandbox) {
process.env.NO_SANDBOX = '1'
}
if (options['--']) {
process.env.ELECTRON_CLI_ARGS = JSON.stringify(options['--'])
}
if (options.entry) {
process.env.ELECTRON_ENTRY = options.entry
}
const { createServer } = await import('./server')
const inlineConfig = createInlineConfig(root, options)
try {
await createServer(inlineConfig)
await createServer(inlineConfig, { rendererOnly: options.rendererOnly })
} catch (e) {
const error = e as Error
createLogger(options.logLevel).error(
@ -73,6 +125,10 @@ cli.command('build [root]', 'build for production').action(async (root: string,
const { build } = await import('./build')
const inlineConfig = createInlineConfig(root, options)
if (options.entry) {
process.env.ELECTRON_ENTRY = options.entry
}
try {
await build(inlineConfig)
} catch (e) {
@ -85,12 +141,26 @@ cli.command('build [root]', 'build for production').action(async (root: string,
// preview
cli
.command('preview [root]', 'start electron app to preview production build')
.action(async (root: string, options: GlobalCLIOptions) => {
.option('--noSandbox', `[boolean] forces renderer process to run un-sandboxed`)
.option('--skipBuild', `[boolean] skip build`)
.action(async (root: string, options: PreviewCLIOptions & GlobalCLIOptions) => {
const { preview } = await import('./preview')
const inlineConfig = createInlineConfig(root, options)
if (options.noSandbox) {
process.env.NO_SANDBOX = '1'
}
if (options.entry) {
process.env.ELECTRON_ENTRY = options.entry
}
if (options['--']) {
process.env.ELECTRON_CLI_ARGS = JSON.stringify(options['--'])
}
try {
await preview(inlineConfig)
await preview(inlineConfig, { skipBuild: options.skipBuild })
} catch (e) {
const error = e as Error
createLogger(options.logLevel).error(colors.red(`error during preview electron app:\n${error.stack}`), { error })
@ -99,6 +169,6 @@ cli
})
cli.help()
cli.version(require('../package.json').version)
cli.version(version)
cli.parse()

View File

@ -1,63 +1,145 @@
import * as path from 'path'
import * as fs from 'fs'
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 {
UserConfig as ViteConfig,
UserConfigExport as UserViteConfigExport,
ConfigEnv,
Plugin,
LogLevel,
type UserConfig as ViteConfig,
type ConfigEnv,
type PluginOption,
type Plugin,
type BuildEnvironmentOptions as ViteBuildOptions,
type LogLevel,
createLogger,
mergeConfig,
normalizePath
} from 'vite'
import { build } from 'esbuild'
import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugin'
import { isObject, dynamicImport } from './utils'
import {
electronMainConfigPresetPlugin,
electronMainConfigValidatorPlugin,
electronPreloadConfigPresetPlugin,
electronPreloadConfigValidatorPlugin,
electronRendererConfigPresetPlugin,
electronRendererConfigValidatorPlugin
} from './plugins/electron'
import assetPlugin from './plugins/asset'
import workerPlugin from './plugins/worker'
import importMetaPlugin from './plugins/importMeta'
import esmShimPlugin from './plugins/esmShim'
import modulePathPlugin from './plugins/modulePath'
import isolateEntriesPlugin from './plugins/isolateEntries'
import { type ExternalOptions, externalizeDepsPlugin } from './plugins/externalizeDeps'
import { type BytecodeOptions, bytecodePlugin } from './plugins/bytecode'
import { isObject, isFilePathESM, deepClone, asyncFlatten } from './utils'
export { defineConfig as defineViteConfig } from 'vite'
interface IsolatedEntriesMixin {
/**
* Build each entry point as an isolated bundle without code splitting.
*
* When enabled, each entry will include all its dependencies inline,
* preventing automatic code splitting across entries and ensuring each
* output file is fully standalone.
*
* **Important**: When using `isolatedEntries` in `preload` config, you
* should also disable `build.externalizeDeps` to ensure third-party dependencies
* from `node_modules` are bundled together, which is required for Electron
* sandbox support.
*
* @experimental
* @default false
*/
isolatedEntries?: boolean
}
interface ExternalizeDepsMixin {
/**
* Options pass on to `externalizeDeps` plugin in electron-vite.
*
* Automatically externalize dependencies.
*
* @default true
*/
externalizeDeps?: boolean | ExternalOptions
}
interface BytecodeMixin {
/**
* Options pass on to `bytecode` plugin in electron-vite.
* https://electron-vite.org/guide/source-code-protection#options
*
* Compile source code to v8 bytecode.
*/
bytecode?: boolean | BytecodeOptions
}
interface MainBuildOptions extends ViteBuildOptions, ExternalizeDepsMixin, BytecodeMixin {}
interface PreloadBuildOptions extends ViteBuildOptions, ExternalizeDepsMixin, BytecodeMixin, IsolatedEntriesMixin {}
interface RendererBuildOptions extends ViteBuildOptions, IsolatedEntriesMixin {}
interface BaseViteConfig<T> extends Omit<ViteConfig, 'build'> {
/**
* Build specific options
*/
build?: T
}
export interface MainViteConfig extends BaseViteConfig<MainBuildOptions> {}
export interface PreloadViteConfig extends BaseViteConfig<PreloadBuildOptions> {}
export interface RendererViteConfig extends BaseViteConfig<RendererBuildOptions> {}
export interface UserConfig {
/**
* Vite config options for electron main process
*
* https://cn.vitejs.dev/config/
* @see https://vitejs.dev/config/
*/
main?: ViteConfig & { configFile?: string | false }
main?: MainViteConfig
/**
* Vite config options for electron renderer process
*
* https://cn.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://cn.vitejs.dev/config/
* @see https://vitejs.dev/config/
*/
preload?: ViteConfig & { configFile?: string | false }
preload?: PreloadViteConfig
}
export interface UserConfigSchema {
/**
* Vite config options for electron main process
*
* https://cn.vitejs.dev/config/
*/
main?: UserViteConfigExport
/**
* Vite config options for electron renderer process
*
* https://cn.vitejs.dev/config/
*/
renderer?: UserViteConfigExport
/**
* Vite config options for electron preload files
*
* https://cn.vitejs.dev/config/
*/
preload?: UserViteConfigExport
export type ElectronViteConfigFnObject = (env: ConfigEnv) => UserConfig
export type ElectronViteConfigFnPromise = (env: ConfigEnv) => Promise<UserConfig>
export type ElectronViteConfigFn = (env: ConfigEnv) => UserConfig | Promise<UserConfig>
export type ElectronViteConfigExport =
| UserConfig
| Promise<UserConfig>
| ElectronViteConfigFnObject
| ElectronViteConfigFnPromise
| ElectronViteConfigFn
/**
* Type helper to make it easier to use `electron.vite.config.*`
* accepts a direct {@link UserConfig} object, or a function that returns it.
* The function receives a object that exposes two properties:
* `command` (either `'build'` or `'serve'`), and `mode`.
*/
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
}
export type InlineConfig = Omit<ViteConfig, 'base'> & {
@ -66,16 +148,6 @@ export type InlineConfig = Omit<ViteConfig, 'base'> & {
ignoreConfigWarning?: boolean
}
export type UserConfigExport = UserConfigSchema | Promise<UserConfigSchema>
/**
* Type helper to make it easier to use `electron.vite.config.ts`
* accepts a direct {@link UserConfig} object, or a function that returns it.
*/
export function defineConfig(config: UserConfigExport): UserConfigExport {
return config
}
export interface ResolvedConfig {
config?: UserConfig
configFile?: string
@ -90,11 +162,7 @@ export async function resolveConfig(
const config = inlineConfig
const mode = inlineConfig.mode || defaultMode
config.mode = mode
if (mode === 'production') {
process.env.NODE_ENV = 'production'
}
process.env.NODE_ENV = defaultMode
let userConfig: UserConfig | undefined
let configFileDependencies: string[] = []
@ -105,6 +173,7 @@ export async function resolveConfig(
mode,
command
}
const loadResult = await loadConfigFromFile(
configEnv,
configFile,
@ -112,49 +181,130 @@ export async function resolveConfig(
config.logLevel,
config.ignoreConfigWarning
)
if (loadResult) {
const root = config.root
delete config.root
delete config.configFile
config.configFile = false
const outDir = config.build?.outDir
if (loadResult.config.main) {
const mainViteConfig: ViteConfig = mergeConfig(loadResult.config.main, deepClone(config))
const mainViteConfig: MainViteConfig = mergeConfig(loadResult.config.main, deepClone(config))
mainViteConfig.mode = inlineConfig.mode || mainViteConfig.mode || defaultMode
if (outDir) {
resetOutDir(mainViteConfig, outDir, 'main')
}
mergePlugins(mainViteConfig, electronMainVitePlugin({ root }))
const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(mainViteConfig)
const builtInMainPlugins: PluginOption[] = [
electronMainConfigPresetPlugin({ root }),
electronMainConfigValidatorPlugin(),
assetPlugin(),
workerPlugin(),
modulePathPlugin(
mergeConfig(
{
plugins: [
electronMainConfigPresetPlugin({ root }),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
},
mainViteConfig
)
),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
mainViteConfig.plugins = builtInMainPlugins.concat(mainViteConfig.plugins || [])
loadResult.config.main = mainViteConfig
loadResult.config.main.configFile = false
}
if (loadResult.config.preload) {
const preloadViteConfig: ViteConfig = mergeConfig(loadResult.config.preload, deepClone(config))
const preloadViteConfig: PreloadViteConfig = mergeConfig(loadResult.config.preload, deepClone(config))
preloadViteConfig.mode = inlineConfig.mode || preloadViteConfig.mode || defaultMode
if (outDir) {
resetOutDir(preloadViteConfig, outDir, 'preload')
}
mergePlugins(preloadViteConfig, electronPreloadVitePlugin({ root }))
const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(preloadViteConfig)
const builtInPreloadPlugins: PluginOption[] = [
electronPreloadConfigPresetPlugin({ root }),
electronPreloadConfigValidatorPlugin(),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
if (preloadViteConfig.build?.isolatedEntries) {
builtInPreloadPlugins.push(
isolateEntriesPlugin(
mergeConfig(
{
plugins: [
electronPreloadConfigPresetPlugin({ root }),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
},
preloadViteConfig
)
)
)
}
preloadViteConfig.plugins = builtInPreloadPlugins.concat(preloadViteConfig.plugins)
loadResult.config.preload = preloadViteConfig
loadResult.config.preload.configFile = false
}
if (loadResult.config.renderer) {
const rendererViteConfig: ViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config))
const rendererViteConfig: RendererViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config))
rendererViteConfig.mode = inlineConfig.mode || rendererViteConfig.mode || defaultMode
if (outDir) {
resetOutDir(rendererViteConfig, outDir, 'renderer')
}
mergePlugins(rendererViteConfig, electronRendererVitePlugin({ root }))
const builtInRendererPlugins: PluginOption[] = [
electronRendererConfigPresetPlugin({ root }),
electronRendererConfigValidatorPlugin()
]
if (rendererViteConfig.build?.isolatedEntries) {
builtInRendererPlugins.push(
isolateEntriesPlugin(
mergeConfig(
{
plugins: [electronRendererConfigPresetPlugin({ root })]
},
rendererViteConfig
)
)
)
}
rendererViteConfig.plugins = builtInRendererPlugins.concat(rendererViteConfig.plugins || [])
loadResult.config.renderer = rendererViteConfig
loadResult.config.renderer.configFile = false
}
userConfig = loadResult.config
@ -172,10 +322,6 @@ export async function resolveConfig(
return resolved
}
function deepClone<T>(data: T): T {
return JSON.parse(JSON.stringify(data))
}
function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void {
let userOutDir = config.build?.outDir
if (outDir === userOutDir) {
@ -188,9 +334,36 @@ function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): voi
}
}
function mergePlugins(config: ViteConfig, plugins: Plugin[]): void {
const userPlugins = config.plugins || []
config.plugins = userPlugins.concat(plugins)
async function resolveConfigDrivenPlugins(config: MainViteConfig | PreloadViteConfig): Promise<PluginOption[]> {
const userPlugins = (await asyncFlatten(config.plugins || [])).filter(Boolean) as Plugin[]
const configDrivenPlugins: PluginOption[] = []
const hasExternalizeDepsPlugin = userPlugins.some(p => p.name === 'vite:externalize-deps')
if (!hasExternalizeDepsPlugin) {
const externalOptions = config.build?.externalizeDeps ?? true
if (externalOptions) {
isOptions<ExternalOptions>(externalOptions)
? configDrivenPlugins.push(externalizeDepsPlugin(externalOptions))
: configDrivenPlugins.push(externalizeDepsPlugin())
}
}
const hasBytecodePlugin = userPlugins.some(p => p.name === 'vite:bytecode')
if (!hasBytecodePlugin) {
const bytecodeOptions = config.build?.bytecode
if (bytecodeOptions) {
isOptions<BytecodeOptions>(bytecodeOptions)
? configDrivenPlugins.push(bytecodePlugin(bytecodeOptions))
: configDrivenPlugins.push(bytecodePlugin())
}
}
return configDrivenPlugins
}
function isOptions<T extends object>(value: boolean | T): value is T {
return typeof value === 'object' && value !== null
}
const CONFIG_FILE_NAME = 'electron.vite.config'
@ -206,14 +379,13 @@ export async function loadConfigFromFile(
config: UserConfig
dependencies: string[]
}> {
let resolvedPath: string
let isESM = false
if (configFile && /^vite.config.(js)|(ts)|(mjs)|(cjs)$/.test(configFile)) {
if (configFile && /^vite.config.(js|ts|mjs|cjs|mts|cts)$/.test(configFile)) {
throw new Error(`config file cannot be named ${configFile}.`)
}
resolvedPath = configFile ? path.resolve(configFile) : findConfigFile(configRoot, ['js', 'ts', 'mjs', 'cjs'])
const resolvedPath = configFile
? path.resolve(configFile)
: findConfigFile(configRoot, ['js', 'ts', 'mjs', 'cjs', 'mts', 'cts'])
if (!resolvedPath) {
return {
@ -223,92 +395,33 @@ export async function loadConfigFromFile(
}
}
if (resolvedPath.endsWith('.mjs')) {
isESM = true
}
if (resolvedPath.endsWith('.js')) {
const pkg = path.join(configRoot, 'package.json')
if (fs.existsSync(pkg)) {
isESM = require(pkg).type === 'module'
}
}
const configFilePath = resolvedPath
const isESM = isFilePathESM(resolvedPath)
try {
const bundled = await bundleConfigFile(resolvedPath)
const { code, dependencies } = await bundleConfigFile(resolvedPath, isESM)
const configExport = await loadConfigFormBundledFile(configRoot, resolvedPath, code, isESM)
if (!isESM) {
resolvedPath = path.resolve(configRoot, `${CONFIG_FILE_NAME}.mjs`)
fs.writeFileSync(resolvedPath, bundled.code)
}
const fileUrl = require('url').pathToFileURL(resolvedPath)
const userConfig = (await dynamicImport(fileUrl)).default
if (!isESM) {
fs.unlinkSync(resolvedPath)
}
const config = await (typeof userConfig === 'function' ? userConfig() : userConfig)
const config = await (typeof configExport === 'function' ? configExport(configEnv) : configExport)
if (!isObject(config)) {
throw new Error(`config must export or return an object`)
}
const configRequired: string[] = []
let mainConfig
if (config.main) {
const mainViteConfig = config.main
mainConfig = await (typeof mainViteConfig === 'function' ? mainViteConfig(configEnv) : mainViteConfig)
if (!isObject(mainConfig)) {
throw new Error(`main config must export or return an object`)
if (!ignoreConfigWarning) {
const missingFields = ['main', 'renderer', 'preload'].filter(field => !config[field])
if (missingFields.length > 0) {
createLogger(logLevel).warn(
`${colors.yellow(colors.bold('(!)'))} ${colors.yellow(`${missingFields.join(' and ')} config is missing`)}\n`
)
}
} 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 {
path: normalizePath(configFilePath),
config: {
main: mainConfig,
renderer: rendererConfig,
preload: preloadConfig
},
dependencies: bundled.dependencies
path: normalizePath(resolvedPath),
config,
dependencies
}
} catch (e) {
createLogger(logLevel).error(colors.red(`failed to load config from ${configFilePath}`), { error: e as Error })
createLogger(logLevel).error(colors.red(`failed to load config from ${resolvedPath}`), { error: e as Error })
throw e
}
}
@ -323,16 +436,25 @@ function findConfigFile(configRoot: string, extensions: string[]): string {
return ''
}
async function bundleConfigFile(fileName: string): Promise<{ code: string; dependencies: string[] }> {
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: ['node20'],
platform: 'node',
bundle: true,
format: 'esm',
format: isESM ? 'esm' : 'cjs',
sourcemap: false,
metafile: true,
define: {
__dirname: dirnameVarName,
__filename: filenameVarName,
'import.meta.url': importMetaUrlVarName
},
plugins: [
{
name: 'externalize-deps',
@ -351,14 +473,16 @@ async function bundleConfigFile(fileName: string): Promise<{ code: string; depen
{
name: 'replace-import-meta',
setup(build): void {
build.onLoad({ filter: /\.[jt]s$/ }, async args => {
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: contents
.replace(/\bimport\.meta\.url\b/g, JSON.stringify(`file://${args.path}`))
.replace(/\b__dirname\b/g, JSON.stringify(path.dirname(args.path)))
.replace(/\b__filename\b/g, JSON.stringify(args.path))
loader: args.path.endsWith('ts') ? 'ts' : 'js',
contents: injectValues + contents
}
})
}
@ -371,3 +495,46 @@ async function bundleConfigFile(fileName: string): Promise<{ code: string; depen
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<ElectronViteConfigExport> {
if (isESM) {
const fileNameTmp = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${Date.now()}.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(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
}
}

161
src/electron.ts Normal file
View File

@ -0,0 +1,161 @@
import path from 'node:path'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import { type ChildProcess, spawn } from 'node:child_process'
import { loadPackageData } from './utils'
const _require = createRequire(import.meta.url)
const ensureElectronEntryFile = (root = process.cwd()): void => {
if (process.env.ELECTRON_ENTRY) return
const pkg = loadPackageData()
if (pkg) {
if (!pkg.main) {
throw new Error('No entry point found for electron app, please add a "main" field to package.json')
} else {
const entryPath = path.resolve(root, pkg.main)
if (!fs.existsSync(entryPath)) {
throw new Error(`No electron app entry file found: ${entryPath}`)
}
}
} else {
throw new Error('Not found: package.json')
}
}
const getElectronMajorVer = (): string => {
let majorVer = process.env.ELECTRON_MAJOR_VER || ''
if (!majorVer) {
const pkg = _require.resolve('electron/package.json')
if (fs.existsSync(pkg)) {
const version = _require(pkg).version
majorVer = version.split('.')[0]
process.env.ELECTRON_MAJOR_VER = 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 {
let electronExecPath = process.env.ELECTRON_EXEC_PATH || ''
if (!electronExecPath) {
const electronModulePath = path.dirname(_require.resolve('electron'))
const pathFile = path.join(electronModulePath, 'path.txt')
let executablePath
if (fs.existsSync(pathFile)) {
executablePath = fs.readFileSync(pathFile, 'utf-8')
}
if (executablePath) {
electronExecPath = path.join(electronModulePath, 'dist', executablePath)
process.env.ELECTRON_EXEC_PATH = electronExecPath
} else {
throw new Error('Electron uninstall')
}
}
return electronExecPath
}
export function getElectronNodeTarget(): string {
const electronVer = getElectronMajorVer()
const nodeVer = {
'39': '22.20',
'38': '22.19',
'37': '22.16',
'36': '22.14',
'35': '22.14',
'34': '20.18',
'33': '20.18',
'32': '20.16',
'31': '20.14',
'30': '20.11',
'29': '20.9',
'28': '18.18',
'27': '18.17',
'26': '18.16',
'25': '18.15',
'24': '18.14',
'23': '18.12',
'22': '16.17'
}
if (electronVer && parseInt(electronVer) > 10) {
let target = nodeVer[electronVer]
if (!target) target = Object.values(nodeVer).reverse()[0]
return 'node' + target
}
return ''
}
export function getElectronChromeTarget(): string {
const electronVer = getElectronMajorVer()
const chromeVer = {
'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',
'26': '116',
'25': '114',
'24': '112',
'23': '110',
'22': '108'
}
if (electronVer && parseInt(electronVer) > 10) {
let target = chromeVer[electronVer]
if (!target) target = Object.values(chromeVer).reverse()[0]
return 'chrome' + target
}
return ''
}
export function startElectron(root: string | undefined): ChildProcess {
ensureElectronEntryFile(root)
const electronPath = getElectronPath()
const isDev = process.env.NODE_ENV_ELECTRON_VITE === 'development'
const args: string[] = process.env.ELECTRON_CLI_ARGS ? JSON.parse(process.env.ELECTRON_CLI_ARGS) : []
if (!!process.env.REMOTE_DEBUGGING_PORT && isDev) {
args.push(`--remote-debugging-port=${process.env.REMOTE_DEBUGGING_PORT}`)
}
if (!!process.env.V8_INSPECTOR_PORT && isDev) {
args.push(`--inspect=${process.env.V8_INSPECTOR_PORT}`)
}
if (!!process.env.V8_INSPECTOR_BRK_PORT && isDev) {
args.push(`--inspect-brk=${process.env.V8_INSPECTOR_BRK_PORT}`)
}
if (process.env.NO_SANDBOX === '1') {
args.push('--no-sandbox')
}
const entry = process.env.ELECTRON_ENTRY || '.'
const ps = spawn(electronPath, [entry].concat(args), { stdio: 'inherit' })
ps.on('close', process.exit)
return ps
}

View File

@ -1,6 +1,9 @@
export { LogLevel, createLogger, splitVendorChunkPlugin } from 'vite'
export { type LogLevel, createLogger, mergeConfig } from 'vite'
export * from './config'
export { createServer } from './server'
export { build } from './build'
export { preview } from './preview'
export * from './plugin'
export { loadEnv } from './utils'
export * from './plugins/bytecode'
export * from './plugins/externalizeDeps'
export * from './plugins/swc'

View File

@ -1,363 +0,0 @@
import path from 'path'
import * as fs from 'fs'
import colors from 'picocolors'
import { builtinModules, createRequire } from 'module'
import { Plugin, mergeConfig, normalizePath } from 'vite'
export interface ElectronPluginOptions {
root?: string
}
function findLibEntry(root: string, scope: string): string {
for (const name of ['index', scope]) {
for (const ext of ['js', 'ts', 'mjs', 'cjs']) {
const entryFile = path.resolve(root, 'src', scope, `${name}.${ext}`)
if (fs.existsSync(entryFile)) {
return entryFile
}
}
}
return ''
}
function findInput(root: string, scope = 'renderer'): string {
const rendererDir = path.resolve(root, 'src', scope, 'index.html')
if (fs.existsSync(rendererDir)) {
return rendererDir
}
return ''
}
function processEnvDefine(): Record<string, string> {
return {
'process.env': `process.env`,
'global.process.env': `global.process.env`,
'globalThis.process.env': `globalThis.process.env`
}
}
export function electronMainVitePlugin(options?: ElectronPluginOptions): Plugin[] {
return [
{
name: 'vite:electron-main-preset-config',
apply: 'build',
enforce: 'pre',
config(config): void {
const root = options?.root || process.cwd()
const electornVer = getElectronMainVer(root)
const nodeTarget = getElectronNodeTarget(electornVer)
const defaultConfig = {
build: {
outDir: path.resolve(root, 'out', 'main'),
target: nodeTarget,
lib: {
entry: findLibEntry(root, 'main'),
formats: ['cjs']
},
rollupOptions: {
external: ['electron', 'sqlite3', ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {
entryFileNames: '[name].js'
}
},
minify: false
}
}
const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
config.build = buildConfig
config.define = config.define || {}
config.define = { ...processEnvDefine(), ...config.define }
config.envPrefix = config.envPrefix || 'MAIN_VITE_'
}
},
{
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
if (!lib) {
throw new Error('build lib field required for the electron vite main config')
} else {
if (!lib.entry) {
throw new Error('build entry field required for the electron vite main config')
}
if (!lib.formats) {
throw new Error('build format field required for the electron vite main config')
} else if (!lib.formats.includes('cjs')) {
throw new Error('the electron vite main config build lib format must be cjs')
}
}
}
}
]
}
export function electronPreloadVitePlugin(options?: ElectronPluginOptions): Plugin[] {
return [
{
name: 'vite:electron-preload-preset-config',
apply: 'build',
enforce: 'pre',
config(config): void {
const root = options?.root || process.cwd()
const electornVer = getElectronMainVer(root)
const nodeTarget = getElectronNodeTarget(electornVer)
const defaultConfig = {
build: {
outDir: path.resolve(root, 'out', 'preload'),
target: nodeTarget,
rollupOptions: {
external: ['electron', ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {
entryFileNames: '[name].js'
}
},
minify: false
}
}
const build = config.build || {}
const rollupOptions = build.rollupOptions || {}
if (!rollupOptions.input) {
defaultConfig.build['lib'] = {
entry: findLibEntry(root, 'preload'),
formats: ['cjs']
}
} else {
if (!rollupOptions.output) {
defaultConfig.build.rollupOptions.output['format'] = 'cjs'
}
}
const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
config.build = buildConfig
config.define = config.define || {}
config.define = { ...processEnvDefine(), ...config.define }
config.envPrefix = config.envPrefix || 'PRELOAD_VITE_'
}
},
{
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
if (!lib) {
const rollupOptions = build.rollupOptions
if (!rollupOptions?.input) {
throw new Error('build lib field required for the electron vite preload config')
} else {
const output = rollupOptions?.output
if (output) {
const formats = Array.isArray(output) ? output : [output]
if (!formats.some(f => f !== '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[] {
return [
{
name: 'vite:electron-renderer-preset-config',
enforce: 'pre',
config(config): void {
const root = options?.root || process.cwd()
config.base = config.mode === 'production' ? './' : config.base
config.root = config.root || './src/renderer'
const electornVer = getElectronMainVer(root)
const chromeTarget = getElectronChromeTarget(electornVer)
const emptyOutDir = (): boolean => {
let outDir = config.build?.outDir
if (outDir) {
if (!path.isAbsolute(outDir)) {
outDir = path.resolve(root, outDir)
}
const resolvedRoot = normalizePath(path.resolve(root))
return normalizePath(outDir).startsWith(resolvedRoot + '/')
}
return true
}
const defaultConfig = {
build: {
outDir: path.resolve(root, 'out', 'renderer'),
target: chromeTarget,
polyfillModulePreload: false,
rollupOptions: {
input: findInput(root),
external: [...builtinModules.flatMap(m => [m, `node:${m}`])]
},
minify: false,
emptyOutDir: emptyOutDir()
}
}
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
if (!build.target) {
throw new Error('build target required for the electron vite renderer config')
} else {
const targets = Array.isArray(build.target) ? build.target : [build.target]
if (targets.some(t => !t.startsWith('chrome'))) {
throw new Error('the electron vite renderer config build target must be chrome')
}
}
const rollupOptions = build.rollupOptions
if (!rollupOptions.input) {
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory`))
throw new Error('build rollupOptions input field required for the electron vite renderer config')
}
}
}
]
}
export function electronConfigServeVitePlugin(options: {
configFile: string
configFileDependencies: string[]
}): Plugin {
const getShortName = (file: string, root: string): string => {
return file.startsWith(root + '/') ? path.posix.relative(root, file) : file
}
return {
name: 'vite:electron-config-serve',
apply: 'serve',
handleHotUpdate({ file, server }): void {
const { config } = server
const logger = config.logger
const shortFile = getShortName(file, config.root)
const isConfig = file === options.configFile
const isConfigDependency = options.configFileDependencies.some(name => file === path.resolve(name))
if (isConfig || isConfigDependency) {
logger.info(`[config change] ${colors.dim(shortFile)}`)
logger.info(colors.green(`${path.relative(process.cwd(), file)} changed, restarting server...`), {
clear: true,
timestamp: true
})
try {
server.restart()
} catch (e) {
logger.error(colors.red('failed to restart server'), { error: e as Error })
}
}
}
}
}
function getElectronMainVer(root: string): string {
let mainVer = process.env.ELECTRON_MAIN_VER || ''
if (!mainVer) {
const electronModulePath = path.resolve(root, 'node_modules', 'electron')
const pkg = path.join(electronModulePath, 'package.json')
if (fs.existsSync(pkg)) {
const require = createRequire(import.meta.url)
const version = require(pkg).version
mainVer = version.split('.')[0]
process.env.ELECTRON_MAIN_VER = mainVer
}
}
return mainVer
}
function getElectronNodeTarget(electronVer: string): string {
const nodeVer = {
'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) {
return 'node' + nodeVer[electronVer]
}
return ''
}
function getElectronChromeTarget(electronVer: string): string {
const chromeVer = {
'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) {
return 'chrome' + chromeVer[electronVer]
}
return ''
}

144
src/plugins/asset.ts Normal file
View File

@ -0,0 +1,144 @@
import path from 'node:path'
import fs from 'node:fs/promises'
import type { SourceMapInput } from 'rollup'
import { type Plugin, normalizePath } from 'vite'
import MagicString from 'magic-string'
import { cleanUrl, getHash, toRelativePath } from '../utils'
import { supportImportMetaPaths } from '../electron'
const nodeAssetRE = /__VITE_NODE_ASSET__([\w$]+)__/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 wasmHelperCode = `
import { join } from 'path'
import { readFile } from 'fs/promises'
export default async function loadWasm(file, importObject = {}) {
const wasmBuffer = await readFile(join(__dirname, file))
const result = await WebAssembly.instantiate(wasmBuffer, importObject)
return result.instance
}
`
export default function assetPlugin(): Plugin {
let publicDir = ''
const publicAssetPathCache = new Map<string, string>()
const assetCache = new Map<string, string>()
const isImportMetaPathSupported = supportImportMetaPaths()
return {
name: 'vite:node-asset',
apply: 'build',
enforce: 'pre',
buildStart(): void {
publicAssetPathCache.clear()
assetCache.clear()
},
configResolved(config): void {
publicDir = config.publicDir
},
resolveId(id): string | void {
if (id === wasmHelperId) {
return id
}
},
async load(id): Promise<string | void> {
if (id === wasmHelperId) {
return wasmHelperCode
}
if (id.startsWith('\0') || !assetImportRE.test(id)) {
return
}
let referenceId: string
const file = cleanUrl(id)
if (publicDir && file.startsWith(publicDir)) {
const hash = getHash(file)
if (!publicAssetPathCache.get(hash)) {
publicAssetPathCache.set(hash, file)
}
referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__`
} else {
const cached = assetCache.get(file)
if (cached) {
referenceId = cached
} else {
const source = await fs.readFile(file)
const hash = this.emitFile({
type: 'asset',
name: path.basename(file),
source: source as unknown as Uint8Array
})
referenceId = `__VITE_NODE_ASSET__${hash}__`
assetCache.set(file, referenceId)
}
}
if (assetRE.test(id)) {
const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname'
if (assetUnpackRE.test(id)) {
return `
import { join } from 'path'
export default join(${dirnameExpr}, ${referenceId}).replace('app.asar', 'app.asar.unpacked')`
} else {
return `
import { join } from 'path'
export default join(${dirnameExpr}, ${referenceId})`
}
}
if (id.endsWith('.node')) {
return `export default require(${referenceId})`
}
if (id.endsWith('.wasm?loader')) {
return `
import loadWasm from ${JSON.stringify(wasmHelperId)}
export default importObject => loadWasm(${referenceId}, importObject)`
}
},
renderChunk(code, chunk, { sourcemap, dir }): { code: string; map: SourceMapInput } | null {
let match: RegExpExecArray | null
let s: MagicString | undefined
nodeAssetRE.lastIndex = 0
while ((match = nodeAssetRE.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
})
}
nodePublicAssetRE.lastIndex = 0
while ((match = nodePublicAssetRE.exec(code))) {
s ||= new MagicString(code)
const [full, hash] = match
const filename = publicAssetPathCache.get(hash)!
const outputFilepath = toRelativePath(filename, normalizePath(path.join(dir!, chunk.fileName)))
const replacement = JSON.stringify(outputFilepath)
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true
})
}
if (s) {
return {
code: s.toString(),
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
}
}
return null
}
}
}

View File

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

428
src/plugins/bytecode.ts Normal file
View File

@ -0,0 +1,428 @@
import path from 'node:path'
import { spawn } from 'node:child_process'
import { createRequire } from 'node:module'
import colors from 'picocolors'
import { type Plugin, type Logger, type LibraryOptions, normalizePath } from 'vite'
import * as babel from '@babel/core'
import MagicString from 'magic-string'
import type { SourceMapInput, OutputChunk, OutputOptions } from 'rollup'
import { getElectronPath } from '../electron'
import { toRelativePath } from '../utils'
// Inspired by https://github.com/bytenode/bytenode
const _require = createRequire(import.meta.url)
function getBytecodeCompilerPath(): string {
return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', 'electron-bytecode.cjs')
}
function compileToBytecode(code: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
let data = Buffer.from([])
const electronPath = getElectronPath()
const bytecodePath = getBytecodeCompilerPath()
const proc = spawn(electronPath, [bytecodePath], {
env: { ELECTRON_RUN_AS_NODE: '1' },
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
})
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 = [
`"use strict";`,
`const fs = require("fs");`,
`const path = require("path");`,
`const vm = require("vm");`,
`const v8 = require("v8");`,
`const Module = require("module");`,
`v8.setFlagsFromString("--no-lazy");`,
`v8.setFlagsFromString("--no-flush-bytecode");`,
`const FLAG_HASH_OFFSET = 12;`,
`const SOURCE_HASH_OFFSET = 8;`,
`let dummyBytecode;`,
`function setFlagHashHeader(bytecodeBuffer) {`,
` if (!dummyBytecode) {`,
` const script = new vm.Script("", {`,
` produceCachedData: true`,
` });`,
` dummyBytecode = script.createCachedData();`,
` }`,
` dummyBytecode.slice(FLAG_HASH_OFFSET, FLAG_HASH_OFFSET + 4).copy(bytecodeBuffer, FLAG_HASH_OFFSET);`,
`};`,
`function getSourceHashHeader(bytecodeBuffer) {`,
` return bytecodeBuffer.slice(SOURCE_HASH_OFFSET, SOURCE_HASH_OFFSET + 4);`,
`};`,
`function buffer2Number(buffer) {`,
` let ret = 0;`,
` ret |= buffer[3] << 24;`,
` ret |= buffer[2] << 16;`,
` ret |= buffer[1] << 8;`,
` ret |= buffer[0];`,
` return ret;`,
`};`,
`Module._extensions[".jsc"] = Module._extensions[".cjsc"] = function (module, filename) {`,
` const bytecodeBuffer = fs.readFileSync(filename);`,
` if (!Buffer.isBuffer(bytecodeBuffer)) {`,
` throw new Error("BytecodeBuffer must be a buffer object.");`,
` }`,
` 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 {
chunkAlias?: string | string[]
transformArrowFunctions?: boolean
removeBundleJS?: boolean
protectedStrings?: string[]
}
/**
* Compile source code to v8 bytecode.
*
* @deprecated use `build.bytecode` config option instead
*/
export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') {
return null
}
const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options
const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
const transformAllChunks = _chunkAlias.length === 0
const isBytecodeChunk = (chunkName: string): boolean => {
return transformAllChunks || _chunkAlias.some(alias => alias === chunkName)
}
const plugins: babel.PluginItem[] = []
if (transformArrowFunctions) {
plugins.push('@babel/plugin-transform-arrow-functions')
}
if (protectedStrings.length > 0) {
plugins.push([protectStringsPlugin, { protectedStrings: new Set(protectedStrings) }])
}
const shouldTransformBytecodeChunk = plugins.length !== 0
const _transform = (code: string, sourceMaps: boolean = false): { code: string; map?: SourceMapInput } | null => {
const re = babel.transform(code, { plugins, sourceMaps })
return re ? { code: re.code || '', map: re.map } : null
}
const useStrict = '"use strict";'
const bytecodeModuleLoader = 'bytecode-loader.cjs'
let logger: Logger
let supported = false
return {
name: 'vite:bytecode',
apply: 'build',
enforce: 'post',
configResolved(config): void {
if (supported) {
return
}
logger = config.logger
const useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config')
if (useInRenderer) {
config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.'))
return
}
const build = config.build
const resolvedOutputs = resolveBuildOutputs(build.rollupOptions.output, build.lib)
if (resolvedOutputs) {
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
const output = outputs[0]
if (output.format === 'es') {
config.logger.warn(
colors.yellow(
'bytecodePlugin does not support ES module, please remove "type": "module" ' +
'in package.json or set the "build.rollupOptions.output.format" option to "cjs".'
)
)
}
supported = output.format === 'cjs' && !useInRenderer
}
},
renderChunk(code, chunk, { sourcemap }): { code: string; map?: SourceMapInput } | null {
if (supported && isBytecodeChunk(chunk.name) && shouldTransformBytecodeChunk) {
return _transform(code, !!sourcemap)
}
return null
},
async generateBundle(_, output): Promise<void> {
if (!supported) {
return
}
const _chunks = Object.values(output)
const chunks = _chunks.filter(chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name)) as 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)
await Promise.all(
bundles.map(async name => {
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(_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 && !moduleInfo.isExternal) {
const { importers, dynamicImporters } = moduleInfo
for (const importerId of importers) idsToHandle.add(importerId)
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
}
}
_code = hasBytecodeMoudle
? _code.replace(
/("use strict";)|('use strict';)/,
`${useStrict}\n${getBytecodeLoaderBlock(chunk.fileName)}`
)
: _code
}
chunk.code = _code
}
}
})
)
if (bytecodeChunkCount && !_chunks.some(ass => ass.type === 'asset' && ass.fileName === bytecodeModuleLoader)) {
this.emitFile({
type: 'asset',
source: bytecodeModuleLoaderCode.join('\n') + '\n',
name: 'Bytecode Loader File',
fileName: bytecodeModuleLoader
})
}
},
writeBundle(_, output): void {
if (supported) {
const bytecodeChunkCount = Object.keys(output).filter(chunk => bytecodeChunkExtensionRE.test(chunk)).length
logger.info(`${colors.green(``)} ${bytecodeChunkCount} chunks compiled into bytecode.`)
}
}
}
}
function resolveBuildOutputs(
outputs: OutputOptions | OutputOptions[] | undefined,
libOptions: LibraryOptions | false
): OutputOptions | OutputOptions[] | undefined {
if (libOptions && !Array.isArray(outputs)) {
const libFormats = libOptions.formats || []
return libFormats.map(format => ({ ...outputs, format }))
}
return outputs
}
interface ProtectStringsPluginState extends babel.PluginPass {
opts: { protectedStrings: Set<string> }
}
function protectStringsPlugin(api: typeof babel & babel.ConfigAPI): babel.PluginObj<ProtectStringsPluginState> {
const { types: t } = api
function createFromCharCodeFunction(value: string): babel.types.CallExpression {
const charCodes = Array.from(value).map(s => s.charCodeAt(0))
const charCodeLiterals = charCodes.map(code => t.numericLiteral(code))
// String.fromCharCode
const memberExpression = t.memberExpression(t.identifier('String'), t.identifier('fromCharCode'))
// String.fromCharCode(...arr)
const callExpression = t.callExpression(memberExpression, [t.spreadElement(t.identifier('arr'))])
// return String.fromCharCode(...arr)
const returnStatement = t.returnStatement(callExpression)
// function (arr) { return ... }
const functionExpression = t.functionExpression(null, [t.identifier('arr')], t.blockStatement([returnStatement]))
// (function(...) { ... })([x, x, x])
return t.callExpression(functionExpression, [t.arrayExpression(charCodeLiterals)])
}
return {
name: 'protect-strings-plugin',
visitor: {
StringLiteral(path, state) {
// obj['property']
if (path.parentPath.isMemberExpression({ property: path.node, computed: true })) {
return
}
// { 'key': value }
if (path.parentPath.isObjectProperty({ key: path.node, computed: false })) {
return
}
// require('fs')
if (
path.parentPath.isCallExpression() &&
t.isIdentifier(path.parentPath.node.callee) &&
path.parentPath.node.callee.name === 'require' &&
path.parentPath.node.arguments[0] === path.node
) {
return
}
// Only CommonJS is supported, import declaration and export declaration checks are ignored
const { value } = path.node
if (state.opts.protectedStrings.has(value)) {
path.replaceWith(createFromCharCodeFunction(value))
}
},
TemplateLiteral(path, state) {
// Must be a pure static template literal
// expressions must be empty (no ${variables})
// quasis must have only one element (meaning the entire string is a single static part).
if (path.node.expressions.length > 0 || path.node.quasis.length !== 1) {
return
}
// Extract the raw value of the template literal
// path.node.quasis[0].value.raw is used to get the raw string, including escape sequences
// path.node.quasis[0].value.cooked is used to get the processed/cooked string (with escape sequences handled)
const value = path.node.quasis[0].value.cooked
if (value && state.opts.protectedStrings.has(value)) {
path.replaceWith(createFromCharCodeFunction(value))
}
}
}
}
}

412
src/plugins/electron.ts Normal file
View File

@ -0,0 +1,412 @@
import path from 'node:path'
import fs from 'node:fs'
import { builtinModules } from 'node:module'
import colors from 'picocolors'
import { type Plugin, type LibraryOptions, mergeConfig, normalizePath } from 'vite'
import type { OutputOptions } from 'rollup'
import { getElectronNodeTarget, getElectronChromeTarget, supportESM } from '../electron'
import { loadPackageData } from '../utils'
export interface ElectronPluginOptions {
root?: string
}
function findLibEntry(root: string, scope: string): string | undefined {
for (const name of ['index', scope]) {
for (const ext of ['js', 'ts', 'mjs', 'cjs']) {
const entryFile = path.resolve(root, 'src', scope, `${name}.${ext}`)
if (fs.existsSync(entryFile)) {
return entryFile
}
}
}
return undefined
}
function findInput(root: string, scope = 'renderer'): string {
const rendererDir = path.resolve(root, 'src', scope, 'index.html')
if (fs.existsSync(rendererDir)) {
return rendererDir
}
return ''
}
function processEnvDefine(): Record<string, string> {
return {
'process.env': `process.env`,
'global.process.env': `global.process.env`,
'globalThis.process.env': `globalThis.process.env`
}
}
function resolveBuildOutputs(
outputs: OutputOptions | OutputOptions[] | undefined,
libOptions: LibraryOptions | false
): OutputOptions | OutputOptions[] | undefined {
if (libOptions && !Array.isArray(outputs)) {
const libFormats = libOptions.formats || []
return libFormats.map(format => ({ ...outputs, format }))
}
return outputs
}
export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
return {
name: 'vite:electron-main-config-preset',
apply: 'build',
enforce: 'pre',
config(config): void {
const root = options?.root || process.cwd()
const nodeTarget = getElectronNodeTarget()
const pkg = loadPackageData() || { type: 'commonjs' }
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
const defaultConfig = {
resolve: {
browserField: false,
mainFields: ['module', 'jsnext:main', 'jsnext'],
conditions: ['node']
},
build: {
outDir: path.resolve(root, 'out', 'main'),
target: nodeTarget,
assetsDir: 'chunks',
rollupOptions: {
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {}
},
reportCompressedSize: false,
minify: false
}
}
const build = config.build || {}
const rollupOptions = build.rollupOptions || {}
if (!rollupOptions.input) {
const libOptions = build.lib
const outputOptions = rollupOptions.output
defaultConfig.build['lib'] = {
entry: findLibEntry(root, 'main'),
formats:
libOptions && libOptions.formats && libOptions.formats.length > 0
? []
: [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
}
} else {
defaultConfig.build.rollupOptions.output['format'] = format
}
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
build.assetsDir || defaultConfig.build.assetsDir,
'[name]-[hash].[ext]'
)
const buildConfig = mergeConfig(defaultConfig.build, build)
config.build = buildConfig
config.resolve = mergeConfig(defaultConfig.resolve, config.resolve || {})
config.define = config.define || {}
config.define = { ...processEnvDefine(), ...config.define }
config.envPrefix = config.envPrefix || ['MAIN_VITE_', 'VITE_']
config.publicDir = config.publicDir || 'resources'
// do not copy public dir
config.build.copyPublicDir = false
// module preload polyfill does not apply to nodejs (main process)
config.build.modulePreload = false
// enable ssr build
config.build.ssr = true
config.build.ssrEmitAssets = true
config.ssr = { ...config.ssr, ...{ noExternal: true } }
}
}
}
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 rollupOptions = build.rollupOptions
if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
throw new Error(
'An entry point is required in the electron vite main config, ' +
'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
)
}
const resolvedOutputs = resolveBuildOutputs(rollupOptions.output, libOptions)
if (resolvedOutputs) {
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
if (outputs.length > 1) {
throw new Error('The electron vite main config does not support multiple outputs.')
} else {
const outpout = outputs[0]
if (['es', 'cjs'].includes(outpout.format || '')) {
if (outpout.format === 'es' && !supportESM()) {
throw new Error(
'The electron vite main config output format does not support "es", ' +
'you can upgrade electron to the latest version or switch to "cjs" format.'
)
}
} else {
throw new Error(
`The electron vite main config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
)
}
}
}
}
}
}
export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
return {
name: 'vite:electron-preload-config-preset',
apply: 'build',
enforce: 'pre',
config(config): void {
const root = options?.root || process.cwd()
const nodeTarget = getElectronNodeTarget()
const pkg = loadPackageData() || { type: 'commonjs' }
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
const defaultConfig = {
ssr: {
resolve: {
conditions: ['module', 'browser', 'development|production'],
mainFields: ['browser', 'module', 'jsnext:main', 'jsnext']
}
},
build: {
outDir: path.resolve(root, 'out', 'preload'),
target: nodeTarget,
assetsDir: 'chunks',
rollupOptions: {
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {}
},
reportCompressedSize: false,
minify: false
}
}
const build = config.build || {}
const rollupOptions = build.rollupOptions || {}
if (!rollupOptions.input) {
const libOptions = build.lib
const outputOptions = rollupOptions.output
defaultConfig.build['lib'] = {
entry: findLibEntry(root, 'preload'),
formats:
libOptions && libOptions.formats && libOptions.formats.length > 0
? []
: [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
}
} else {
defaultConfig.build.rollupOptions.output['format'] = format
}
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
build.assetsDir || defaultConfig.build.assetsDir,
'[name]-[hash].[ext]'
)
const buildConfig = mergeConfig(defaultConfig.build, build)
config.build = buildConfig
const resolvedOutputs = resolveBuildOutputs(config.build.rollupOptions!.output, config.build.lib || false)
if (resolvedOutputs) {
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
if (outputs.find(({ format }) => format === 'es')) {
if (Array.isArray(config.build.rollupOptions!.output)) {
config.build.rollupOptions!.output.forEach(output => {
if (output.format === 'es') {
output['entryFileNames'] = '[name].mjs'
output['chunkFileNames'] = '[name]-[hash].mjs'
}
})
} else {
config.build.rollupOptions!.output!['entryFileNames'] = '[name].mjs'
config.build.rollupOptions!.output!['chunkFileNames'] = '[name]-[hash].mjs'
}
}
}
config.define = config.define || {}
config.define = { ...processEnvDefine(), ...config.define }
config.envPrefix = config.envPrefix || ['PRELOAD_VITE_', 'VITE_']
config.publicDir = config.publicDir || 'resources'
// do not copy public dir
config.build.copyPublicDir = false
// module preload polyfill does not apply to nodejs (preload scripts)
config.build.modulePreload = false
// enable ssr build
config.build.ssr = true
config.build.ssrEmitAssets = true
config.ssr = mergeConfig(defaultConfig.ssr, config.ssr || {})
config.ssr.noExternal = true
}
}
}
export function electronPreloadConfigValidatorPlugin(): Plugin {
return {
name: 'vite:electron-preload-config-validator',
apply: 'build',
enforce: 'post',
configResolved(config): void {
const build = config.build
if (!build.target) {
throw new Error('build.target option is required in the electron vite preload config.')
} else {
const targets = Array.isArray(build.target) ? build.target : [build.target]
if (targets.some(t => !t.startsWith('node'))) {
throw new Error('The electron vite preload config build.target must be "node?".')
}
}
const libOptions = build.lib
const rollupOptions = build.rollupOptions
if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
throw new Error(
'An entry point is required in the electron vite preload config, ' +
'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
)
}
const resolvedOutputs = resolveBuildOutputs(rollupOptions.output, libOptions)
if (resolvedOutputs) {
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
if (outputs.length > 1) {
throw new Error('The electron vite preload config does not support multiple outputs.')
} else {
const outpout = outputs[0]
if (['es', 'cjs'].includes(outpout.format || '')) {
if (outpout.format === 'es' && !supportESM()) {
throw new Error(
'The electron vite preload config output format does not support "es", ' +
'you can upgrade electron to the latest version or switch to "cjs" format.'
)
}
} else {
throw new Error(
`The electron vite preload config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
)
}
}
}
}
}
}
export function electronRendererConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
return {
name: 'vite:electron-renderer-config-preset',
enforce: 'pre',
config(config): void {
const root = options?.root || process.cwd()
config.base =
config.mode === 'production' || process.env.NODE_ENV_ELECTRON_VITE === 'production' ? './' : config.base
config.root = config.root || './src/renderer'
const chromeTarget = getElectronChromeTarget()
const emptyOutDir = (): boolean => {
let outDir = config.build?.outDir
if (outDir) {
if (!path.isAbsolute(outDir)) {
outDir = path.resolve(root, outDir)
}
const resolvedRoot = normalizePath(path.resolve(root))
return normalizePath(outDir).startsWith(resolvedRoot + '/')
}
return true
}
const defaultConfig = {
build: {
outDir: path.resolve(root, 'out', 'renderer'),
target: chromeTarget,
modulePreload: { polyfill: false },
rollupOptions: {
input: findInput(root)
},
reportCompressedSize: false,
minify: false,
emptyOutDir: emptyOutDir()
}
}
if (config.build?.outDir) {
config.build.outDir = path.resolve(root, config.build.outDir)
}
const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
config.build = buildConfig
config.envDir = config.envDir || path.resolve(root)
config.envPrefix = config.envPrefix || ['RENDERER_VITE_', 'VITE_']
}
}
}
export function electronRendererConfigValidatorPlugin(): Plugin {
return {
name: 'vite:electron-renderer-config-validator',
enforce: 'post',
configResolved(config): void {
if (config.base !== './' && config.base !== '/') {
config.logger.warn(colors.yellow('(!) Should not set "base" option for the electron vite renderer config.'))
}
const build = config.build
if (!build.target) {
throw new Error('build.target option is required in the electron vite renderer config.')
} else {
const targets = Array.isArray(build.target) ? build.target : [build.target]
if (targets.some(t => !t.startsWith('chrome') && !/^es((202\d{1})|next)$/.test(t))) {
config.logger.warn(
'The electron vite renderer config build.target is not "chrome?" or "es?". This could be a mistake.'
)
}
}
const rollupOptions = build.rollupOptions
if (!rollupOptions.input) {
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory.`))
throw new Error('build.rollupOptions.input option is required in the electron vite renderer config.')
}
}
}
}

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

@ -0,0 +1,74 @@
/*
* The core of this plugin was conceived by pi0 and is taken from the following repository:
* https://github.com/unjs/unbuild/blob/main/src/builder/plugins/cjs.ts
* license: https://github.com/unjs/unbuild/blob/main/LICENSE
*/
import MagicString from 'magic-string'
import type { SourceMapInput } from 'rollup'
import type { Plugin } from 'vite'
import { supportImportMetaPaths } from '../electron'
const CJSyntaxRe = /__filename|__dirname|require\(|require\.resolve\(/
const CJSShim_normal = `
// -- CommonJS Shims --
import __cjs_url__ from 'node:url';
import __cjs_path__ from 'node:path';
import __cjs_mod__ from 'node:module';
const __filename = __cjs_url__.fileURLToPath(import.meta.url);
const __dirname = __cjs_path__.dirname(__filename);
const require = __cjs_mod__.createRequire(import.meta.url);
`
const CJSShim_node_20_11 = `
// -- CommonJS Shims --
import __cjs_mod__ from 'node:module';
const __filename = import.meta.filename;
const __dirname = import.meta.dirname;
const require = __cjs_mod__.createRequire(import.meta.url);
`
const ESMStaticImportRe =
/(?<=\s|^|;)import\s*([\s"']*(?<imports>[\p{L}\p{M}\w\t\n\r $*,/{}@.]+)from\s*)?["']\s*(?<specifier>(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][\s;]*/gmu
interface StaticImport {
end: number
}
function findStaticImports(code: string): StaticImport[] {
const matches: StaticImport[] = []
for (const match of code.matchAll(ESMStaticImportRe)) {
matches.push({ end: (match.index || 0) + match[0].length })
}
return matches
}
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 }): { code: string; map?: SourceMapInput } | null {
if (format === 'es') {
if (code.includes(CJSShim) || !CJSyntaxRe.test(code)) {
return null
}
const lastESMImport = findStaticImports(code).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

@ -0,0 +1,45 @@
import { type Plugin, mergeConfig } from 'vite'
import { loadPackageData } from '../utils'
export interface ExternalOptions {
exclude?: string[]
include?: string[]
}
/**
* Automatically externalize dependencies.
*
* @deprecated use `build.externalizeDeps` config option instead
*/
export function externalizeDepsPlugin(options: ExternalOptions = {}): Plugin | null {
const { exclude = [], include = [] } = options
const pkg = loadPackageData() || {}
let deps = Object.keys(pkg.dependencies || {})
if (include.length) {
deps = deps.concat(include.filter(dep => dep.trim() !== ''))
}
if (exclude.length) {
deps = deps.filter(dep => !exclude.includes(dep))
}
deps = [...new Set(deps)]
return {
name: 'vite:externalize-deps',
enforce: 'pre',
config(config): void {
const defaultConfig = {
build: {
rollupOptions: {
external: deps.length > 0 ? [...deps, new RegExp(`^(${deps.join('|')})/.+`)] : []
}
}
}
const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
config.build = buildConfig
}
}
}

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

@ -0,0 +1,21 @@
import type { Plugin } from 'vite'
export default function importMetaPlugin(): Plugin {
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
}
}
}

View File

@ -0,0 +1,198 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */
import path from 'node:path'
import { type InlineConfig, type Plugin, type Logger, type LogLevel, build as viteBuild, mergeConfig } from 'vite'
import type { InputOptions, RollupOutput } from 'rollup'
import colors from 'picocolors'
import buildReporterPlugin from './buildReporter'
const VIRTUAL_ENTRY_ID = '\0virtual:isolate-entries'
const LogLevels: Record<LogLevel, number> = {
silent: 0,
error: 1,
warn: 2,
info: 3
}
export default function isolateEntriesPlugin(userConfig: InlineConfig): Plugin {
let logger: Logger
let entries: string[] | { [x: string]: string }[]
let transformedCount = 0
const assetCache = new Set<string>()
return {
name: 'vite:isolate-entries',
apply: 'build',
configResolved(config): void {
logger = config.logger
},
options(opts): InputOptions | void {
const { input } = opts
if (input && typeof input === 'object') {
if ((Array.isArray(input) && input.length > 0) || Object.keys(input).length > 1) {
opts.input = VIRTUAL_ENTRY_ID
entries = Array.isArray(input) ? input : Object.entries(input).map(([key, value]) => ({ [key]: value }))
return opts
}
}
},
buildStart(): void {
transformedCount = 0
assetCache.clear()
},
resolveId(id): string | null {
if (id === VIRTUAL_ENTRY_ID) {
return id
}
return null
},
async load(id): Promise<string | void> {
if (id === VIRTUAL_ENTRY_ID) {
const shouldLog = LogLevels[userConfig.logLevel || 'info'] >= LogLevels.info
const shouldWatch = this.meta.watchMode
const watchFiles = new Set<string>()
for (const entry of entries) {
const re = await bundleEntryFile(entry, userConfig, shouldWatch, shouldLog, transformedCount)
const outputChunks = re.bundles.output
for (const chunk of outputChunks) {
if (assetCache.has(chunk.fileName)) {
continue
}
this.emitFile({
type: 'asset',
fileName: chunk.fileName,
source: chunk.type === 'chunk' ? chunk.code : chunk.source
})
assetCache.add(chunk.fileName)
}
for (const id of re.watchFiles) {
watchFiles.add(id)
}
transformedCount += re.transformedCount
}
for (const id of watchFiles) {
this.addWatchFile(id)
}
return `
// This is the virtual entry file
console.log(1)`
}
},
renderStart(): void {
clearLine(-1)
logger.info(`${colors.green(``)} ${transformedCount} modules transformed.`)
},
generateBundle(_, bundle): void {
for (const chunkName in bundle) {
if (chunkName.includes('virtual_isolate-entries')) {
delete bundle[chunkName]
}
}
}
}
}
async function bundleEntryFile(
input: string | Record<string, string>,
config: InlineConfig,
watch: boolean,
shouldLog: boolean,
preTransformedCount: number
): Promise<{ bundles: RollupOutput; watchFiles: string[]; transformedCount: number }> {
const transformReporter = transformReporterPlugin(preTransformedCount, shouldLog)
const buildReporter = watch ? buildReporterPlugin() : undefined
const viteConfig = mergeConfig(config, {
build: {
write: false,
watch: false
},
plugins: [transformReporter, buildReporter],
logLevel: 'warn',
configFile: false
}) as InlineConfig
// rewrite the input instead of merging
viteConfig.build!.rollupOptions!.input = input
const bundles = await viteBuild(viteConfig)
return {
bundles: bundles as RollupOutput,
watchFiles: buildReporter?.api?.getWatchFiles() || [],
transformedCount: transformReporter?.api?.getTransformedCount() || 0
}
}
function transformReporterPlugin(
preTransformedCount = 0,
shouldLog = true
): Plugin<{ getTransformedCount: () => number }> {
let transformedCount = 0
let root
const log = throttle(id => {
writeLine(`transforming (${preTransformedCount + transformedCount}) ${colors.dim(path.relative(root, id))}`)
})
return {
name: 'vite:transform-reporter',
configResolved(config) {
root = config.root
},
transform(_, id) {
transformedCount++
if (!shouldLog) return
if (id.includes('?')) return
log(id)
},
api: {
getTransformedCount() {
return transformedCount
}
}
}
}
function writeLine(output: string): void {
clearLine()
if (output.length < process.stdout.columns) {
process.stdout.write(output)
} else {
process.stdout.write(output.substring(0, process.stdout.columns - 1))
}
}
function clearLine(move: number = 0): void {
if (move < 0) {
process.stdout.moveCursor(0, move)
}
process.stdout.clearLine(0)
process.stdout.cursorTo(0)
}
function throttle(fn: Function) {
let timerHandle: NodeJS.Timeout | null = null
return (...args: any[]) => {
if (timerHandle) return
fn(...args)
timerHandle = setTimeout(() => {
timerHandle = null
}, 100)
}
}

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

@ -0,0 +1,123 @@
import path from 'node:path'
import { type Plugin, type InlineConfig, build as viteBuild, mergeConfig } from 'vite'
import type { SourceMapInput, RollupOutput, OutputOptions } from 'rollup'
import MagicString from 'magic-string'
import buildReporterPlugin from './buildReporter'
import { cleanUrl, toRelativePath } from '../utils'
import { supportImportMetaPaths } from '../electron'
const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g
/**
* Resolve `?modulePath` import and return the module bundle path.
*/
export default function modulePathPlugin(config: InlineConfig): 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 re = await bundleEntryFile(cleanUrl(id), config, this.meta.watchMode)
const [outputChunk, ...outputChunks] = re.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)
}
for (const id of re.watchFiles) {
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: 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,
watch: boolean
): Promise<{ bundles: RollupOutput; watchFiles: string[] }> {
const reporter = watch ? buildReporterPlugin() : undefined
const viteConfig = mergeConfig(config, {
build: {
write: false,
watch: false
},
plugins: [
{
name: 'vite:entry-file-name',
outputOptions(output): OutputOptions {
if (typeof output.entryFileNames !== 'function' && output.entryFileNames) {
output.entryFileNames = '[name]-[hash]' + path.extname(output.entryFileNames)
}
return output
}
},
reporter
],
logLevel: 'warn',
configFile: false
}) as InlineConfig
// rewrite the input instead of merging
const buildOptions = viteConfig.build!
buildOptions.rollupOptions = {
...buildOptions.rollupOptions,
input
}
const bundles = await viteBuild(viteConfig)
return {
bundles: bundles as RollupOutput,
watchFiles: reporter?.api?.getWatchFiles() || []
}
}

118
src/plugins/swc.ts Normal file
View File

@ -0,0 +1,118 @@
import { createRequire } from 'node:module'
import type { SourceMap } from 'rollup'
import type { TransformConfig, Output as TransformResult, JscConfig } from '@swc/core'
import type { Plugin, UserConfig, FilterPattern } from 'vite'
import { createFilter } from 'vite'
type SwcTransformResult = Omit<TransformResult, 'map'> & {
map: SourceMap
}
type SwcTransformOptions = {
sourcemap?: boolean | 'inline' | undefined
minify?: boolean
} & TransformConfig
async function transformWithSWC(code: string, id: string, options: SwcTransformOptions): Promise<SwcTransformResult> {
const { sourcemap = false, minify = false } = options
delete options.sourcemap
delete options.minify
const isTs = /\.tsx?$/.test(id)
const require = createRequire(import.meta.url)
let swc: typeof import('@swc/core')
try {
swc = require('@swc/core')
} catch {
throw new Error('swc plugin require @swc/core, you need to install it.')
}
const jsc: JscConfig = {
parser: {
syntax: isTs ? 'typescript' : 'ecmascript',
decorators: true
},
transform: {
legacyDecorator: true,
decoratorMetadata: true,
...options
},
keepClassNames: true,
target: 'es2022',
minify: {
format: {
comments: false
}
}
}
const result = await swc.transform(code, {
jsc,
sourceMaps: sourcemap,
minify,
configFile: false,
swcrc: false
})
const map: SourceMap = sourcemap && result.map ? JSON.parse(result.map) : { mappings: '' }
return {
code: result.code,
map
}
}
export type SwcOptions = {
include?: FilterPattern
exclude?: FilterPattern
transformOptions?: TransformConfig
}
/**
* Use SWC to support for emitting type metadata for decorators.
* When using `swcPlugin`, you need to install `@swc/core`.
*/
export function swcPlugin(options: SwcOptions = {}): Plugin {
const filter = createFilter(options.include || /\.(m?ts|[jt]sx)$/, options.exclude || /\.js$/)
let sourcemap: boolean | 'inline' = false
let minify: boolean | 'esbuild' | 'terser' = false
return {
name: 'vite:swc',
config(): UserConfig {
return {
esbuild: false
}
},
async configResolved(resolvedConfig): Promise<void> {
sourcemap = resolvedConfig.build?.sourcemap === 'inline' ? 'inline' : !!resolvedConfig.build?.sourcemap
minify = resolvedConfig.build?.minify
},
async transform(code, id): Promise<void | { code: string; map: SourceMap }> {
if (filter(id)) {
const result = await transformWithSWC(code, id, { sourcemap, ...(options.transformOptions || {}) })
return {
code: result.code,
map: result.map
}
}
},
async renderChunk(code, chunk): Promise<null | { code: string; map: SourceMap }> {
if (!minify || minify === 'terser') {
return null
}
const result = await transformWithSWC(code, chunk.fileName, {
sourcemap,
minify: true,
...(options.transformOptions || {})
})
return {
code: result.code,
map: result.map
}
}
}
}

65
src/plugins/worker.ts Normal file
View File

@ -0,0 +1,65 @@
import type { Plugin } from 'vite'
import type { SourceMapInput } from 'rollup'
import MagicString from 'magic-string'
import { cleanUrl, toRelativePath } from '../utils'
const nodeWorkerAssetUrlRE = /__VITE_NODE_WORKER_ASSET__([\w$]+)__/g
const nodeWorkerRE = /\?nodeWorker(?:&|$)/
const nodeWorkerImporterRE = /(?:\?)nodeWorker&importer=([^&]+)(?:&|$)/
/**
* Resolve `?nodeWorker` import and automatically generate `Worker` wrapper.
*/
export default function workerPlugin(): Plugin {
return {
name: 'vite:node-worker',
apply: 'build',
enforce: 'pre',
resolveId(id, importer): string | void {
if (id.endsWith('?nodeWorker')) {
return id + `&importer=${importer}`
}
},
load(id): string | void {
if (nodeWorkerRE.test(id)) {
const match = nodeWorkerImporterRE.exec(id)
if (match) {
const hash = this.emitFile({
type: 'chunk',
id: cleanUrl(id),
importer: match[1]
})
const assetRefId = `__VITE_NODE_WORKER_ASSET__${hash}__`
return `
import { Worker } from 'node:worker_threads';
export default function (options) { return new Worker(new URL(${assetRefId}, import.meta.url), options); }`
}
}
},
renderChunk(code, chunk, { sourcemap }): { code: string; map: SourceMapInput } | null {
let match: RegExpExecArray | null
let s: MagicString | undefined
nodeWorkerAssetUrlRE.lastIndex = 0
while ((match = nodeWorkerAssetUrlRE.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
}
}
}

View File

@ -1,27 +1,17 @@
import { spawn } from 'child_process'
import colors from 'picocolors'
import { createLogger } from 'vite'
import { InlineConfig } from './config'
import { ensureElectronEntryFile, getElectronPath } from './utils'
import type { InlineConfig } from './config'
import { startElectron } from './electron'
import { build } from './build'
export async function preview(inlineConfig: InlineConfig = {}): Promise<void> {
await build(inlineConfig)
export async function preview(inlineConfig: InlineConfig = {}, options: { skipBuild?: boolean }): Promise<void> {
if (!options.skipBuild) {
await build(inlineConfig)
}
const logger = createLogger(inlineConfig.logLevel)
ensureElectronEntryFile(inlineConfig.root)
startElectron(inlineConfig.root)
const electronPath = getElectronPath()
const ps = spawn(electronPath, ['.'])
ps.stdout.on('data', chunk => {
chunk.toString().trim() && logger.info(chunk.toString())
})
ps.stderr.on('data', chunk => {
chunk.toString().trim() && logger.error(chunk.toString())
})
ps.on('close', process.exit)
logger.info(colors.green(`\nstart electron app...`))
logger.info(colors.green(`\nstarting electron app...\n`))
}

View File

@ -1,34 +1,82 @@
import { spawn } from 'child_process'
import { createServer as ViteCreateServer, build as viteBuild, createLogger } from 'vite'
import type { ChildProcess } from 'node:child_process'
import {
type UserConfig as ViteConfig,
type ViteDevServer,
createServer as viteCreateServer,
build as viteBuild,
createLogger,
mergeConfig
} from 'vite'
import colors from 'picocolors'
import { InlineConfig, resolveConfig } from './config'
import { ensureElectronEntryFile, getElectronPath, resolveHostname } from './utils'
import { type InlineConfig, resolveConfig } from './config'
import { resolveHostname } from './utils'
import { startElectron } from './electron'
export async function createServer(inlineConfig: InlineConfig = {}): Promise<void> {
export async function createServer(
inlineConfig: InlineConfig = {},
options: { rendererOnly?: boolean }
): Promise<void> {
process.env.NODE_ENV_ELECTRON_VITE = 'development'
const config = await resolveConfig(inlineConfig, 'serve', 'development')
if (config.config) {
const logger = createLogger(inlineConfig.logLevel)
const mainViteConfig = config.config?.main
if (mainViteConfig) {
await viteBuild(mainViteConfig)
let server: ViteDevServer | undefined
let ps: ChildProcess | undefined
logger.info(colors.green(`\nbuild the electron main process successfully`))
const errorHook = (e): void => {
logger.error(`${colors.bgRed(colors.white(' ERROR '))} ${colors.red(e.message)}`)
}
const mainViteConfig = config.config?.main
if (mainViteConfig && !options.rendererOnly) {
const watchHook = (): void => {
logger.info(colors.green(`\nelectron main process rebuilt successfully`))
if (ps) {
ps.removeAllListeners()
ps.kill()
ps = startElectron(inlineConfig.root)
logger.info(colors.green(`\nrestarting electron app...\n`))
}
}
await doBuild(mainViteConfig, watchHook, errorHook)
logger.info(colors.green(`\nelectron main process built successfully`))
}
const preloadViteConfig = config.config?.preload
if (preloadViteConfig) {
if (preloadViteConfig && !options.rendererOnly) {
logger.info(colors.gray(`\n-----\n`))
await viteBuild(preloadViteConfig)
logger.info(colors.green(`\nbuild the electron preload files successfully`))
const watchHook = (): void => {
logger.info(colors.green(`\nelectron preload scripts rebuilt successfully`))
if (server) {
logger.info(colors.cyan(`\nreloading electron renderer...\n`))
server.ws.send({ type: 'full-reload' })
}
}
await doBuild(preloadViteConfig, watchHook, errorHook)
logger.info(colors.green(`\nelectron preload scripts built successfully`))
}
if (options.rendererOnly) {
logger.warn(
`\n${colors.yellow(colors.bold('(!)'))} ${colors.yellow('skipped building main process and preload scripts (using previous build)')}`
)
}
const rendererViteConfig = config.config?.renderer
if (rendererViteConfig) {
logger.info(colors.gray(`\n-----\n`))
const server = await ViteCreateServer(rendererViteConfig)
server = await viteCreateServer(rendererViteConfig)
if (!server.httpServer) {
throw new Error('HTTP server not available')
@ -46,25 +94,49 @@ export async function createServer(inlineConfig: InlineConfig = {}): Promise<voi
const slogger = server.config.logger
slogger.info(colors.green(`dev server running for the electron renderer process at:\n`), {
clear: !slogger.hasWarned
clear: !slogger.hasWarned && !options.rendererOnly
})
server.printUrls()
}
ensureElectronEntryFile(inlineConfig.root)
ps = startElectron(inlineConfig.root)
const electronPath = getElectronPath()
const ps = spawn(electronPath, ['.'])
ps.stdout.on('data', chunk => {
chunk.toString().trim() && logger.info(chunk.toString())
})
ps.stderr.on('data', chunk => {
chunk.toString().trim() && logger.error(chunk.toString())
})
ps.on('close', process.exit)
logger.info(colors.green(`\nstart electron app...`))
logger.info(colors.green(`\nstarting electron app...\n`))
}
}
type UserConfig = ViteConfig & { configFile?: string | false }
async function doBuild(config: UserConfig, watchHook: () => void, errorHook: (e: Error) => void): Promise<void> {
return new Promise(resolve => {
if (config.build?.watch) {
let firstBundle = true
const closeBundle = (): void => {
if (firstBundle) {
firstBundle = false
resolve()
} else {
watchHook()
}
}
config = mergeConfig(config, {
plugins: [
{
name: 'vite:electron-watcher',
closeBundle
}
]
})
}
viteBuild(config)
.then(() => {
if (!config.build?.watch) {
resolve()
}
})
.catch(e => errorHook(e))
})
}

View File

@ -1,45 +1,122 @@
import * as path from 'path'
import * as fs from 'fs'
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */
import path from 'node:path'
import fs from 'node:fs'
import { createHash } from 'node:crypto'
import { createRequire } from 'node:module'
import { loadEnv as viteLoadEnv } from 'vite'
export function isObject(value: unknown): value is Record<string, unknown> {
return Object.prototype.toString.call(value) === '[object Object]'
}
export const dynamicImport = new Function('file', 'return import(file)')
export function ensureElectronEntryFile(root = process.cwd()): void {
const pkg = path.join(root, 'package.json')
if (fs.existsSync(pkg)) {
const main = require(pkg).main
if (!main) {
throw new Error('not found an entry point to electorn app, please add main field for your package.json')
} else {
const entryPath = path.resolve(root, main)
if (!fs.existsSync(entryPath)) {
throw new Error(`not found the electorn app entry file: ${entryPath}`)
}
}
} else {
throw new Error('no package.json')
}
}
export function getElectronPath(): string {
const electronModulePath = path.resolve(process.cwd(), 'node_modules', 'electron')
const pathFile = path.join(electronModulePath, 'path.txt')
let executablePath
if (fs.existsSync(pathFile)) {
executablePath = fs.readFileSync(pathFile, 'utf-8')
}
if (executablePath) {
return path.join(electronModulePath, 'dist', executablePath)
} else {
throw new Error('Electron uninstall')
}
}
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 {
return typeof optionsHost === 'string' && !wildcardHosts.has(optionsHost) ? optionsHost : 'localhost'
}
export const queryRE = /\?.*$/s
export const hashRE = /#.*$/s
export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '')
export function getHash(text: Buffer | string): string {
return createHash('sha256')
.update(text as unknown as Uint8Array)
.digest('hex')
.substring(0, 8)
}
export function toRelativePath(filename: string, importer: string): string {
const relPath = path.posix.relative(path.dirname(importer), filename)
return relPath.startsWith('.') ? relPath : `./${relPath}`
}
/**
* 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.
*/
export function loadEnv(
mode: string,
envDir: string = process.cwd(),
prefixes: string | string[] = ['VITE_', 'MAIN_VITE_', 'PRELOAD_VITE_', 'RENDERER_VITE_']
): Record<string, string> {
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>
}
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": {
"target": "es2019",
"module": "esnext",
"lib": ["esnext"],
"target": "ES2023",
"module": "ESNext",
"lib": ["ESNext"],
"sourceMap": false,
"strict": true,
"allowJs": true,
"esModuleInterop": true,
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": false,
"noImplicitReturns": true,
"declaration": true,
"declarationDir": "dist/types",
"outDir": "dist"
"noImplicitReturns": true
},
"include": ["src"]
"include": ["src", "rollup.config.ts"]
}