Compare commits

...

304 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
alex8088
ffda7049c7 release: v1.0.6 2022-08-25 19:29:47 +08:00
alex8088
d24519d71b chore: update deps 2022-08-25 19:28:17 +08:00
alex8088
a5f6f13b9c release: v1.0.5 2022-08-20 20:38:29 +08:00
alex8088
7a60cce781 feat: define env prefix 2022-08-20 20:33:14 +08:00
alex8088
5dc1ddc6ad docs: update 2022-08-20 20:18:23 +08:00
alex8088
d47984cf4f docs: new official website released 2022-08-20 20:16:11 +08:00
alex8088
cc2e71bed3 docs: update 2022-08-08 19:38:41 +08:00
alex8088
cec47266bd perf: build target for Electron 20 2022-08-08 19:19:49 +08:00
alex8088
71dc5fd18b fix: can not get import meta env variables in renderer #10 2022-08-08 19:14:08 +08:00
alex8088
839a1801a3 release: v1.0.4 2022-07-03 15:51:30 +08:00
alex8088
44b8eee48f chore: update contributing 2022-07-03 15:31:37 +08:00
alex8088
04abdda294 fix: error occurs when the preload config is a function 2022-07-03 15:29:19 +08:00
alex.wei
e35552b4b6
Merge pull request #6 from Beiluola/fix-host-true
fix: ELECTRON_RENDERER_URL is incorrect
2022-06-20 10:34:56 +08:00
xiaohui
c568348fc9 fix: ELECTRON_RENDERER_URL is incorrect 2022-06-20 00:38:05 +08:00
alex8088
c71d5aa661 feat: add sqlite3 module to rollup external option 2022-06-05 12:28:18 +08:00
alex8088
68f660fbc3 docs: update 2022-06-05 12:24:47 +08:00
alex8088
62b21ee994 fix: commit verify error 2022-05-26 23:04:36 +08:00
43 changed files with 5568 additions and 3001 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,6 +1,382 @@
### 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
- chore: use vite@3
### v1.0.5 (_2022-08-20_)
- fix: can not get import meta env variables in renderer
- feat: define env prefix
- feat: new official website released
### v1.0.4 (_2022-07-03_)
- fix: error occurs when the preload config is a function (by @Beiluola)
- fix: ELECTRON_RENDERER_URL is incorrect when host is true (by @Beiluola)
- feat: add sqlite3 module to rollup external option
### v1.0.3 (_2022-05-01_)
- fix: throw error when vite.config.* file in root ([#3](https://github.com/alex8088/electron-vite/issues/3))
- fix: throw error when vite.config.\* file in root ([#3](https://github.com/alex8088/electron-vite/issues/3))
- feat: export splitVendorChunkPlugin from vite
- perf: build target for Electron 19

View File

@ -14,8 +14,9 @@ pnpm install
## Pull Request
- Checkout a topic branch from a base branch, e.g. master, and merge back against that branch.
- Checkout a topic branch from a base branch, e.g. fix-bug, and merge back against that branch.
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
- To check that your contributions match the project coding style make sure `pnpm lint` && `pnpm typecheck` passes. To build project run: `pnpm build`.
- Commit messages must follow the [commit message convention](./.github/commit-convention.md). Commit messages are automatically validated before commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks)).
- Commit messages preferably in English.
- No need to worry about code style as long as you have installed the dev dependencies - modified files are automatically formatted with Prettier on commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks)).

330
README.md
View File

@ -1,23 +1,41 @@
# electron-vite
<p>
<img src="https://img.shields.io/badge/node->14.0.0-blue.svg" alt="node" />
<img src="https://img.shields.io/badge/vite->2.6.0-747bff.svg" alt="vite" />
<p align="center">
<img src="https://alex8088.github.io/assets/electron-vite.svg" width="150px" height="150px">
</p>
English | [简体中文](./README.zh-CN.md)
<div align="center">
<h1>electron-vite</h1>
</div>
<p align="center">Next generation Electron build tooling based on Vite</p>
> An Electron CLI integrated with Vite
<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/electron-vite?color=blue" alt="license" />
</p>
---
<p align="center">
<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.electron-vite.org">中文文档</a>
</p>
<br />
<br />
## Features
- ⚡Use the same way as [Vite](https://vitejs.dev)
- 🔨Main process, renderer process and preload script source code are built using Vite
- 📃Main process, renderer process and preload script Vite configuration combined into one file
- 📦Preset optimal build 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
@ -34,66 +52,14 @@ In a project where `electron-vite` is installed, you can use `electron-vite` bin
```json
{
"scripts": {
"start": "electron-vite preview", // start electron app to preview production build
"dev": "electron-vite dev", // start dev server and electron app
"prebuild": "electron-vite build" // build for production
"start": "electron-vite preview",
"dev": "electron-vite dev",
"prebuild": "electron-vite build"
}
}
```
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 remote 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'))
}
}
```
**Note**: For development, the renderer process `index.html` file needs to reference your script code via `<script type="module">`.
### Recommended project directory
```shell
├──src
│ ├──main
│ │ ├──index.js
│ │ └──...
│ ├──preload
│ │ ├──index.js
│ │ └──...
│ └──renderer
│ ├──src
│ ├──index.html
│ └──...
├──electron.vite.config.js
└──package.json
```
### Get 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
```
## Configure
### Config file
### 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:
@ -112,223 +78,27 @@ export default {
}
```
You can also explicitly specify a config file to use with the `--config` CLI option (resolved relative to `cwd`):
### Getting Started
```sh
electron-vite --config my-config.js
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 create @quick-start/electron@latest
```
**Tips**: `electron-vite` also supports `ts` or `mjs` config file.
Currently supported template presets include:
### Config intellisense
| 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) |
Since `electron-vite` ships with TypeScript typings, you can leverage your IDE's intellisense with jsdoc type hints:
## Contribution
```js
/**
* @type {import('electron-vite').UserConfig}
*/
const config = {
// ...
}
export default config
```
Alternatively, you can use the `defineConfig` and `defineViteConfig` helper which should provide intellisense without the need for jsdoc annotations:
```js
import { defineConfig, defineViteConfig } from 'electron-vite'
export default defineConfig({
main: {
// ...
},
preload: {
// ...
},
renderer: defineViteConfig(({ command, mode }) => {
// conditional config use defineViteConfig
// ...
})
})
```
**Tips**: The `defineViteConfig` exports from `Vite`.
### Config reference
See [vitejs.dev](https://vitejs.dev/config)
### Config presets
#### Build options for `main`:
- **outDir**: `out\main`(relative to project root)
- **target**: `node*`, automatically match node target of `Electron`. For example, the node target of Electron 17 is `node16.13`
- **lib.entry**: `src\main\{index|main}.{js|ts|mjs|cjs}`(relative to project root), empty string if not found
- **lib.formats**: `cjs`
- **rollupOptions.external**: `electron` and all builtin modules
#### Build options for `preload`:
- **outDir**: `out\preload`(relative to project root)
- **target**: the same as `main`
- **lib.entry**: `src\preload\{index|preload}.{js|ts|mjs|cjs}`(relative to project root), empty string if not found
- **lib.formats**: `cjs`
- **rollupOptions.external**: the same as `main`
#### Build options for `renderer`:
- **root**: `src\renderer`(relative to project root)
- **outDir**: `out\renderer`(relative to project root)
- **target**: `chrome*`, automatically match chrome target of `Electron`. For example, the chrome target of Electron 17 is `chrome98`
- **lib.entry**: `src\renderer\index.html`(relative to project root), empty string if not found
- **polyfillModulePreload**: `false`, there is no need to polyfill `Module Preload` for the Electron renderer
- **rollupOptions.external**: the same as `main`
#### Define option for `main` and `preload`
In web development, Vite will transform `'process.env.'` to `'({}).'`. This is reasonable and correct. But in nodejs development, we sometimes need to use `process.env`, so `electron-vite` will automatically add config define field to redefine global variable replacements like this:
```js
export default {
main: {
define: {
'process.env': 'process.env'
}
}
}
```
**Note**: If you want to use these configurations in an existing project, please see the Vite plugin [vite-plugin-electron-config](https://github.com/alex8088/vite-plugin-electron-config)
### Config FAQs
#### How do I configure when the Electron app has multiple windows?
When your electron app has multiple windows, it means there are multiple html files or preload files. You can modify your config file like this:
```js
export default {
main: {},
preload: {
build: {
rollupOptions: {
input: {
browser: resolve(__dirname, 'src/preload/browser.ts'),
webview: resolve(__dirname, 'src/preload/webview.ts')
}
}
}
},
renderer: {
build: {
rollupOptions: {
input: {
browser: resolve(__dirname, 'src/renderer/browser.html'),
webview: resolve(__dirname, 'src/renderer/webview.html')
}
}
}
}
}
```
## CLI options
For the full list of CLI options, you can run `npx electron-vite -h` in your project. The flags listed below are only available via the command line interface:
- `--ignoreConfigWarning`: boolean, allow you ignore warning when config missing
- `--outDir`: string, output directory (default: out)
## API
### build
Type Signature:
```js
async function build(inlineConfig: InlineConfig = {}): Promise<void>
```
Example Usage:
```js
const path = require('path')
const { build } = require('electron-vite')
;(async () => {
await build({
build: {
outDir: 'out'
rollupOptions: {
// ...
}
}
})
})()
```
### createServer
Type Signature:
```js
async function createServer(inlineConfig: InlineConfig = {}): Promise<void>
```
Example Usage:
```js
const { createServer } = require('electron-vite')
;(async () => {
await createServer({
server: {
port: 1337
}
})
})()
```
### preview
Type Signature:
```js
async function preview(inlineConfig: InlineConfig = {}): Promise<void>
```
Example Usage:
```js
const { preview } = require('electron-vite')
;(async () => {
await preview({})
})()
```
### InlineConfig
The InlineConfig interface extends Vite [UserConfig](https://vitejs.dev/guide/api-javascript.html#inlineconfig) with additional properties:
- `ignoreConfigWarning`: set to `false` to ignore warning when config missing
And omit `base` property because it is not necessary to set the base public path in Electron.
### resolveConfig
Type Signature:
```js
async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
defaultMode = 'development'
): Promise<ResolvedConfig>
```
See [Contributing Guide](CONTRIBUTING.md).
## License

View File

@ -1,335 +0,0 @@
# electron-vite
<p>
<img src="https://img.shields.io/badge/node->14.0.0-blue.svg" alt="node" />
<img src="https://img.shields.io/badge/vite->2.6.0-747bff.svg" alt="vite" />
</p>
[English](./README.md) | 简体中文
> 新一代 Electron 开发构建工具
---
## 特性
- ⚡️使用方式与 [Vite](https://vitejs.dev) 相同
- 🔨主进程/渲染进程/ preload 脚本都使用 Vite 构建
- 📃统一所有配置,合并到一个文件中
- 📦预设构建配置,无需关注配置
- 🚀支持渲染进程热更新(HMR)
## 用法
### 安装
```sh
npm i electron-vite -D
```
### 开发 & 编译
在安装了 `electron-vite` 的项目中,可以直接使用 `npx electron-vite` 运行, 也可以在 `package.json` 文件中添加 npm scripts
```json
{
"scripts": {
"start": "electron-vite preview", // 开启 Electron 预览生产构建
"dev": "electron-vite dev", // 开启开发服务并启动 Electron 程序
"prebuild": "electron-vite build" // 为生产打包构建
}
}
```
为了使用热更新(HMR),需要使用环境变量(`ELECTRON_RENDERER_URL`)来决定 Electron 窗口加载本地页面还是远程页面。
```js
function createWindow() {
// 创建窗口
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js')
}
})
// 开发模式下使用支持HMR的远程地址生产模式下使用本地html页面
if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}
}
```
**注意**:在开发中,渲染进程 `index.html` 文件需要通过 `<script type="module">` 引用脚本。
### 推荐项目目录
```shell
├──src
│ ├──main
│ │ ├──index.js
│ │ └──...
│ ├──preload
│ │ ├──index.js
│ │ └──...
│ └──renderer
│ ├──src
│ ├──index.html
│ └──...
├──electron.vite.config.js
└──package.json
```
### 开始学习
克隆 [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) 模板或者使用 [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) 脚手架来搭建项目学习。
``` bash
npm init @quick-start/electron
```
## 配置
### 配置文件
当以命令行方式运行 `electron-vite` 时,将会自动尝试解析项目根目录中名为 `electron.vite.config.js` 的配置文件。最基本的配置文件如下所示:
```js
// electron.vite.config.js
export default {
main: {
// vite 配置选项
},
preload: {
// vite 配置选项
},
renderer: {
// vite 配置选项
}
}
```
你可以显式地通过 `--config` 命令行选项指定一个配置文件(相对于 `cwd` 路径进行解析):
```sh
electron-vite --config my-config.js
```
**提示**`electron-vite` 也支持 `ts` 或者 `mjs` 的配置文件。
### 配置智能提示
因为 `electron-vite` 本身附带 Typescript 类型,所以你可以通过 IDE 和 jsdoc 的配合来实现智能提示:
```js
/**
* @type {import('electron-vite').UserConfig}
*/
const config = {
// ...
}
export default config
```
你还可以使用 `defineConfig` and `defineViteConfig` 工具函数,这样不用 jsdoc 注解也可以获取类型提示:
```js
import { defineConfig, defineViteConfig } from 'electron-vite'
export default defineConfig({
main: {
// ...
},
preload: {
// ...
},
renderer: defineViteConfig(({ command, mode }) => {
// 条件配置可使用 defineViteConfig
// ...
})
})
```
**提示**`defineViteConfig``Vite` 中导出。
### 配置参考
见 [vitejs.dev](https://vitejs.dev/config)
### 配置预设
#### `主进程`编译项预设:
- **outDir**`out\main`(相对于根目录)
- **target**`node*`,自动匹配 `Electron``node` 构建目标,如 Electron 17 为 `node16.13`
- **lib.entry**`src\main\{index|main}.{js|ts|mjs|cjs}`(相对于根目录),找不到则为空
- **lib.formats**`cjs`
- **rollupOptions.external**`electron` 和所有内置 node 模块(如果用户配置了外部模块ID将自动合并)
#### `preload` 脚本编译项预设:
- **outDir**`out\preload`(相对于根目录)
- **target**:同`主进程`
- **lib.entry**`src\preload\{index|preload}.{js|ts|mjs|cjs}`(相对于根目录),找不到则为空
- **lib.formats**`cjs`
- **rollupOptions.external**:同`主进程`
#### `渲染进程`编译项预设:
- **root**`src\renderer`(相对于根目录)
- **outDir**`out\renderer`(相对于根目录)
- **target**`chrome*`, 自动匹配 `Electron``chrome` 构建目标,如 Electron 17 为 `chrome98`
- **lib.entry**`src\renderer\index.html`(相对于根目录),找不到则为空
- **polyfillModulePreload**`false`,不需要为渲染进程 polyfill `Module Preload`
- **rollupOptions.external**:同`主进程`
#### `主进程``preload` 脚本的 `define` 项设置:
在 Web 开发中Vite 会将 `'process.env.'` 替换为 `'({}).'`,这是合理和正确的。但在 nodejs 开发中,我们有时候需要使用 `process.env` ,所以 `electron-vite` 重新预设全局变量替换,恢复其使用,预设如下:
```js
export default {
main: {
define: {
'process.env': 'process.env'
}
}
}
```
**提示**:如果你想在已有的项目中使用这些预设配置,可以使用 Vite 的插件 [vite-plugin-electron-config](https://github.com/alex8088/vite-plugin-electron-config)
### 配置问题
#### 如果 Electron 具有多窗口应该如何配置?
当 Electron 应用程序具有多窗口时,就意味着可能有多个 html 页面和 preload 脚本,你可以像下面一样修改你的配置文件:
```js
export default {
main: {},
preload: {
build: {
rollupOptions: {
input: {
browser: resolve(__dirname, 'src/preload/browser.ts'),
webview: resolve(__dirname, 'src/preload/webview.ts')
}
}
}
},
renderer: {
build: {
rollupOptions: {
input: {
browser: resolve(__dirname, 'src/renderer/browser.html'),
webview: resolve(__dirname, 'src/renderer/webview.html')
}
}
}
}
}
```
## 命令行选项
在项目中,可运行 `npx electron-vite -h` 获得完整的命令行选项列表。下面列出的标志只能通过命令行使用:
- `--ignoreConfigWarning`boolean忽略配置缺失警告如配置文件中移除 preload 配置,不使用 preload 开发时,是有用的)
- `--outDir`string输出路径相对根目录 (默认out)
## API
### build
类型:
```js
async function build(inlineConfig: InlineConfig = {}): Promise<void>
```
示例:
```js
const path = require('path')
const { build } = require('electron-vite')
;(async () => {
await build({
build: {
outDir: 'out'
rollupOptions: {
// ...
}
}
})
})()
```
### createServer
类型:
```js
async function createServer(inlineConfig: InlineConfig = {}): Promise<void>
```
示例:
```js
const { createServer } = require('electron-vite')
;(async () => {
await createServer({
server: {
port: 1337
}
})
})()
```
### preview
类型:
```js
async function preview(inlineConfig: InlineConfig = {}): Promise<void>
```
示例:
```js
const { preview } = require('electron-vite')
;(async () => {
await preview({})
})()
```
### InlineConfig
`InlineConfig` 接口扩展了 Vite [UserConfig](https://vitejs.dev/guide/api-javascript.html#inlineconfig) 并添加了以下属性:
- `ignoreConfigWarning`:设置为 `false` 来忽略配置缺失警告
同时移除 `base` 属性,因为在 Electron 中没有必要指定公共基础路径。
### resolveConfig
类型:
```js
async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
defaultMode = 'development'
): Promise<ResolvedConfig>
```
## License
[MIT](./LICENSE) © alex.wei

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.3",
"description": "Use vite for your electron app.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"version": "5.0.0",
"description": "Electron build tooling based on Vite",
"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": "^2.9.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.19.4",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-typescript": "^8.3.0",
"@types/node": "16.11.22",
"@typescript-eslint/eslint-plugin": "^5.11.0",
"@typescript-eslint/parser": "^5.11.0",
"eslint": "^8.7.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"fs-extra": "^10.0.0",
"lint-staged": "^12.3.6",
"prettier": "^2.5.1",
"rollup": "^2.64.0",
"simple-git-hooks": "^2.7.0",
"tslib": "^2.3.1",
"typescript": "^4.5.5",
"vite": "^2.9.6"
"@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.38",
"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"
]
}
}

3919
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,18 +1,18 @@
// 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()
console.error(
` ${colors.bgRed.white(' ERROR ')} ${colors.red(`invalid commit message format.`)}\n\n` +
` ${colors.bgRed(colors.white(' ERROR '))} ${colors.red(`invalid commit message format.`)}\n\n` +
colors.red(` Proper commit message format is required for automated changelog generation. Examples:\n\n`) +
` ${colors.green(`feat: add 'comments' option`)}\n` +
` ${colors.green(`fix: handle events on blur (close #28)`)}\n\n` +

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)
if (!config.config) {
return
}
const preloadViteConfig = config.config?.preload
if (preloadViteConfig) {
await viteBuild(preloadViteConfig)
// 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
}
const rendererViteConfig = config.config?.renderer
if (rendererViteConfig) {
await viteBuild(rendererViteConfig)
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/
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`.
*/
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 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(preloadViteConfig)) {
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,353 +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', ...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 }
}
},
{
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 }
}
},
{
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
}
},
{
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 = {
'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 = {
'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> {
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 } 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')
@ -39,32 +87,56 @@ export async function createServer(inlineConfig: InlineConfig = {}): Promise<voi
const conf = server.config.server
const protocol = conf.https ? 'https:' : 'http:'
const host = conf.host || 'localhost'
const host = resolveHostname(conf.host)
const port = conf.port
process.env.ELECTRON_RENDERER_URL = `${protocol}//${host}:${port}`
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,39 +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 const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000'])
export function ensureElectronEntryFile(root = process.cwd()): void {
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 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}`)
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 {
throw new Error('no package.json')
const pkg = loadPackageData()
return pkg?.type === 'module'
}
}
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')
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 (executablePath) {
return path.join(electronModulePath, 'dist', executablePath)
} else {
throw new Error('Electron uninstall')
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"]
}