mirror of
https://github.com/alex8088/electron-vite.git
synced 2026-11-02 14:51:51 +08:00
Compare commits
241 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23d7bfc8ba | ||
|
|
2ca5e40433 | ||
|
|
bffc6c95df | ||
|
|
b0b33bdd5e | ||
|
|
3ec79bbf65 | ||
|
|
19050aaec9 | ||
|
|
0655933704 | ||
|
|
6fc103eb5b | ||
|
|
6b0f85a2a7 | ||
|
|
fa199eb372 | ||
|
|
b66840e2ec | ||
|
|
1cbef3fb80 | ||
|
|
36a0147a6d | ||
|
|
4c4bdf5248 | ||
|
|
395f0dae0c | ||
|
|
22b12bd102 | ||
|
|
c104b2d15b | ||
|
|
c047b58d5d | ||
|
|
55239df0ee | ||
|
|
d8e7702dc5 | ||
|
|
816c6dac61 | ||
|
|
98d30e57e3 | ||
|
|
a135566fa5 | ||
|
|
08acca3585 | ||
|
|
68b80f0ad5 | ||
|
|
bc1ef5b023 | ||
|
|
53bc35b240 | ||
|
|
a7f76d8d44 | ||
|
|
2937574aa4 | ||
|
|
3680d60b21 | ||
|
|
8df70754ca | ||
|
|
e691efbd82 | ||
|
|
ac2cdd1a21 | ||
|
|
811a9f2a75 | ||
|
|
608461c04c | ||
|
|
31965d2972 | ||
|
|
4bede21c81 | ||
|
|
31bfc4886b | ||
|
|
371b650d71 | ||
|
|
5333d5216a | ||
|
|
2765eea74d | ||
|
|
d3095d24ab | ||
|
|
a041f42e0b | ||
|
|
2b7c4c33bc | ||
|
|
517d703cbf | ||
|
|
fa84b6d6c3 | ||
|
|
2f1f389776 | ||
|
|
a9a496dc1a | ||
|
|
201eb213ac | ||
|
|
3d2a979fb1 | ||
|
|
e58369f975 | ||
|
|
e9289f32f7 | ||
|
|
78a117d5be | ||
|
|
5a8bbe3d0e | ||
|
|
91368b6655 | ||
|
|
b1fd596afe | ||
|
|
438e9e7672 | ||
|
|
9eba4df577 | ||
|
|
465690ab0d | ||
|
|
6aae37833e | ||
|
|
9152dfc943 | ||
|
|
0276407b5b | ||
|
|
fe7e631f47 | ||
|
|
08be346407 | ||
|
|
3e9ded666c | ||
|
|
1bba6766e8 | ||
|
|
4edffe3b9a | ||
|
|
cfd9812a91 | ||
|
|
7c7f31b2a3 | ||
|
|
ae57b2489a | ||
|
|
397b02e384 | ||
|
|
a4f7693712 | ||
|
|
56fb519092 | ||
|
|
eb0a7e3ffe | ||
|
|
de70dfe1dc | ||
|
|
8892bf3679 | ||
|
|
2576484604 | ||
|
|
88f6db2239 | ||
|
|
0a79da03db | ||
|
|
c3939ade45 | ||
|
|
38228f9b3f | ||
|
|
8b193864fd | ||
|
|
f264d41d7f | ||
|
|
5debb6f83b | ||
|
|
d530597339 | ||
|
|
a9b5326544 | ||
|
|
7587d2c674 | ||
|
|
0fb8918090 | ||
|
|
c7955aa6fd | ||
|
|
70e027d38a | ||
|
|
28bb22b353 | ||
|
|
327adc23df | ||
|
|
4a6aea3704 | ||
|
|
3f7f65bf57 | ||
|
|
0badfc493f | ||
|
|
cbb039c3e9 | ||
|
|
ad50cba495 | ||
|
|
b55eb725c4 | ||
|
|
1eeb15ea77 | ||
|
|
52abc48abf | ||
|
|
cbee52c8bb | ||
|
|
48e6f4f570 | ||
|
|
4071778f07 | ||
|
|
3fd16d0c23 | ||
|
|
d2e8b1271b | ||
|
|
28d7df6e91 | ||
|
|
b7763a7f77 | ||
|
|
1c084cc090 | ||
|
|
9ec164d33e | ||
|
|
a9197f5cc9 | ||
|
|
3d5c9f68a1 | ||
|
|
1b411d3633 | ||
|
|
b56d3c2d21 | ||
|
|
f2eff25268 | ||
|
|
2d8e513e07 | ||
|
|
d8063320dc | ||
|
|
f33c5b2abe | ||
|
|
e91e70c105 | ||
|
|
ea144aef19 | ||
|
|
987c55ee8b | ||
|
|
8064bd81ff | ||
|
|
6e8572d9b7 | ||
|
|
4b47ef0bd4 | ||
|
|
5a5af050b2 | ||
|
|
96ae3c5cd9 | ||
|
|
79ac91dee2 | ||
|
|
1599d730f6 | ||
|
|
3c6e08b2f2 | ||
|
|
dfe6a3e3f8 | ||
|
|
6c01417909 | ||
|
|
5ffd49eddc | ||
|
|
bf1220875f | ||
|
|
02e0dff9f4 | ||
|
|
0a02ace008 | ||
|
|
e638dcae1b | ||
|
|
3264abc70b | ||
|
|
19b42225f8 | ||
|
|
d9aaf24f84 | ||
|
|
3605aca1e8 | ||
|
|
73dfee5a4f | ||
|
|
b3185d7fc5 | ||
|
|
d79de5abb6 | ||
|
|
1838bdbf3e | ||
|
|
ff23f7d44d | ||
|
|
a48e12a9df | ||
|
|
1abedce6c2 | ||
|
|
c2c655367f | ||
|
|
6170705eae | ||
|
|
eeaa7de45c | ||
|
|
f42a9d370f | ||
|
|
08ff86f2c4 | ||
|
|
0ce505a12f | ||
|
|
ad891af811 | ||
|
|
27ade03acf | ||
|
|
52fce25787 | ||
|
|
19489a28c8 | ||
|
|
7f13ea2a84 | ||
|
|
932fc556f5 | ||
|
|
a12646f25e | ||
|
|
921aa9d4a7 | ||
|
|
694e134a52 | ||
|
|
40a1b64639 | ||
|
|
476ef54498 | ||
|
|
9b04362e12 | ||
|
|
a8f5182d25 | ||
|
|
0c98f33573 | ||
|
|
db1128089a | ||
|
|
0b265442a1 | ||
|
|
7b27e684d6 | ||
|
|
dbd7b0137b | ||
|
|
73f2562172 | ||
|
|
cc0df8a8cc | ||
|
|
93e84f6bce | ||
|
|
619a337c6d | ||
|
|
963f8aba51 | ||
|
|
082b1331ff | ||
|
|
b578f61f1c | ||
|
|
5de87d681b | ||
|
|
a7b3c65576 | ||
|
|
4924e36d74 | ||
|
|
61eb32aa04 | ||
|
|
c1e1ca0471 | ||
|
|
cb58ef8649 | ||
|
|
93763597fe | ||
|
|
dc87a798ab | ||
|
|
4f12196e54 | ||
|
|
b93a1878c1 | ||
|
|
8bb03f8b24 | ||
|
|
f951edf492 | ||
|
|
7c6020e11b | ||
|
|
4e48537b39 | ||
|
|
10099eced0 | ||
|
|
fbc45f696b | ||
|
|
d30d3765eb | ||
|
|
5c4c28ca0a | ||
|
|
5e31794c2d | ||
|
|
588b39b0b4 | ||
|
|
ce56ecd4dc | ||
|
|
5021496f74 | ||
|
|
8e1f9f6845 | ||
|
|
7369960b99 | ||
|
|
ac47dacc1b | ||
|
|
2360d502a4 | ||
|
|
01163866fb | ||
|
|
8b839872f8 | ||
|
|
670c23b2e1 | ||
|
|
566f88960e | ||
|
|
8a774ef76d | ||
|
|
27acfb7c1d | ||
|
|
5c440a8a76 | ||
|
|
ed64873434 | ||
|
|
254ce0b5b6 | ||
|
|
0c46c1064b | ||
|
|
6c590ad65b | ||
|
|
d8307e8fd4 | ||
|
|
532ba52714 | ||
|
|
8914878a53 | ||
|
|
d7b4ebc54a | ||
|
|
342a0556cd | ||
|
|
9d76799072 | ||
|
|
f0f21db904 | ||
|
|
11b6f3c39d | ||
|
|
ed13174829 | ||
|
|
583c318af1 | ||
|
|
3507149b6e | ||
|
|
81117c6809 | ||
|
|
3567f26f02 | ||
|
|
9f5ca0d92d | ||
|
|
8b445b67c5 | ||
|
|
c43d1832d2 | ||
|
|
0367f73a95 | ||
|
|
785b32f2b0 | ||
|
|
4f796e4c8a | ||
|
|
3def0ed84b | ||
|
|
b71bf82f02 | ||
|
|
587efabac5 | ||
|
|
bac8c04816 | ||
|
|
6dff3475b4 | ||
|
|
c26a9ff71c | ||
|
|
388b590808 | ||
|
|
8c924188de |
@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
dist
|
|
||||||
38
.eslintrc.js
38
.eslintrc.js
@ -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
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
github: alex8088
|
||||||
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,6 +1,5 @@
|
|||||||
name: "\U0001F41E Bug Report"
|
name: "\U0001F41E Bug Report"
|
||||||
description: Report an issue with electron-vite
|
description: Report an issue with electron-vite
|
||||||
labels: ['bug', 'triage']
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@ -45,7 +44,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: Read the [Contributing Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md).
|
- label: Read the [Contributing Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md).
|
||||||
required: true
|
required: true
|
||||||
- label: Read the [docs](https://evite.netlify.app).
|
- label: Read the [docs](https://electron-vite.org).
|
||||||
required: true
|
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 reports the same bug to avoid creating a duplicate.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
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.
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -40,7 +40,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: Read the [Contributing Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md).
|
- label: Read the [Contributing Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md).
|
||||||
required: true
|
required: true
|
||||||
- label: Read the [docs](https://evite.netlify.app).
|
- label: Read the [docs](https://electron-vite.org).
|
||||||
required: true
|
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
|
required: true
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
.DS_Store
|
||||||
|
.eslintcache
|
||||||
*.log*
|
*.log*
|
||||||
|
|||||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
325
CHANGELOG.md
325
CHANGELOG.md
@ -1,3 +1,328 @@
|
|||||||
|
### v6.0.0 (_2026-10-15_)
|
||||||
|
|
||||||
|
- feat(bytecode): support RegExp for chunkAlias option ([#885](https://github.com/alex8088/electron-vite/issues/885))
|
||||||
|
- refactor: share and normalize build output resolution
|
||||||
|
- refactor: inline bytecode build plugin name
|
||||||
|
- refactor!: invoke the bytecode compiler only once to compile all code
|
||||||
|
- refactor: use pipe stdio for spawn Electron bytecode compliler and append stderr error
|
||||||
|
- refactor: use rolldown instead of esbuild to bundle config file
|
||||||
|
- refactor: compile bytecode using the Electron main process or renderer process rather then via the `ELECTRON_RUN_AS_NODE`
|
||||||
|
- refactor!: simplify resolve config and isolate user config for sub-builds
|
||||||
|
- refactor: `?modulePath` sub-build output strategy
|
||||||
|
- perf(swcPlugin): skip redundant minification when using Oxc ([#922](https://github.com/alex8088/electron-vite/issues/922))
|
||||||
|
- perf: skip Babel loading in development and builds without bytecode
|
||||||
|
- perf: short-circuit bytecode detection in builds
|
||||||
|
- perf: setup rollupOption compatibility via config factory
|
||||||
|
- perf: build compatibility target for Electron 42, 43, 44
|
||||||
|
- fix(asset): use consistent file paths for asset cache lookups
|
||||||
|
- fix(bytecode): unable to obtain the bytecode build session in Node.js 24 ([#928](https://github.com/alex8088/electron-vite/issues/928))
|
||||||
|
- fix(dev): exit non-zero when Electron is killed by a signal ([#918](https://github.com/alex8088/electron-vite/issues/918))
|
||||||
|
- fix(dev): forward termination signals to Electron child ([#900](https://github.com/alex8088/electron-vite/issues/900))
|
||||||
|
- fix(dev): improve Electron process signal handling
|
||||||
|
- fix(bytecode): set the regular expression's `lastIndex` to 0 to perform a global match for chunkAlias option
|
||||||
|
- fix(esmShim)!: use `this.parse` to find the last static import ([#906](https://github.com/alex8088/electron-vite/issues/906))
|
||||||
|
- fix(deps)!: downgraded @babel/* to v7 due to Node requirements
|
||||||
|
- fix: remove useless preset config
|
||||||
|
- fix(swcPlugin): also disable oxc on Vite 8 ([#920](https://github.com/alex8088/electron-vite/issues/920))
|
||||||
|
- fix: clone config without plugins [#902](https://github.com/alex8088/electron-vite/issues/902)
|
||||||
|
- fix(deps)!: update magic-string to v1
|
||||||
|
- fix(deps)!: update @babel/* to v8
|
||||||
|
- fix!: compatible with rollupOptions and rolldownOptions
|
||||||
|
- fix: chunk filename is wrong in ES mode
|
||||||
|
- fix(asset): isolate asset caches per environment
|
||||||
|
- fix: use the environment api in plugins
|
||||||
|
- fix: skip TTY terminal manipulations in CI environments
|
||||||
|
- fix(deps)!: update vite to v8 ([#894](https://github.com/alex8088/electron-vite/issues/894))
|
||||||
|
- fix: Typescript compiler option 'outDir' must be located inside Rollup 'dir' option
|
||||||
|
- fix(isolateEntries): handle non-TTY stdout in build reporter (#887)
|
||||||
|
- chore: simplify bytecode compiler initialization
|
||||||
|
- chore: improve jsdoc for bytecode options ([#898](https://github.com/alex8088/electron-vite/issues/898))
|
||||||
|
- chore(deps): update all non-major dependencies
|
||||||
|
- chore(deps): update lint-staged to v17
|
||||||
|
- chore(deps): update cac to v7
|
||||||
|
- chore(deps): update globals to v17
|
||||||
|
- chore(deps): update eslint and @eslint/js to v10
|
||||||
|
- chore(deps): update all non-major dependencies
|
||||||
|
|
||||||
|
### v6.0.0-beta.8 (_2026-10-11_)
|
||||||
|
|
||||||
|
See [v6.0.0-beta.8 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.8/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v6.0.0-beta.7 (_2026-10-08_)
|
||||||
|
|
||||||
|
See [v6.0.0-beta.7 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.7/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v6.0.0-beta.6 (_2026-10-07_)
|
||||||
|
|
||||||
|
See [v6.0.0-beta.6 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.6/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v6.0.0-beta.5 (_2026-09-29_)
|
||||||
|
|
||||||
|
See [v6.0.0-beta.5 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.5/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v6.0.0-beta.4 (_2026-09-28_)
|
||||||
|
|
||||||
|
See [v6.0.0-beta.4 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.4/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v6.0.0-beta.3 (_2026-09-27_)
|
||||||
|
|
||||||
|
See [v6.0.0-beta.3 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.3/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v6.0.0-beta.2 (_2026-09-27_)
|
||||||
|
|
||||||
|
See [v6.0.0-beta.2 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.2/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v6.0.0-beta.1 (_2026-04-12_)
|
||||||
|
|
||||||
|
See [v6.0.0-beta.1 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.1/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v6.0.0-beta.0 (_2026-04-09_)
|
||||||
|
|
||||||
|
See [v6.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.0/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v5.0.0 (_2025-12-07_)
|
||||||
|
|
||||||
|
- feat(config): add `build.externalizeDeps` and `build.bytecode` config options to replace `externalizeDepsPlugin` and `bytecodePlugin`
|
||||||
|
- feat: reporter plugin for isolated builds
|
||||||
|
- feat: enhanced string protection
|
||||||
|
- feat: add `isolatedEntries` option for `preload` and `renderer` to build entries as standalone bundles [#154](https://github.com/alex8088/electron-vite/issues/154)
|
||||||
|
- refactor(config): move the `isolateEntries` options to the `build` option
|
||||||
|
- refactor: deprecated `externalizeDepsPlugin` and `bytecodePlugin`
|
||||||
|
- refactor(config)!: remove function resolution for nested config fields
|
||||||
|
- refactor(asset): remove redundant path normalization
|
||||||
|
- refactor: split electron plugin into preset and validator plugins
|
||||||
|
- refactor(config)!: restructure Electron Vite config interfaces
|
||||||
|
- refactor(build): simplify build logic
|
||||||
|
- refactor: replace `JSON.parse/stringify` with manual deep clone
|
||||||
|
- refactor(bytecodePlugin): improved bytecode bundle generation and made a new string protection plugin
|
||||||
|
- refactor(modulePath): better support for tree-shaking and code-splitting
|
||||||
|
- refactor: remove Electron 18, 19, 20, 21 build compatilibity target
|
||||||
|
- perf(buildReport): exclude node_modules from watch list
|
||||||
|
- perf(isolateEntries): transform log
|
||||||
|
- perf(isolateEntries): optimize entries transformation
|
||||||
|
- perf: build compatibility target for Electron 39
|
||||||
|
- perf(plugin): more efficient module filtering via regular expressions
|
||||||
|
- perf(plugin): no need to cache `sourcemap` option
|
||||||
|
- perf(plugin): lazily initialize `MagicString` and remove the redundant pre-check
|
||||||
|
- perf(bytecodePlugin): better way to count bytecode chunks
|
||||||
|
- perf(plugin): enhance path resolution using `import.meta.dirname` for ES modules
|
||||||
|
- fix(modulePath): rewrite the build input instead of merging
|
||||||
|
- fix(asset): normalize imported public asset chunk path
|
||||||
|
- fix: avoid duplicate chunk emission
|
||||||
|
- fix(modulePath): prevent duplicate asset emission
|
||||||
|
- fix(modulePath): support watch mode
|
||||||
|
- chore: fix jsdoc
|
||||||
|
- chore: improve logging message clarity and consistency
|
||||||
|
- chore(deps): update all non-major dependencies
|
||||||
|
- chore: update eslint config
|
||||||
|
- chore: replace `tseslint.config` with `defineConfig`
|
||||||
|
- chore: remove redundant external id
|
||||||
|
- chore: rename the file `esm` to `esmShim`
|
||||||
|
- docs: update
|
||||||
|
|
||||||
|
### v5.0.0-beta.3 (_2025-11-01_)
|
||||||
|
|
||||||
|
See [v5.0.0-beta.3 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.3/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v5.0.0-beta.2 (_2025-10-30_)
|
||||||
|
|
||||||
|
See [v5.0.0-beta.2 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.2/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v5.0.0-beta.1 (_2025-10-29_)
|
||||||
|
|
||||||
|
See [v5.0.0-beta.1 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.1/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v5.0.0-beta.0 (_2025-10-19_)
|
||||||
|
|
||||||
|
See [v5.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.0/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v4.0.1 (_2025-09-21_)
|
||||||
|
|
||||||
|
- perf: build compatibility target for Electron 38
|
||||||
|
|
||||||
|
### v4.0.0 (_2025-07-06_)
|
||||||
|
|
||||||
|
- refactor!: bump required node version to 20.19+, 22.12+
|
||||||
|
- fix(deps)!: update Vite to v7 and remove cjs build
|
||||||
|
- fix: use `import type` for type-only imports
|
||||||
|
- perf: build compatibility target for Electron 36 ([#766](https://github.com/alex8088/electron-vite/pull/766))
|
||||||
|
- perf: build compatibility target for Electron 37
|
||||||
|
- chore(deps): update pnpm to v10
|
||||||
|
- chore(deps): update all non-major dependencies
|
||||||
|
- chore(deps): update lint-staged to v16
|
||||||
|
|
||||||
|
### v4.0.0-beta.0 (_2025-06-28_)
|
||||||
|
|
||||||
|
See [v4.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v4.0.0-beta.0/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v3.1.0 (_2025-03-25_)
|
||||||
|
|
||||||
|
- fix(bytecodePlugin): optimize 'use strict' directive replacement ([#681](https://github.com/alex8088/electron-vite/issues/681))
|
||||||
|
- perf: build compatilibity target for Electron 35 ([#729](https://github.com/alex8088/electron-vite/pull/729))
|
||||||
|
- chore(deps): update all non-major dependencies
|
||||||
|
- chore(deps): update globals to v16
|
||||||
|
- chore(deps): update esbuild to v0.25
|
||||||
|
|
||||||
|
### v3.1.0-beta.0 (_2025-03-12_)
|
||||||
|
|
||||||
|
See [v3.1.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v3.1.0-beta.0/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v3.0.0 (_2025-02-16_)
|
||||||
|
|
||||||
|
- feat: resolve conditions for preload
|
||||||
|
- perf: build compatilibity target for Electron 32
|
||||||
|
- perf: build compatilibity target for Electron 33 ([#651](https://github.com/alex8088/electron-vite/pull/651))
|
||||||
|
- perf: build compatilibity target for Electron 34
|
||||||
|
- chore: move to eslint flat config
|
||||||
|
- chore(deps): update all non-major dependencies
|
||||||
|
- chore(deps): update @rollup/plugin-node-resolve to v16
|
||||||
|
- chore(deps): update @rollup/plugin-typescript to v12
|
||||||
|
- chore(deps): update esbuild to v0.24
|
||||||
|
- chore(deps): update vite to v6
|
||||||
|
- chore(deps): update @type/node to v22
|
||||||
|
|
||||||
|
### v3.0.0-beta.0 (_2025-01-22_)
|
||||||
|
|
||||||
|
See [v3.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v3.0.0-beta.0/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v2.3.0 (_2024-06-23_)
|
||||||
|
|
||||||
|
- feat: resolve import.meta.\[dirname|filename\] to support CommonJS format
|
||||||
|
- fix: don't handle module ID that begin with \0 ([#530](https://github.com/alex8088/electron-vite/pull/530))
|
||||||
|
- fix: not using the mode from the config file ([#539](https://github.com/alex8088/electron-vite/pull/539))
|
||||||
|
- fix: default mode should not overrite user config mode
|
||||||
|
- perf: build compatilibity target for Electron 31
|
||||||
|
- perf: improve cjs shim
|
||||||
|
- chore(deps): update all non-major dependencies
|
||||||
|
- chore(deps): update @typescript-eslint/\* to v7
|
||||||
|
- chore(deps): update esbuild to v0.21
|
||||||
|
|
||||||
|
### v2.2.0 (_2024-04-21_)
|
||||||
|
|
||||||
|
- feat: export mergeConfig from vite ([#471](https://github.com/alex8088/electron-vite/issues/471))
|
||||||
|
- fix(types): narrow down the return type of defineConfig
|
||||||
|
- perf: build compatilibity target for Electron 30
|
||||||
|
- refactor(config): defineConfig types
|
||||||
|
- chore: fix camelcase typo
|
||||||
|
- chore: use rollup-plugin-rm to clean dist
|
||||||
|
|
||||||
|
### v2.1.0 (_2024-03-03_)
|
||||||
|
|
||||||
|
- feat: easy way to fork processes and use workers
|
||||||
|
- fix: config via build.lib fails when default entry point not found ([#393](https://github.com/alex8088/electron-vite/issues/393))
|
||||||
|
- perf: build compatilibity target for Electron 29
|
||||||
|
- perf: allow integrating more complex render solutions ([#412](https://github.com/alex8088/electron-vite/pull/412))
|
||||||
|
- perf(bytecodePlugin): warn that strings cannot be protected when minification is enabled ([#417](https://github.com/alex8088/electron-vite/issues/417))
|
||||||
|
|
||||||
|
### v2.0.0 (_2024-01-09_)
|
||||||
|
|
||||||
|
- feat: bump minimum node version to 18
|
||||||
|
- feat: migrate to ESM
|
||||||
|
- feat: support vite 5
|
||||||
|
- feat: add package.json to export map
|
||||||
|
- feat: support ESM in Electron
|
||||||
|
- feat: env variables prefixed with VITE\_ will be shared in main process and renderer
|
||||||
|
- feat: support for passing arguments to electron in dev and preview commands ([#339](https://github.com/alex8088/electron-vite/pull/339))
|
||||||
|
- feat: config file supports "type": "module" in package.json
|
||||||
|
- fix: emit assets when ssr is enabled
|
||||||
|
- fix: externalizeDepPlugin not work
|
||||||
|
- fix: electron's export subpaths also need to be externalized ([#372](https://github.com/alex8088/electron-vite/issues/372))
|
||||||
|
- perf: improve package.json resolve
|
||||||
|
- perf: use magic-string hires boundary for sourcemaps
|
||||||
|
- perf: build compatilibity target for Electron 28
|
||||||
|
- pref: resolve import meta url in CommonJS format
|
||||||
|
- perf(worker): ESM syntax
|
||||||
|
- perf: package version
|
||||||
|
- perf: dev error message
|
||||||
|
- perf(externalizeDepsPlugin): use cached package data to improve performance
|
||||||
|
- perf: loadEnv api also needs to load shared env variables prefixed with VITE\_
|
||||||
|
- refactor: build
|
||||||
|
- refactor: file hashes use url-safe base64 encoded hashes in vite 5 (rollup 4)
|
||||||
|
- refactor: remove Electron 11, 12 build compatilibity target
|
||||||
|
- refactor: use dynamic import directly
|
||||||
|
- build: use rollup-plugin-dts
|
||||||
|
- chore(deps): update all non-major dependencies
|
||||||
|
- chore(deps): update lint-staged to v15
|
||||||
|
- chore(deps): update eslint-config-prettier to v9
|
||||||
|
- chore(deps): update @rollup/plugin-typescript to v11
|
||||||
|
- chore(deps): update rollup to v4
|
||||||
|
- chore(deps): update vite to v5
|
||||||
|
- chore(deps): update esbuild to v0.19
|
||||||
|
- chore(deps): update typescript to 5.3.3
|
||||||
|
- chore: improve prettier config
|
||||||
|
- chore: update homepage
|
||||||
|
|
||||||
|
### v2.0.0-beta.4 (_2024-01-06_)
|
||||||
|
|
||||||
|
See [v2.0.0-beta.4 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.4/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v2.0.0-beta.3 (_2024-01-04_)
|
||||||
|
|
||||||
|
See [v2.0.0-beta.3 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.3/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v2.0.0-beta.2 (_2023-12-19_)
|
||||||
|
|
||||||
|
See [v2.0.0-beta.2 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.2/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v2.0.0-beta.1 (_2023-12-14_)
|
||||||
|
|
||||||
|
See [v2.0.0-beta.1 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.1/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v2.0.0-beta.0 (_2023-12-13_)
|
||||||
|
|
||||||
|
See [v2.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.0/CHANGELOG.md)
|
||||||
|
|
||||||
|
### v1.0.29 (_2023-11-17_)
|
||||||
|
|
||||||
|
- 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_)
|
### v1.0.22 (_2023-04-23_)
|
||||||
|
|
||||||
- feat(cli): add --rendererOnly flag to dev command
|
- feat(cli): add --rendererOnly flag to dev command
|
||||||
|
|||||||
121
README.md
121
README.md
@ -9,17 +9,17 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/npm/v/electron-vite?color=6988e6&label=version">
|
<img src="https://img.shields.io/npm/v/electron-vite?color=6988e6&label=version">
|
||||||
<img src="https://img.shields.io/github/license/alex8088/wx-vue-next?color=blue" alt="license" />
|
<img src="https://img.shields.io/github/license/alex8088/electron-vite?color=blue" alt="license" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://evite.netlify.app/">Documentation</a> |
|
<a href="https://electron-vite.org">Documentation</a> |
|
||||||
<a href="https://evite.netlify.app/guide/">Getting Started</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>
|
<a href="https://github.com/alex8088/quick-start/tree/master/packages/create-electron">create-electron</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://cn-evite.netlify.app/">中文文档</a>
|
<a href="https://cn.electron-vite.org">中文文档</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
@ -28,15 +28,13 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- ⚡️ [Vite](https://vitejs.dev) powered and use the same way.
|
- ⚡️ [Vite](https://vitejs.dev) powered and use the same way.
|
||||||
- 🛠 Centralized Configuration.
|
- 🛠 Pre-configure with sensible defaults optimized for Electron.
|
||||||
- 💡 Pre-configured for Electron, don't worry about configuration.
|
- 💡 Optimize asset handling for Electron main process.
|
||||||
- 🚀 Fast HMR for renderers.
|
- 🚀 Fast HMR & hot reloading.
|
||||||
- 🔥 Hot reloading for main process and preload scripts.
|
- 🔥 Isolated build for multi-entry application development.
|
||||||
- 🔌 Easy to debug.
|
- ✨ Simplify multi-threading development.
|
||||||
- 🔋 Static asset handling (Node.js addons, WebAssembly, etc).
|
- 🔒 Compile code to v8 bytecode to protect source code.
|
||||||
- 🔒 Compile to v8 bytecode to protect source code.
|
- 🔌 Easy to debug in IDEs such as VSCode or WebStorm.
|
||||||
- 🏷️ Support for TypeScript decorators.
|
|
||||||
- 🔩 Easy to use workers and fork process.
|
|
||||||
- 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more.
|
- 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -61,7 +59,7 @@ In a project where `electron-vite` is installed, you can use `electron-vite` bin
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuring
|
### Configuration
|
||||||
|
|
||||||
When running `electron-vite` from the command line, electron-vite will automatically try to resolve a config file named `electron.vite.config.js` inside project root. The most basic config file looks like this:
|
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:
|
||||||
|
|
||||||
@ -80,97 +78,24 @@ export default {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Use HMR in Renderer
|
|
||||||
|
|
||||||
In order to use the renderer process HMR, you need to use the `environment variables` to determine whether the window browser loads a local html file or a local URL.
|
|
||||||
|
|
||||||
```js
|
|
||||||
function createWindow() {
|
|
||||||
// Create the browser window
|
|
||||||
const mainWindow = new BrowserWindow({
|
|
||||||
width: 800,
|
|
||||||
height: 600,
|
|
||||||
webPreferences: {
|
|
||||||
preload: path.join(__dirname, '../preload/index.js')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Load the remote URL for development or the local html file for production
|
|
||||||
if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) {
|
|
||||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
|
||||||
} else {
|
|
||||||
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hot Reloading
|
|
||||||
|
|
||||||
Hot reloading refers to quickly rebuilding and restarting the Electron app when the main process or preload scripts module changes. In fact, it's not really hot reloading, but similar. It also brings a good development experience to developers.
|
|
||||||
|
|
||||||
There are two ways to enable it:
|
|
||||||
|
|
||||||
1. Use CLI option `-w` or `--watch`, e.g. `electron-vite dev --watch`. This is the preferred way, it's more flexible.
|
|
||||||
|
|
||||||
2. Use configuration option `build.watch` and set to `{}`. In addition, more watcher options can be configured, see [WatcherOptions](https://rollupjs.org/guide/en/#watch-options).
|
|
||||||
|
|
||||||
### Debugging in VSCode
|
|
||||||
|
|
||||||
Add a file `.vscode/launch.json` with the following configuration:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Debug Main Process",
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"cwd": "${workspaceRoot}",
|
|
||||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
|
|
||||||
"windows": {
|
|
||||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
|
|
||||||
},
|
|
||||||
"runtimeArgs": ["--sourcemap"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, set some breakpoints in `main.ts` (source code), and start debugging in the `VSCode Debug View`.
|
|
||||||
|
|
||||||
### Source Code Protection
|
|
||||||
|
|
||||||
Use the plugin `bytecodePlugin` to enable it:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { defineConfig, bytecodePlugin } from 'electron-vite'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
main: {
|
|
||||||
plugins: [bytecodePlugin()]
|
|
||||||
},
|
|
||||||
preload: {
|
|
||||||
plugins: [bytecodePlugin()]
|
|
||||||
},
|
|
||||||
renderer: {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
`bytecodePlugin` only works in production and supports the main process and preload scripts.
|
|
||||||
|
|
||||||
Also, you can learn more by playing with the [example](https://github.com/alex8088/electron-vite-bytecode-example).
|
|
||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project.
|
Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm init @quick-start/electron
|
npm create @quick-start/electron@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Currently supported template presets include:
|
||||||
|
|
||||||
|
| JavaScript | TypeScript |
|
||||||
|
| :--------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------: |
|
||||||
|
| [vanilla](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vanilla) | [vanilla-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vanilla-ts) |
|
||||||
|
| [vue](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vue) | [vue-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vue-ts) |
|
||||||
|
| [react](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/react) | [react-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/react-ts) |
|
||||||
|
| [svelte](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/svelte) | [svelte-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/svelte-ts) |
|
||||||
|
| [solid](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/solid) | [solid-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/solid-ts) |
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
|
|
||||||
See [Contributing Guide](CONTRIBUTING.md).
|
See [Contributing Guide](CONTRIBUTING.md).
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
|
||||||
|
|
||||||
"mainEntryPointFilePath": "./dist/types/index.d.ts",
|
|
||||||
|
|
||||||
"dtsRollup": {
|
|
||||||
"enabled": true,
|
|
||||||
"untrimmedFilePath": "",
|
|
||||||
"publicTrimmedFilePath": "./dist/index.d.ts"
|
|
||||||
},
|
|
||||||
|
|
||||||
"apiReport": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"docModel": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"tsdocMetadata": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"messages": {
|
|
||||||
"compilerMessageReporting": {
|
|
||||||
"default": {
|
|
||||||
"logLevel": "warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"extractorMessageReporting": {
|
|
||||||
"default": {
|
|
||||||
"logLevel": "warning",
|
|
||||||
"addToApiReportFile": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"ae-missing-release-tag": {
|
|
||||||
"logLevel": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"tsdocMessageReporting": {
|
|
||||||
"default": {
|
|
||||||
"logLevel": "warning"
|
|
||||||
},
|
|
||||||
|
|
||||||
"tsdoc-undefined-tag": {
|
|
||||||
"logLevel": "none"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
bin/bytecode-compiler.cjs
Normal file
15
bin/bytecode-compiler.cjs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const vm = require('node:vm')
|
||||||
|
const v8 = require('node:v8')
|
||||||
|
|
||||||
|
v8.setFlagsFromString('--no-lazy')
|
||||||
|
v8.setFlagsFromString('--no-flush-bytecode')
|
||||||
|
|
||||||
|
const params = ['exports', 'require', 'module', '__filename', '__dirname']
|
||||||
|
|
||||||
|
module.exports = function compile(code) {
|
||||||
|
if (typeof code !== 'string') throw new TypeError('Compilation source must be a string')
|
||||||
|
const { cachedData } = vm.compileFunction(code, params, { produceCachedData: true })
|
||||||
|
if (!Buffer.isBuffer(cachedData)) throw new Error('V8 did not produce cached data')
|
||||||
|
// Explicit wire format works across both Node IPC and Electron structured clone.
|
||||||
|
return { sourceLength: code.length, cachedData: cachedData.toString('base64') }
|
||||||
|
}
|
||||||
109
bin/electron-bytecode-main.cjs
Normal file
109
bin/electron-bytecode-main.cjs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
const { app, BrowserWindow, ipcMain } = require('electron')
|
||||||
|
const path = require('node:path')
|
||||||
|
const RpcPeer = require('./rpc-peer.cjs')
|
||||||
|
const compile = require('./bytecode-compiler.cjs')
|
||||||
|
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
app.dock.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = 'electron-vite:bytecode:rpc'
|
||||||
|
const readyChannel = 'electron-vite:bytecode:ready'
|
||||||
|
let win
|
||||||
|
let rendererRPC
|
||||||
|
let rendererStarting
|
||||||
|
let stopping = false
|
||||||
|
|
||||||
|
function fail(error) {
|
||||||
|
console.error(error)
|
||||||
|
parentRPC.close(error)
|
||||||
|
rendererRPC?.close(error)
|
||||||
|
app.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentRPC = new RpcPeer(
|
||||||
|
message =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
if (!process.connected || !process.send) return reject(new Error('Parent IPC is disconnected'))
|
||||||
|
process.send(message, error => (error ? reject(error) : resolve()))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
function createRenderer() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let ready = false
|
||||||
|
const timer = setTimeout(() => finish(new Error('Compiler preload readiness timed out')), 30000)
|
||||||
|
function finish(error) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
ipcMain.removeListener(readyChannel, onReady)
|
||||||
|
if (error) reject(error)
|
||||||
|
else {
|
||||||
|
ready = true
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onReady(event) {
|
||||||
|
if (event.sender === win.webContents) finish()
|
||||||
|
}
|
||||||
|
function onFailure(error) {
|
||||||
|
if (stopping) return
|
||||||
|
if (ready) fail(error)
|
||||||
|
else finish(error)
|
||||||
|
}
|
||||||
|
ipcMain.on(readyChannel, onReady)
|
||||||
|
win = new BrowserWindow({
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'electron-bytecode-preload.cjs'),
|
||||||
|
sandbox: false,
|
||||||
|
contextIsolation: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
rendererRPC = new RpcPeer(message => {
|
||||||
|
if (win.isDestroyed()) throw new Error('Compiler window is closed')
|
||||||
|
win.webContents.send(channel, message)
|
||||||
|
})
|
||||||
|
ipcMain.on(channel, (event, message) => {
|
||||||
|
if (!win.isDestroyed() && event.sender === win.webContents) void rendererRPC.receive(message)
|
||||||
|
})
|
||||||
|
win.webContents.on('preload-error', (_event, _path, error) => onFailure(error))
|
||||||
|
win.webContents.on('render-process-gone', (_event, details) => {
|
||||||
|
onFailure(new Error(`Compiler renderer exited: ${details.reason}`))
|
||||||
|
})
|
||||||
|
win.on('closed', () => onFailure(new Error('Compiler window closed unexpectedly')))
|
||||||
|
win.loadURL('data:text/html,<!doctype html><html></html>').catch(onFailure)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialized = app.whenReady().then(() => true)
|
||||||
|
parentRPC.register('ready', () => initialized)
|
||||||
|
parentRPC.register('compile', async (target, code) => {
|
||||||
|
await initialized
|
||||||
|
if (target === 'main') return compile(code)
|
||||||
|
if (target === 'preload') {
|
||||||
|
rendererStarting ||= createRenderer()
|
||||||
|
await rendererStarting
|
||||||
|
return rendererRPC.request('compile', code)
|
||||||
|
}
|
||||||
|
throw new Error(`Invalid bytecode target: ${target}`)
|
||||||
|
})
|
||||||
|
initialized.catch(fail)
|
||||||
|
|
||||||
|
process.on('message', message => {
|
||||||
|
if (message?.type === 'shutdown') {
|
||||||
|
stopping = true
|
||||||
|
app.quit()
|
||||||
|
} else {
|
||||||
|
void parentRPC.receive(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
process.on('disconnect', () => {
|
||||||
|
stopping = true
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
stopping = true
|
||||||
|
parentRPC.close()
|
||||||
|
rendererRPC?.close()
|
||||||
|
})
|
||||||
|
if (!process.send) fail(new Error('Bytecode compiler must be launched with an IPC channel'))
|
||||||
10
bin/electron-bytecode-preload.cjs
Normal file
10
bin/electron-bytecode-preload.cjs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const { ipcRenderer } = require('electron')
|
||||||
|
const RpcPeer = require('./rpc-peer.cjs')
|
||||||
|
const compile = require('./bytecode-compiler.cjs')
|
||||||
|
|
||||||
|
const channel = 'electron-vite:bytecode:rpc'
|
||||||
|
const rpc = new RpcPeer(message => ipcRenderer.send(channel, message))
|
||||||
|
rpc.register('ready', () => true)
|
||||||
|
rpc.register('compile', compile)
|
||||||
|
ipcRenderer.on(channel, (_event, message) => void rpc.receive(message))
|
||||||
|
ipcRenderer.send('electron-vite:bytecode:ready')
|
||||||
@ -24,7 +24,7 @@ if (debugIndex > 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
require('../dist/cli')
|
import('../dist/cli.js')
|
||||||
}
|
}
|
||||||
|
|
||||||
run()
|
run()
|
||||||
|
|||||||
89
bin/rpc-peer.cjs
Normal file
89
bin/rpc-peer.cjs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Transport-independent peer. Messages must be JSON serializable.
|
||||||
|
class RpcPeer {
|
||||||
|
constructor(send, timeout = 30000) {
|
||||||
|
this.send = send
|
||||||
|
this.timeout = timeout
|
||||||
|
this.methods = new Map()
|
||||||
|
this.pending = new Map()
|
||||||
|
this.nextId = 0
|
||||||
|
this.closed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
register(cmd, method) {
|
||||||
|
this.methods.set(cmd, method)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
request(cmd, ...args) {
|
||||||
|
if (this.closed) return Promise.reject(new Error('RPC connection is closed'))
|
||||||
|
const invocationId = this.nextId++
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
this.pending.delete(invocationId)
|
||||||
|
reject(new Error(`RPC request timed out: ${cmd}`))
|
||||||
|
}, this.timeout)
|
||||||
|
this.pending.set(invocationId, { resolve, reject, timer })
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => this.send({ type: 'request', invocationId, cmd, args }))
|
||||||
|
.catch(error => {
|
||||||
|
const pending = this.pending.get(invocationId)
|
||||||
|
if (!pending) return
|
||||||
|
clearTimeout(timer)
|
||||||
|
this.pending.delete(invocationId)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async receive(message) {
|
||||||
|
if (this.closed || !message || !Number.isSafeInteger(message.invocationId)) return
|
||||||
|
const { type, invocationId } = message
|
||||||
|
if (type === 'response') {
|
||||||
|
const pending = this.pending.get(invocationId)
|
||||||
|
if (!pending) return
|
||||||
|
this.pending.delete(invocationId)
|
||||||
|
clearTimeout(pending.timer)
|
||||||
|
if (message.error) {
|
||||||
|
const error = new Error(message.error.message)
|
||||||
|
error.name = message.error.name || 'Error'
|
||||||
|
if (message.error.stack) error.stack = message.error.stack
|
||||||
|
pending.reject(error)
|
||||||
|
} else {
|
||||||
|
pending.resolve(message.result)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (type !== 'request' || typeof message.cmd !== 'string' || !Array.isArray(message.args)) return
|
||||||
|
const response = { type: 'response', invocationId }
|
||||||
|
try {
|
||||||
|
const method = this.methods.get(message.cmd)
|
||||||
|
if (!method) throw new Error(`Invalid method: ${message.cmd}`)
|
||||||
|
response.result = await method(...message.args)
|
||||||
|
} catch (error) {
|
||||||
|
response.error = {
|
||||||
|
name: error instanceof Error ? error.name : 'Error',
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
stack: error instanceof Error ? error.stack : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.closed) {
|
||||||
|
try {
|
||||||
|
await this.send(response)
|
||||||
|
} catch (error) {
|
||||||
|
this.close(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(error = new Error('RPC connection is closed')) {
|
||||||
|
this.closed = true
|
||||||
|
for (const pending of this.pending.values()) {
|
||||||
|
clearTimeout(pending.timer)
|
||||||
|
pending.reject(error)
|
||||||
|
}
|
||||||
|
this.pending.clear()
|
||||||
|
this.methods.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RpcPeer
|
||||||
69
eslint.config.js
Normal file
69
eslint.config.js
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
85
node.d.ts
vendored
85
node.d.ts
vendored
@ -1,9 +1,15 @@
|
|||||||
// node worker
|
// node worker
|
||||||
declare module '*?nodeWorker' {
|
declare module '*?nodeWorker' {
|
||||||
import { Worker, WorkerOptions } from 'node:worker_threads'
|
import type { Worker, WorkerOptions } from 'node:worker_threads'
|
||||||
export default function (options: WorkerOptions): Worker
|
export default function (options: WorkerOptions): Worker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// module path
|
||||||
|
declare module '*?modulePath' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
|
||||||
// node asset
|
// node asset
|
||||||
declare module '*?asset' {
|
declare module '*?asset' {
|
||||||
const src: string
|
const src: string
|
||||||
@ -15,6 +21,11 @@ declare module '*?asset&asarUnpack' {
|
|||||||
export default src
|
export default src
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '*.json?commonjs-external&asset' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
|
||||||
// native node module
|
// native node module
|
||||||
declare module '*.node' {
|
declare module '*.node' {
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
@ -27,3 +38,75 @@ declare module '*.wasm?loader' {
|
|||||||
const loadWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>
|
const loadWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>
|
||||||
export default loadWasm
|
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
|
||||||
|
}
|
||||||
|
|||||||
82
package.json
82
package.json
@ -1,9 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "electron-vite",
|
"name": "electron-vite",
|
||||||
"version": "1.0.22",
|
"version": "6.0.0",
|
||||||
"description": "Electron build tooling based on Vite",
|
"description": "Electron build tooling based on Vite",
|
||||||
"main": "dist/index.js",
|
"type": "module",
|
||||||
"types": "dist/index.d.ts",
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/index.js",
|
||||||
|
"./node": {
|
||||||
|
"types": "./node.d.ts"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"electron-vite": "bin/electron-vite.js"
|
"electron-vite": "bin/electron-vite.js"
|
||||||
},
|
},
|
||||||
@ -13,8 +21,9 @@
|
|||||||
"node.d.ts"
|
"node.d.ts"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.0.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
|
"packageManager": "pnpm@10.12.4",
|
||||||
"author": "Alex Wei<https://github.com/alex8088>",
|
"author": "Alex Wei<https://github.com/alex8088>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
@ -24,7 +33,7 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/alex8088/electron-vite/issues"
|
"url": "https://github.com/alex8088/electron-vite/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/alex8088/electron-vite#readme",
|
"homepage": "https://electron-vite.org",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"electron",
|
"electron",
|
||||||
"vite",
|
"vite",
|
||||||
@ -33,9 +42,9 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "eslint --ext .ts src/**",
|
"lint": "eslint --cache .",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "npm run lint && node scripts/build.js"
|
"build": "pnpm run lint && rollup -c rollup.config.ts --configPlugin typescript"
|
||||||
},
|
},
|
||||||
"simple-git-hooks": {
|
"simple-git-hooks": {
|
||||||
"pre-commit": "npx lint-staged",
|
"pre-commit": "npx lint-staged",
|
||||||
@ -52,7 +61,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@swc/core": "^1.0.0",
|
"@swc/core": "^1.0.0",
|
||||||
"vite": "^3.0.0 || ^4.0.0"
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@swc/core": {
|
"@swc/core": {
|
||||||
@ -60,31 +69,40 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/api-extractor": "^7.34.4",
|
"@eslint/js": "^10.0.1",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
"@rollup/plugin-typescript": "^9.0.2",
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||||
"@swc/core": "^1.3.37",
|
"@rollup/plugin-typescript": "^12.3.0",
|
||||||
"@types/node": "16.18.3",
|
"@swc/core": "^1.16.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
"@types/babel__core": "^7.20.5",
|
||||||
"@typescript-eslint/parser": "^5.54.0",
|
"@types/node": "^22.20.4",
|
||||||
"eslint": "^8.35.0",
|
"eslint": "^10.11.0",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"fs-extra": "^10.1.0",
|
"globals": "^17.12.0",
|
||||||
"lint-staged": "^13.1.2",
|
"lint-staged": "^17.5.1",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^3.9.9",
|
||||||
"rollup": "^3.18.0",
|
"rollup": "^4.63.5",
|
||||||
"simple-git-hooks": "^2.8.1",
|
"rollup-plugin-dts": "^6.5.1",
|
||||||
"tslib": "^2.5.0",
|
"rollup-plugin-rm": "^1.0.2",
|
||||||
"typescript": "^4.8.4",
|
"simple-git-hooks": "^2.14.0",
|
||||||
"vite": "^4.2.1"
|
"tslib": "^2.8.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.70.1",
|
||||||
|
"vite": "^8.3.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.21.0",
|
"@babel/core": "^7.29.7",
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.20.7",
|
"@babel/plugin-transform-arrow-functions": "^7.29.7",
|
||||||
"cac": "^6.7.14",
|
"cac": "^7.0.0",
|
||||||
"esbuild": "^0.17.13",
|
"magic-string": "^1.4.2",
|
||||||
"magic-string": "^0.30.0",
|
"picocolors": "^1.1.1",
|
||||||
"picocolors": "^1.0.0"
|
"rolldown": "~1.2.10"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@swc/core",
|
||||||
|
"simple-git-hooks"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4151
pnpm-lock.yaml
generated
4151
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
42
rollup.config.ts
Normal file
42
rollup.config.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { createRequire } from 'node:module'
|
||||||
|
import { defineConfig } from 'rollup'
|
||||||
|
import ts from '@rollup/plugin-typescript'
|
||||||
|
import resolve from '@rollup/plugin-node-resolve'
|
||||||
|
import json from '@rollup/plugin-json'
|
||||||
|
import dts from 'rollup-plugin-dts'
|
||||||
|
import rm from 'rollup-plugin-rm'
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
const pkg = require('./package.json')
|
||||||
|
|
||||||
|
const external = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
input: ['src/index.ts', 'src/cli.ts'],
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
dir: 'dist',
|
||||||
|
entryFileNames: '[name].js',
|
||||||
|
chunkFileNames: 'chunks/lib-[hash].js',
|
||||||
|
format: 'es'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
rm('dist', 'buildStart'),
|
||||||
|
json(),
|
||||||
|
ts({ compilerOptions: { rootDir: 'src', outDir: 'dist', declaration: true, declarationDir: 'dist/types' } }),
|
||||||
|
resolve()
|
||||||
|
],
|
||||||
|
treeshake: {
|
||||||
|
moduleSideEffects: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'dist/types/index.d.ts',
|
||||||
|
external: ['vite'],
|
||||||
|
output: [{ file: pkg.types, format: 'es' }],
|
||||||
|
plugins: [dts(), rm('dist/types', 'buildEnd')]
|
||||||
|
}
|
||||||
|
])
|
||||||
@ -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`))
|
|
||||||
})()
|
|
||||||
@ -1,13 +1,13 @@
|
|||||||
// Invoked on the commit-msg git hook by simple-git-hooks.
|
// Invoked on the commit-msg git hook by simple-git-hooks.
|
||||||
|
|
||||||
const colors = require('picocolors')
|
import colors from 'picocolors'
|
||||||
const fs = require('fs')
|
import fs from 'node:fs'
|
||||||
|
|
||||||
const msgPath = process.argv[2]
|
const msgPath = process.argv[2]
|
||||||
const msg = fs.readFileSync(msgPath, 'utf-8').trim()
|
const msg = fs.readFileSync(msgPath, 'utf-8').trim()
|
||||||
|
|
||||||
const commitRE =
|
const commitRE =
|
||||||
/^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?: .{1,50}/
|
/^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?!?: .{1,50}/
|
||||||
|
|
||||||
if (!commitRE.test(msg)) {
|
if (!commitRE.test(msg)) {
|
||||||
console.log()
|
console.log()
|
||||||
|
|||||||
69
src/build.ts
69
src/build.ts
@ -1,33 +1,56 @@
|
|||||||
import { build as viteBuild } from 'vite'
|
import { build as viteBuild, mergeConfig } from 'vite'
|
||||||
import { InlineConfig, resolveConfig } from './config'
|
import { type InlineConfig, type MainViteConfig, type PreloadViteConfig, resolveConfig } from './config'
|
||||||
|
import { BytecodeCompiler } from './bytecodeCompiler'
|
||||||
|
import { asyncFlatten } from './utils'
|
||||||
|
|
||||||
|
async function usesBytecode(config?: MainViteConfig | PreloadViteConfig): Promise<boolean> {
|
||||||
|
if (!config) return false
|
||||||
|
if (config.build?.bytecode) return true
|
||||||
|
const plugins = await asyncFlatten(config.plugins || [])
|
||||||
|
return plugins.some(plugin => plugin && typeof plugin === 'object' && plugin.name === 'vite:bytecode')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bundles the electron app for production.
|
* Bundles the electron app for production.
|
||||||
*/
|
*/
|
||||||
export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
|
export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
|
||||||
process.env.NODE_ENV_ELECTRON_VITE = 'production'
|
process.env.NODE_ENV_ELECTRON_VITE = 'production'
|
||||||
const config = await resolveConfig(inlineConfig, 'build', 'production')
|
|
||||||
if (config.config) {
|
const bytecodeCompiler = new BytecodeCompiler()
|
||||||
const mainViteConfig = config.config?.main
|
const config = await resolveConfig(
|
||||||
if (mainViteConfig) {
|
mergeConfig(inlineConfig, {
|
||||||
if (mainViteConfig.build?.watch) {
|
plugins: [{ name: 'vite:electron-bytecode-build', api: { compiler: bytecodeCompiler } }]
|
||||||
mainViteConfig.build.watch = null
|
}),
|
||||||
|
'build',
|
||||||
|
'production'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!config.config) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasBytecode = (await usesBytecode(config.config.main)) || (await usesBytecode(config.config.preload))
|
||||||
|
|
||||||
|
const buildTargets = async (): Promise<void> => {
|
||||||
|
// Build targets in order: main -> preload -> renderer
|
||||||
|
for (const target of ['main', 'preload', 'renderer'] as const) {
|
||||||
|
const viteConfig = config.config?.[target]
|
||||||
|
if (viteConfig) {
|
||||||
|
// Disable watch mode in production builds
|
||||||
|
if (viteConfig.build?.watch) {
|
||||||
|
viteConfig.build.watch = null
|
||||||
|
}
|
||||||
|
await viteBuild(viteConfig)
|
||||||
}
|
}
|
||||||
await viteBuild(mainViteConfig)
|
|
||||||
}
|
|
||||||
const preloadViteConfig = config.config?.preload
|
|
||||||
if (preloadViteConfig) {
|
|
||||||
if (preloadViteConfig.build?.watch) {
|
|
||||||
preloadViteConfig.build.watch = null
|
|
||||||
}
|
|
||||||
await viteBuild(preloadViteConfig)
|
|
||||||
}
|
|
||||||
const rendererViteConfig = config.config?.renderer
|
|
||||||
if (rendererViteConfig) {
|
|
||||||
if (rendererViteConfig.build?.watch) {
|
|
||||||
rendererViteConfig.build.watch = null
|
|
||||||
}
|
|
||||||
await viteBuild(rendererViteConfig)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasBytecode) return buildTargets()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bytecodeCompiler.start()
|
||||||
|
await buildTargets()
|
||||||
|
} finally {
|
||||||
|
await bytecodeCompiler.stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
188
src/bytecodeCompiler.ts
Normal file
188
src/bytecodeCompiler.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import { type ChildProcess, spawn } from 'node:child_process'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { createRequire } from 'node:module'
|
||||||
|
import { getElectronPath } from './electron'
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
const RPC_TIMEOUT = 30_000
|
||||||
|
const SHUTDOWN_TIMEOUT = 3_000
|
||||||
|
|
||||||
|
export type BytecodeCompileResult = {
|
||||||
|
sourceLength: number
|
||||||
|
/** Base64-encoded V8 cachedData; decode with Buffer.from(cachedData, 'base64'). */
|
||||||
|
cachedData: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BytecodeTarget = 'main' | 'preload'
|
||||||
|
|
||||||
|
type CompilerResponse = {
|
||||||
|
type: 'response'
|
||||||
|
invocationId: number
|
||||||
|
result?: unknown
|
||||||
|
error?: { name: string; message: string; stack?: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
type PendingRequest = {
|
||||||
|
resolve: (value: unknown) => void
|
||||||
|
reject: (reason: Error) => void
|
||||||
|
timer: ReturnType<typeof setTimeout>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BytecodeCompiler {
|
||||||
|
private ps?: ChildProcess
|
||||||
|
private rpcMap = new Map<number, PendingRequest>()
|
||||||
|
private invocationId = 0
|
||||||
|
private starting?: Promise<void>
|
||||||
|
private stopping?: Promise<void>
|
||||||
|
private ready = false
|
||||||
|
private stderr = ''
|
||||||
|
|
||||||
|
start(): Promise<void> {
|
||||||
|
if (this.stopping) return Promise.reject(new Error('BytecodeCompiler is stopping'))
|
||||||
|
if (this.starting) return this.starting
|
||||||
|
if (this.ready) return Promise.resolve()
|
||||||
|
if (this.ps) return Promise.reject(new Error('Previous Electron process has not closed; await stop() first'))
|
||||||
|
this.starting = this.initialize().finally(() => {
|
||||||
|
this.starting = undefined
|
||||||
|
})
|
||||||
|
return this.starting
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initialize(): Promise<void> {
|
||||||
|
const env: NodeJS.ProcessEnv = { ...process.env, ELECTRON_VITE_RPC: 'true' }
|
||||||
|
delete env.ELECTRON_RUN_AS_NODE
|
||||||
|
this.stderr = ''
|
||||||
|
const compilerPath = path.join(
|
||||||
|
path.dirname(require.resolve('electron-vite/package.json')),
|
||||||
|
'bin',
|
||||||
|
'electron-bytecode-main.cjs'
|
||||||
|
)
|
||||||
|
const ps = spawn(getElectronPath(), [compilerPath], {
|
||||||
|
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
||||||
|
windowsHide: true,
|
||||||
|
env
|
||||||
|
})
|
||||||
|
this.ps = ps
|
||||||
|
if (ps.stderr) {
|
||||||
|
ps.stderr.setEncoding('utf8')
|
||||||
|
ps.stderr.on('data', (chunk: string) => {
|
||||||
|
if (this.ps === ps) this.stderr = (this.stderr + chunk).slice(-64 * 1024)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ps.on('message', (message: CompilerResponse) => {
|
||||||
|
if (this.ps !== ps || !message || message.type !== 'response') return
|
||||||
|
const pending = this.rpcMap.get(message.invocationId)
|
||||||
|
if (!pending) return
|
||||||
|
clearTimeout(pending.timer)
|
||||||
|
this.rpcMap.delete(message.invocationId)
|
||||||
|
if (message.error) {
|
||||||
|
const error = new Error(message.error.message)
|
||||||
|
error.name = message.error.name
|
||||||
|
if (message.error.stack) error.stack = message.error.stack
|
||||||
|
pending.reject(error)
|
||||||
|
} else pending.resolve(message.result)
|
||||||
|
})
|
||||||
|
ps.on('error', error => {
|
||||||
|
if (this.ps === ps) this.rejectPending(error)
|
||||||
|
})
|
||||||
|
ps.on('disconnect', () => {
|
||||||
|
if (this.ps === ps) this.rejectPending(new Error('Electron IPC disconnected'))
|
||||||
|
})
|
||||||
|
ps.on('exit', (code, signal) => {
|
||||||
|
if (this.ps === ps) this.rejectPending(new Error('Electron exited (code ' + code + ', signal ' + signal + ')'))
|
||||||
|
})
|
||||||
|
ps.on('close', () => {
|
||||||
|
if (this.ps === ps) {
|
||||||
|
this.rejectPending(new Error('Electron process closed'))
|
||||||
|
this.ps = undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
await this.sendRequest('ready', [])
|
||||||
|
if (!ps.connected || this.stopping) throw new Error('Electron stopped during initialization')
|
||||||
|
this.ready = true
|
||||||
|
} catch (error) {
|
||||||
|
await this.stop()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compile(target: BytecodeTarget, code: string): Promise<BytecodeCompileResult> {
|
||||||
|
return this.request<BytecodeCompileResult>('compile', target, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
async request<T = unknown>(cmd: string, ...args: unknown[]): Promise<T> {
|
||||||
|
if (!this.ready || this.stopping) throw new Error('BytecodeCompiler is not ready; await start() first')
|
||||||
|
return this.sendRequest(cmd, args) as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendRequest(cmd: string, args: unknown[]): Promise<unknown> {
|
||||||
|
const ps = this.ps
|
||||||
|
if (!ps?.connected) return Promise.reject(new Error('Electron IPC is not connected'))
|
||||||
|
const invocationId = this.invocationId++
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fail = (error: Error): void => {
|
||||||
|
const pending = this.rpcMap.get(invocationId)
|
||||||
|
if (!pending) return
|
||||||
|
clearTimeout(pending.timer)
|
||||||
|
this.rpcMap.delete(invocationId)
|
||||||
|
reject(this.withStderr(error))
|
||||||
|
}
|
||||||
|
const timer = setTimeout(() => fail(new Error('Electron RPC timed out: ' + cmd)), RPC_TIMEOUT)
|
||||||
|
this.rpcMap.set(invocationId, { resolve, reject, timer })
|
||||||
|
try {
|
||||||
|
ps.send({ type: 'request', invocationId, cmd, args }, error => {
|
||||||
|
if (error) fail(error)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
fail(error instanceof Error ? error : new Error(String(error)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private rejectPending(error: Error): void {
|
||||||
|
this.ready = false
|
||||||
|
if (this.rpcMap.size) this.withStderr(error)
|
||||||
|
for (const pending of this.rpcMap.values()) {
|
||||||
|
clearTimeout(pending.timer)
|
||||||
|
pending.reject(error)
|
||||||
|
}
|
||||||
|
this.rpcMap.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private withStderr(error: Error): Error {
|
||||||
|
const stderr = this.stderr.trim()
|
||||||
|
if (stderr) {
|
||||||
|
const stack = error.stack
|
||||||
|
error.message += '\n' + stderr
|
||||||
|
if (stack) error.stack = stack + '\n' + stderr
|
||||||
|
}
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(): Promise<void> {
|
||||||
|
if (this.stopping) return this.stopping
|
||||||
|
this.rejectPending(new Error('BytecodeCompiler stopped'))
|
||||||
|
const ps = this.ps
|
||||||
|
if (!ps) return Promise.resolve()
|
||||||
|
this.stopping = new Promise<void>(resolve => {
|
||||||
|
const timer = setTimeout(() => ps.kill('SIGKILL'), SHUTDOWN_TIMEOUT)
|
||||||
|
ps.once('close', () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
if (ps.connected) {
|
||||||
|
try {
|
||||||
|
ps.send({ type: 'shutdown' }, error => {
|
||||||
|
if (error) ps.kill()
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
ps.kill()
|
||||||
|
}
|
||||||
|
} else ps.kill()
|
||||||
|
}).finally(() => {
|
||||||
|
this.stopping = undefined
|
||||||
|
})
|
||||||
|
return this.stopping
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/cli.ts
66
src/cli.ts
@ -1,7 +1,8 @@
|
|||||||
import { cac } from 'cac'
|
import { cac } from 'cac'
|
||||||
import colors from 'picocolors'
|
import colors from 'picocolors'
|
||||||
import { LogLevel, createLogger } from 'vite'
|
import { type LogLevel, createLogger } from 'vite'
|
||||||
import { InlineConfig } from './config'
|
import type { InlineConfig } from './config'
|
||||||
|
import { version } from '../package.json'
|
||||||
|
|
||||||
const cli = cac('electron-vite')
|
const cli = cac('electron-vite')
|
||||||
|
|
||||||
@ -24,6 +25,20 @@ interface GlobalCLIOptions {
|
|||||||
w?: boolean
|
w?: boolean
|
||||||
watch?: boolean
|
watch?: boolean
|
||||||
outDir?: string
|
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 {
|
function createInlineConfig(root: string, options: GlobalCLIOptions): InlineConfig {
|
||||||
@ -52,6 +67,7 @@ cli
|
|||||||
.option('--ignoreConfigWarning', `[boolean] ignore config warning`)
|
.option('--ignoreConfigWarning', `[boolean] ignore config warning`)
|
||||||
.option('--sourcemap', `[boolean] output source maps for debug (default: false)`)
|
.option('--sourcemap', `[boolean] output source maps for debug (default: false)`)
|
||||||
.option('--outDir <dir>', `[string] output directory (default: out)`)
|
.option('--outDir <dir>', `[string] output directory (default: out)`)
|
||||||
|
.option('--entry <file>', `[string] specify electron entry file`)
|
||||||
|
|
||||||
// dev
|
// dev
|
||||||
cli
|
cli
|
||||||
@ -59,13 +75,36 @@ cli
|
|||||||
.alias('serve')
|
.alias('serve')
|
||||||
.alias('dev')
|
.alias('dev')
|
||||||
.option('-w, --watch', `[boolean] rebuilds when main process or preload script modules have changed on disk`)
|
.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('--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`)
|
.option('--rendererOnly', `[boolean] only dev server for the renderer`)
|
||||||
.action(async (root: string, options: { remoteDebuggingPort?: string; rendererOnly: boolean } & GlobalCLIOptions) => {
|
.action(async (root: string, options: DevCLIOptions & GlobalCLIOptions) => {
|
||||||
if (options.remoteDebuggingPort) {
|
if (options.remoteDebuggingPort) {
|
||||||
process.env.REMOTE_DEBUGGING_PORT = 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 { createServer } = await import('./server')
|
||||||
const inlineConfig = createInlineConfig(root, options)
|
const inlineConfig = createInlineConfig(root, options)
|
||||||
|
|
||||||
@ -86,6 +125,10 @@ cli.command('build [root]', 'build for production').action(async (root: string,
|
|||||||
const { build } = await import('./build')
|
const { build } = await import('./build')
|
||||||
const inlineConfig = createInlineConfig(root, options)
|
const inlineConfig = createInlineConfig(root, options)
|
||||||
|
|
||||||
|
if (options.entry) {
|
||||||
|
process.env.ELECTRON_ENTRY = options.entry
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await build(inlineConfig)
|
await build(inlineConfig)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -98,11 +141,24 @@ cli.command('build [root]', 'build for production').action(async (root: string,
|
|||||||
// preview
|
// preview
|
||||||
cli
|
cli
|
||||||
.command('preview [root]', 'start electron app to preview production build')
|
.command('preview [root]', 'start electron app to preview production build')
|
||||||
|
.option('--noSandbox', `[boolean] forces renderer process to run un-sandboxed`)
|
||||||
.option('--skipBuild', `[boolean] skip build`)
|
.option('--skipBuild', `[boolean] skip build`)
|
||||||
.action(async (root: string, options: { skipBuild?: boolean } & GlobalCLIOptions) => {
|
.action(async (root: string, options: PreviewCLIOptions & GlobalCLIOptions) => {
|
||||||
const { preview } = await import('./preview')
|
const { preview } = await import('./preview')
|
||||||
const inlineConfig = createInlineConfig(root, options)
|
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 {
|
try {
|
||||||
await preview(inlineConfig, { skipBuild: options.skipBuild })
|
await preview(inlineConfig, { skipBuild: options.skipBuild })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -113,6 +169,6 @@ cli
|
|||||||
})
|
})
|
||||||
|
|
||||||
cli.help()
|
cli.help()
|
||||||
cli.version(require('../package.json').version)
|
cli.version(version)
|
||||||
|
|
||||||
cli.parse()
|
cli.parse()
|
||||||
|
|||||||
511
src/config.ts
511
src/config.ts
@ -1,77 +1,124 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import fs from 'node:fs'
|
|
||||||
import { pathToFileURL } from 'node:url'
|
|
||||||
import { createRequire } from 'node:module'
|
|
||||||
import colors from 'picocolors'
|
import colors from 'picocolors'
|
||||||
import {
|
import {
|
||||||
type UserConfig as ViteConfig,
|
type UserConfig as ViteConfig,
|
||||||
type UserConfigExport as UserViteConfigExport,
|
|
||||||
type ConfigEnv,
|
type ConfigEnv,
|
||||||
|
type PluginOption,
|
||||||
type Plugin,
|
type Plugin,
|
||||||
|
type BuildEnvironmentOptions as ViteBuildOptions,
|
||||||
type LogLevel,
|
type LogLevel,
|
||||||
createLogger,
|
createLogger,
|
||||||
mergeConfig,
|
mergeConfig,
|
||||||
normalizePath
|
normalizePath
|
||||||
} from 'vite'
|
} from 'vite'
|
||||||
import { build } from 'esbuild'
|
|
||||||
|
|
||||||
import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugins/electron'
|
import {
|
||||||
|
electronMainConfigPresetPlugin,
|
||||||
|
electronMainConfigValidatorPlugin,
|
||||||
|
electronPreloadConfigPresetPlugin,
|
||||||
|
electronPreloadConfigValidatorPlugin,
|
||||||
|
electronRendererConfigPresetPlugin,
|
||||||
|
electronRendererConfigValidatorPlugin
|
||||||
|
} from './plugins/electron'
|
||||||
import assetPlugin from './plugins/asset'
|
import assetPlugin from './plugins/asset'
|
||||||
import workerPlugin from './plugins/worker'
|
import workerPlugin from './plugins/worker'
|
||||||
import { isObject, dynamicImport } from './utils'
|
import importMetaPlugin from './plugins/importMeta'
|
||||||
|
import esmShimPlugin from './plugins/esmShim'
|
||||||
|
import modulePathPlugin from './plugins/modulePath'
|
||||||
|
import isolateEntriesPlugin from './plugins/isolateEntries'
|
||||||
|
import { type ExternalOptions, externalizeDepsPlugin } from './plugins/externalizeDeps'
|
||||||
|
import { type BytecodeOptions, bytecodePlugin } from './plugins/bytecode'
|
||||||
|
import { isObject, isFilePathESM, cloneConfig, asyncFlatten } from './utils'
|
||||||
|
import { findConfigFile, bundleConfigFile, loadConfigFormBundledFile } from './load'
|
||||||
|
|
||||||
export { defineConfig as defineViteConfig } from 'vite'
|
export { defineConfig as defineViteConfig } from 'vite'
|
||||||
|
|
||||||
|
interface IsolatedEntriesMixin {
|
||||||
|
/**
|
||||||
|
* Build each entry point as an isolated bundle without code splitting.
|
||||||
|
*
|
||||||
|
* When enabled, each entry will include all its dependencies inline,
|
||||||
|
* preventing automatic code splitting across entries and ensuring each
|
||||||
|
* output file is fully standalone.
|
||||||
|
*
|
||||||
|
* **Important**: When using `isolatedEntries` in `preload` config, you
|
||||||
|
* should also disable `build.externalizeDeps` to ensure third-party dependencies
|
||||||
|
* from `node_modules` are bundled together, which is required for Electron
|
||||||
|
* sandbox support.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
isolatedEntries?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExternalizeDepsMixin {
|
||||||
|
/**
|
||||||
|
* Options pass on to `externalizeDeps` plugin in electron-vite.
|
||||||
|
*
|
||||||
|
* Automatically externalize dependencies.
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
externalizeDeps?: boolean | ExternalOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BytecodeMixin {
|
||||||
|
/**
|
||||||
|
* Options pass on to `bytecode` plugin in electron-vite.
|
||||||
|
* https://electron-vite.org/guide/source-code-protection#options
|
||||||
|
*
|
||||||
|
* Compiles source code to v8 bytecode and works only in the `build` command.
|
||||||
|
*/
|
||||||
|
bytecode?: boolean | BytecodeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MainBuildOptions extends ViteBuildOptions, ExternalizeDepsMixin, BytecodeMixin {}
|
||||||
|
|
||||||
|
interface PreloadBuildOptions extends ViteBuildOptions, ExternalizeDepsMixin, BytecodeMixin, IsolatedEntriesMixin {}
|
||||||
|
|
||||||
|
interface RendererBuildOptions extends ViteBuildOptions, IsolatedEntriesMixin {}
|
||||||
|
|
||||||
|
interface BaseViteConfig<T> extends Omit<ViteConfig, 'build'> {
|
||||||
|
/**
|
||||||
|
* Build specific options
|
||||||
|
*/
|
||||||
|
build?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainViteConfig extends BaseViteConfig<MainBuildOptions> {}
|
||||||
|
|
||||||
|
export interface PreloadViteConfig extends BaseViteConfig<PreloadBuildOptions> {}
|
||||||
|
|
||||||
|
export interface RendererViteConfig extends BaseViteConfig<RendererBuildOptions> {}
|
||||||
|
|
||||||
export interface UserConfig {
|
export interface UserConfig {
|
||||||
/**
|
/**
|
||||||
* Vite config options for electron main process
|
* Vite config options for electron main process
|
||||||
*
|
*
|
||||||
* https://cn.vitejs.dev/config/
|
* @see https://vitejs.dev/config/
|
||||||
*/
|
*/
|
||||||
main?: ViteConfig & { configFile?: string | false }
|
main?: MainViteConfig
|
||||||
/**
|
/**
|
||||||
* Vite config options for electron renderer process
|
* Vite config options for electron renderer process
|
||||||
*
|
*
|
||||||
* https://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 {
|
export type ElectronViteConfigFnObject = (env: ConfigEnv) => UserConfig
|
||||||
/**
|
export type ElectronViteConfigFnPromise = (env: ConfigEnv) => Promise<UserConfig>
|
||||||
* Vite config options for electron main process
|
export type ElectronViteConfigFn = (env: ConfigEnv) => UserConfig | Promise<UserConfig>
|
||||||
*
|
|
||||||
* https://cn.vitejs.dev/config/
|
|
||||||
*/
|
|
||||||
main?: UserViteConfigExport
|
|
||||||
/**
|
|
||||||
* Vite config options for electron renderer process
|
|
||||||
*
|
|
||||||
* https://cn.vitejs.dev/config/
|
|
||||||
*/
|
|
||||||
renderer?: UserViteConfigExport
|
|
||||||
/**
|
|
||||||
* Vite config options for electron preload files
|
|
||||||
*
|
|
||||||
* https://cn.vitejs.dev/config/
|
|
||||||
*/
|
|
||||||
preload?: UserViteConfigExport
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InlineConfig = Omit<ViteConfig, 'base'> & {
|
export type ElectronViteConfigExport =
|
||||||
configFile?: string | false
|
UserConfig | Promise<UserConfig> | ElectronViteConfigFnObject | ElectronViteConfigFnPromise | ElectronViteConfigFn
|
||||||
envFile?: false
|
|
||||||
ignoreConfigWarning?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UserConfigFn = (env: ConfigEnv) => UserConfigSchema | Promise<UserConfigSchema>
|
|
||||||
export type UserConfigExport = UserConfigSchema | Promise<UserConfigSchema> | UserConfigFn
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type helper to make it easier to use `electron.vite.config.*`
|
* Type helper to make it easier to use `electron.vite.config.*`
|
||||||
@ -79,10 +126,21 @@ export type UserConfigExport = UserConfigSchema | Promise<UserConfigSchema> | Us
|
|||||||
* The function receives a object that exposes two properties:
|
* The function receives a object that exposes two properties:
|
||||||
* `command` (either `'build'` or `'serve'`), and `mode`.
|
* `command` (either `'build'` or `'serve'`), and `mode`.
|
||||||
*/
|
*/
|
||||||
export function defineConfig(config: UserConfigExport): UserConfigExport {
|
export function defineConfig(config: UserConfig): UserConfig
|
||||||
|
export function defineConfig(config: Promise<UserConfig>): Promise<UserConfig>
|
||||||
|
export function defineConfig(config: ElectronViteConfigFnObject): ElectronViteConfigFnObject
|
||||||
|
export function defineConfig(config: ElectronViteConfigFnPromise): ElectronViteConfigFnPromise
|
||||||
|
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport
|
||||||
|
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InlineConfig = Omit<ViteConfig, 'base'> & {
|
||||||
|
configFile?: string | false
|
||||||
|
envFile?: false
|
||||||
|
ignoreConfigWarning?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface ResolvedConfig {
|
export interface ResolvedConfig {
|
||||||
config?: UserConfig
|
config?: UserConfig
|
||||||
configFile?: string
|
configFile?: string
|
||||||
@ -97,11 +155,10 @@ export async function resolveConfig(
|
|||||||
const config = inlineConfig
|
const config = inlineConfig
|
||||||
const mode = inlineConfig.mode || defaultMode
|
const mode = inlineConfig.mode || defaultMode
|
||||||
|
|
||||||
config.mode = mode
|
|
||||||
|
|
||||||
process.env.NODE_ENV = defaultMode
|
process.env.NODE_ENV = defaultMode
|
||||||
|
|
||||||
let userConfig: UserConfig | undefined
|
const userConfig: UserConfig | undefined = {}
|
||||||
|
|
||||||
let configFileDependencies: string[] = []
|
let configFileDependencies: string[] = []
|
||||||
|
|
||||||
let { configFile } = config
|
let { configFile } = config
|
||||||
@ -110,6 +167,7 @@ export async function resolveConfig(
|
|||||||
mode,
|
mode,
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadResult = await loadConfigFromFile(
|
const loadResult = await loadConfigFromFile(
|
||||||
configEnv,
|
configEnv,
|
||||||
configFile,
|
configFile,
|
||||||
@ -117,52 +175,30 @@ export async function resolveConfig(
|
|||||||
config.logLevel,
|
config.logLevel,
|
||||||
config.ignoreConfigWarning
|
config.ignoreConfigWarning
|
||||||
)
|
)
|
||||||
|
|
||||||
if (loadResult) {
|
if (loadResult) {
|
||||||
const root = config.root
|
const root = config.root
|
||||||
delete config.root
|
delete config.root
|
||||||
delete config.configFile
|
delete config.configFile
|
||||||
|
|
||||||
|
config.configFile = false
|
||||||
|
|
||||||
const outDir = config.build?.outDir
|
const outDir = config.build?.outDir
|
||||||
|
|
||||||
if (loadResult.config.main) {
|
const { main, preload, renderer } = loadResult.config
|
||||||
const mainViteConfig: ViteConfig = mergeConfig(loadResult.config.main, deepClone(config))
|
|
||||||
|
|
||||||
if (outDir) {
|
if (main) {
|
||||||
resetOutDir(mainViteConfig, outDir, 'main')
|
userConfig.main = await new MainConfigFactory(main, config, { outDir, root }).build()
|
||||||
}
|
|
||||||
|
|
||||||
mergePlugins(mainViteConfig, [...electronMainVitePlugin({ root }), assetPlugin(), workerPlugin()])
|
|
||||||
|
|
||||||
loadResult.config.main = mainViteConfig
|
|
||||||
loadResult.config.main.configFile = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadResult.config.preload) {
|
if (preload) {
|
||||||
const preloadViteConfig: ViteConfig = mergeConfig(loadResult.config.preload, deepClone(config))
|
userConfig.preload = await new PreloadConfigFactory(preload, config, { outDir, root }).build()
|
||||||
|
|
||||||
if (outDir) {
|
|
||||||
resetOutDir(preloadViteConfig, outDir, 'preload')
|
|
||||||
}
|
|
||||||
mergePlugins(preloadViteConfig, [...electronPreloadVitePlugin({ root }), assetPlugin()])
|
|
||||||
|
|
||||||
loadResult.config.preload = preloadViteConfig
|
|
||||||
loadResult.config.preload.configFile = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadResult.config.renderer) {
|
if (renderer) {
|
||||||
const rendererViteConfig: ViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config))
|
userConfig.renderer = await new RendererConfigFactory(renderer, config, { outDir, root }).build()
|
||||||
|
|
||||||
if (outDir) {
|
|
||||||
resetOutDir(rendererViteConfig, outDir, 'renderer')
|
|
||||||
}
|
|
||||||
|
|
||||||
mergePlugins(rendererViteConfig, electronRendererVitePlugin({ root }))
|
|
||||||
|
|
||||||
loadResult.config.renderer = rendererViteConfig
|
|
||||||
loadResult.config.renderer.configFile = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userConfig = loadResult.config
|
|
||||||
configFile = loadResult.path
|
configFile = loadResult.path
|
||||||
configFileDependencies = loadResult.dependencies
|
configFileDependencies = loadResult.dependencies
|
||||||
}
|
}
|
||||||
@ -177,8 +213,109 @@ export async function resolveConfig(
|
|||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
function deepClone<T>(data: T): T {
|
export abstract class ConfigFactory<T extends MainViteConfig | PreloadViteConfig | RendererViteConfig> {
|
||||||
return JSON.parse(JSON.stringify(data))
|
constructor(
|
||||||
|
protected readonly baseConfig: T,
|
||||||
|
protected readonly inlineConfig: InlineConfig,
|
||||||
|
protected readonly options: { outDir?: string; root?: string }
|
||||||
|
) {
|
||||||
|
baseConfig.build ??= {}
|
||||||
|
baseConfig.build.rolldownOptions ??= baseConfig.build.rollupOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
async build(cleanMode?: boolean): Promise<T> {
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
|
const config = mergeConfig(cloneConfig(this.baseConfig) as any, cloneConfig(this.inlineConfig)) as T
|
||||||
|
|
||||||
|
config.mode = this.inlineConfig.mode || config.mode || process.env.NODE_ENV
|
||||||
|
|
||||||
|
if (this.options.outDir) {
|
||||||
|
resetOutDir(config, this.options.outDir, this.processType())
|
||||||
|
}
|
||||||
|
|
||||||
|
const builtinPlugins = await this.resolveBuiltinPlugins(config, cleanMode)
|
||||||
|
|
||||||
|
config.plugins = builtinPlugins.concat(config.plugins || [])
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract processType(): 'main' | 'preload' | 'renderer'
|
||||||
|
|
||||||
|
protected abstract resolveBuiltinPlugins(config: T, cleanMode?: boolean): Promise<PluginOption[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MainConfigFactory extends ConfigFactory<MainViteConfig> {
|
||||||
|
protected processType(): 'main' {
|
||||||
|
return 'main'
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async resolveBuiltinPlugins(config: MainViteConfig, cleanMode?: boolean): Promise<PluginOption[]> {
|
||||||
|
const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(config)
|
||||||
|
|
||||||
|
return cleanMode
|
||||||
|
? [
|
||||||
|
electronMainConfigPresetPlugin({ root: this.options.root }),
|
||||||
|
assetPlugin(),
|
||||||
|
importMetaPlugin(),
|
||||||
|
esmShimPlugin(),
|
||||||
|
...configDrivenPlugins
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
electronMainConfigPresetPlugin({ root: this.options.root }),
|
||||||
|
electronMainConfigValidatorPlugin(),
|
||||||
|
assetPlugin(),
|
||||||
|
workerPlugin(),
|
||||||
|
modulePathPlugin(this),
|
||||||
|
importMetaPlugin(),
|
||||||
|
esmShimPlugin(),
|
||||||
|
...configDrivenPlugins
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PreloadConfigFactory extends ConfigFactory<PreloadViteConfig> {
|
||||||
|
protected processType(): 'preload' {
|
||||||
|
return 'preload'
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async resolveBuiltinPlugins(config: PreloadViteConfig, cleanMode?: boolean): Promise<PluginOption[]> {
|
||||||
|
const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(config)
|
||||||
|
|
||||||
|
return cleanMode
|
||||||
|
? [
|
||||||
|
electronPreloadConfigPresetPlugin({ root: this.options.root }),
|
||||||
|
assetPlugin(),
|
||||||
|
importMetaPlugin(),
|
||||||
|
esmShimPlugin(),
|
||||||
|
...configDrivenPlugins
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
electronPreloadConfigPresetPlugin({ root: this.options.root }),
|
||||||
|
electronPreloadConfigValidatorPlugin(),
|
||||||
|
assetPlugin(),
|
||||||
|
importMetaPlugin(),
|
||||||
|
esmShimPlugin(),
|
||||||
|
...configDrivenPlugins,
|
||||||
|
...(config.build?.isolatedEntries ? [isolateEntriesPlugin(this)] : [])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RendererConfigFactory extends ConfigFactory<RendererViteConfig> {
|
||||||
|
protected processType(): 'renderer' {
|
||||||
|
return 'renderer'
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async resolveBuiltinPlugins(config: RendererViteConfig, cleanMode?: boolean): Promise<PluginOption[]> {
|
||||||
|
return cleanMode
|
||||||
|
? [electronRendererConfigPresetPlugin({ root: this.options.root })]
|
||||||
|
: [
|
||||||
|
electronRendererConfigPresetPlugin({ root: this.options.root }),
|
||||||
|
electronRendererConfigValidatorPlugin(),
|
||||||
|
...(config.build?.isolatedEntries ? [isolateEntriesPlugin(this)] : [])
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void {
|
function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void {
|
||||||
@ -193,12 +330,37 @@ function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): voi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergePlugins(config: ViteConfig, plugins: Plugin[]): void {
|
async function resolveConfigDrivenPlugins(config: MainViteConfig | PreloadViteConfig): Promise<PluginOption[]> {
|
||||||
const userPlugins = config.plugins || []
|
const userPlugins = (await asyncFlatten(config.plugins || [])).filter(Boolean) as Plugin[]
|
||||||
config.plugins = userPlugins.concat(plugins)
|
|
||||||
|
const configDrivenPlugins: PluginOption[] = []
|
||||||
|
|
||||||
|
const hasExternalizeDepsPlugin = userPlugins.some(p => p.name === 'vite:externalize-deps')
|
||||||
|
if (!hasExternalizeDepsPlugin) {
|
||||||
|
const externalOptions = config.build?.externalizeDeps ?? true
|
||||||
|
if (externalOptions) {
|
||||||
|
isOptions<ExternalOptions>(externalOptions)
|
||||||
|
? configDrivenPlugins.push(externalizeDepsPlugin(externalOptions))
|
||||||
|
: configDrivenPlugins.push(externalizeDepsPlugin())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasBytecodePlugin = userPlugins.some(p => p.name === 'vite:bytecode')
|
||||||
|
if (!hasBytecodePlugin) {
|
||||||
|
const bytecodeOptions = config.build?.bytecode
|
||||||
|
if (bytecodeOptions) {
|
||||||
|
isOptions<BytecodeOptions>(bytecodeOptions)
|
||||||
|
? configDrivenPlugins.push(bytecodePlugin(bytecodeOptions))
|
||||||
|
: configDrivenPlugins.push(bytecodePlugin())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configDrivenPlugins
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_FILE_NAME = 'electron.vite.config'
|
function isOptions<T extends object>(value: boolean | T): value is T {
|
||||||
|
return typeof value === 'object' && value !== null
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadConfigFromFile(
|
export async function loadConfigFromFile(
|
||||||
configEnv: ConfigEnv,
|
configEnv: ConfigEnv,
|
||||||
@ -215,9 +377,7 @@ export async function loadConfigFromFile(
|
|||||||
throw new Error(`config file cannot be named ${configFile}.`)
|
throw new Error(`config file cannot be named ${configFile}.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = configFile
|
const resolvedPath = configFile ? path.resolve(configFile) : findConfigFile(configRoot)
|
||||||
? path.resolve(configFile)
|
|
||||||
: findConfigFile(configRoot, ['js', 'ts', 'mjs', 'cjs', 'mts', 'cts'])
|
|
||||||
|
|
||||||
if (!resolvedPath) {
|
if (!resolvedPath) {
|
||||||
return {
|
return {
|
||||||
@ -227,186 +387,33 @@ export async function loadConfigFromFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// electron does not support adding type: "module" to package.json
|
const isESM = isFilePathESM(resolvedPath)
|
||||||
let isESM = false
|
|
||||||
if (/\.m[jt]s$/.test(resolvedPath) || resolvedPath.endsWith('.ts')) {
|
|
||||||
isESM = true
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bundled = await bundleConfigFile(resolvedPath, isESM)
|
const { code, dependencies } = await bundleConfigFile(resolvedPath, isESM)
|
||||||
const userConfig = await loadConfigFormBundledFile(configRoot, resolvedPath, bundled.code, isESM)
|
const configExport = await loadConfigFormBundledFile<ElectronViteConfigExport>(resolvedPath, code, isESM)
|
||||||
|
|
||||||
const config = await (typeof userConfig === 'function' ? userConfig(configEnv) : userConfig)
|
const config = await (typeof configExport === 'function' ? configExport(configEnv) : configExport)
|
||||||
if (!isObject(config)) {
|
if (!isObject(config)) {
|
||||||
throw new Error(`config must export or return an object`)
|
throw new Error(`config must export or return an object`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const configRequired: string[] = []
|
if (!ignoreConfigWarning) {
|
||||||
|
const missingFields = ['main', 'renderer', 'preload'].filter(field => !config[field])
|
||||||
let mainConfig
|
if (missingFields.length > 0) {
|
||||||
if (config.main) {
|
createLogger(logLevel).warn(
|
||||||
const mainViteConfig = config.main
|
`${colors.yellow(colors.bold('(!)'))} ${colors.yellow(`${missingFields.join(' and ')} config is missing`)}\n`
|
||||||
mainConfig = await (typeof mainViteConfig === 'function' ? mainViteConfig(configEnv) : mainViteConfig)
|
)
|
||||||
if (!isObject(mainConfig)) {
|
|
||||||
throw new Error(`main config must export or return an object`)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
configRequired.push('main')
|
|
||||||
}
|
|
||||||
|
|
||||||
let rendererConfig
|
|
||||||
if (config.renderer) {
|
|
||||||
const rendererViteConfig = config.renderer
|
|
||||||
rendererConfig = await (typeof rendererViteConfig === 'function'
|
|
||||||
? rendererViteConfig(configEnv)
|
|
||||||
: rendererViteConfig)
|
|
||||||
if (!isObject(rendererConfig)) {
|
|
||||||
throw new Error(`renderer config must export or return an object`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
configRequired.push('renderer')
|
|
||||||
}
|
|
||||||
|
|
||||||
let preloadConfig
|
|
||||||
if (config.preload) {
|
|
||||||
const preloadViteConfig = config.preload
|
|
||||||
preloadConfig = await (typeof preloadViteConfig === 'function' ? preloadViteConfig(configEnv) : preloadViteConfig)
|
|
||||||
if (!isObject(preloadConfig)) {
|
|
||||||
throw new Error(`preload config must export or return an object`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
configRequired.push('preload')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ignoreConfigWarning && configRequired.length > 0) {
|
|
||||||
createLogger(logLevel).warn(colors.yellow(`${configRequired.join(' and ')} config is missing`))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: normalizePath(resolvedPath),
|
path: normalizePath(resolvedPath),
|
||||||
config: {
|
config,
|
||||||
main: mainConfig,
|
dependencies
|
||||||
renderer: rendererConfig,
|
|
||||||
preload: preloadConfig
|
|
||||||
},
|
|
||||||
dependencies: bundled.dependencies
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
createLogger(logLevel).error(colors.red(`failed to load config from ${resolvedPath}`), { error: e as Error })
|
createLogger(logLevel).error(colors.red(`failed to load config from ${resolvedPath}`), { error: e as Error })
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findConfigFile(configRoot: string, extensions: string[]): string {
|
|
||||||
for (const ext of extensions) {
|
|
||||||
const configFile = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${ext}`)
|
|
||||||
if (fs.existsSync(configFile)) {
|
|
||||||
return configFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async function bundleConfigFile(fileName: string, isESM: boolean): Promise<{ code: string; dependencies: string[] }> {
|
|
||||||
const dirnameVarName = '__electron_vite_injected_dirname'
|
|
||||||
const filenameVarName = '__electron_vite_injected_filename'
|
|
||||||
const importMetaUrlVarName = '__electron_vite_injected_import_meta_url'
|
|
||||||
const result = await build({
|
|
||||||
absWorkingDir: process.cwd(),
|
|
||||||
entryPoints: [fileName],
|
|
||||||
write: false,
|
|
||||||
target: ['node14.18', 'node16'],
|
|
||||||
platform: 'node',
|
|
||||||
bundle: true,
|
|
||||||
format: isESM ? 'esm' : 'cjs',
|
|
||||||
sourcemap: false,
|
|
||||||
metafile: true,
|
|
||||||
define: {
|
|
||||||
__dirname: dirnameVarName,
|
|
||||||
__filename: filenameVarName,
|
|
||||||
'import.meta.url': importMetaUrlVarName
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
name: 'externalize-deps',
|
|
||||||
setup(build): void {
|
|
||||||
build.onResolve({ filter: /.*/ }, args => {
|
|
||||||
const id = args.path
|
|
||||||
if (id[0] !== '.' && !path.isAbsolute(id)) {
|
|
||||||
return {
|
|
||||||
external: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'replace-import-meta',
|
|
||||||
setup(build): void {
|
|
||||||
build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async args => {
|
|
||||||
const contents = await fs.promises.readFile(args.path, 'utf8')
|
|
||||||
const injectValues =
|
|
||||||
`const ${dirnameVarName} = ${JSON.stringify(path.dirname(args.path))};` +
|
|
||||||
`const ${filenameVarName} = ${JSON.stringify(args.path)};` +
|
|
||||||
`const ${importMetaUrlVarName} = ${JSON.stringify(pathToFileURL(args.path).href)};`
|
|
||||||
|
|
||||||
return {
|
|
||||||
loader: args.path.endsWith('ts') ? 'ts' : 'js',
|
|
||||||
contents: injectValues + contents
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
const { text } = result.outputFiles[0]
|
|
||||||
return {
|
|
||||||
code: text,
|
|
||||||
dependencies: result.metafile ? Object.keys(result.metafile.inputs) : []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NodeModuleWithCompile extends NodeModule {
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
_compile(code: string, filename: string): any
|
|
||||||
}
|
|
||||||
|
|
||||||
const _require = createRequire(import.meta.url)
|
|
||||||
async function loadConfigFormBundledFile(
|
|
||||||
configRoot: string,
|
|
||||||
configFile: string,
|
|
||||||
bundledCode: string,
|
|
||||||
isESM: boolean
|
|
||||||
): Promise<UserConfigExport> {
|
|
||||||
if (isESM) {
|
|
||||||
const fileNameTmp = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${Date.now()}.mjs`)
|
|
||||||
fs.writeFileSync(fileNameTmp, bundledCode)
|
|
||||||
|
|
||||||
const fileUrl = pathToFileURL(fileNameTmp)
|
|
||||||
try {
|
|
||||||
return (await dynamicImport(fileUrl)).default
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(fileNameTmp)
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const extension = path.extname(configFile)
|
|
||||||
const realFileName = fs.realpathSync(configFile)
|
|
||||||
const loaderExt = extension in _require.extensions ? extension : '.js'
|
|
||||||
const defaultLoader = _require.extensions[loaderExt]!
|
|
||||||
_require.extensions[loaderExt] = (module: NodeModule, filename: string): void => {
|
|
||||||
if (filename === realFileName) {
|
|
||||||
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
|
|
||||||
} else {
|
|
||||||
defaultLoader(module, filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete _require.cache[_require.resolve(configFile)]
|
|
||||||
const raw = _require(configFile)
|
|
||||||
_require.extensions[loaderExt] = defaultLoader
|
|
||||||
return raw.__esModule ? raw.default : raw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
152
src/electron.ts
152
src/electron.ts
@ -1,19 +1,19 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
import { type ChildProcessWithoutNullStreams, spawn } from 'node:child_process'
|
import { type ChildProcess, spawn } from 'node:child_process'
|
||||||
import { type Logger } from 'vite'
|
import { loadPackageData } from './utils'
|
||||||
|
|
||||||
const _require = createRequire(import.meta.url)
|
const _require = createRequire(import.meta.url)
|
||||||
|
|
||||||
const ensureElectronEntryFile = (root = process.cwd()): void => {
|
const ensureElectronEntryFile = (root = process.cwd()): void => {
|
||||||
const pkg = path.join(root, 'package.json')
|
if (process.env.ELECTRON_ENTRY) return
|
||||||
if (fs.existsSync(pkg)) {
|
const pkg = loadPackageData()
|
||||||
const main = require(pkg).main
|
if (pkg) {
|
||||||
if (!main) {
|
if (!pkg.main) {
|
||||||
throw new Error('No entry point found for electron app, please add a "main" field to package.json')
|
throw new Error('No entry point found for electron app, please add a "main" field to package.json')
|
||||||
} else {
|
} else {
|
||||||
const entryPath = path.resolve(root, main)
|
const entryPath = path.resolve(root, pkg.main)
|
||||||
if (!fs.existsSync(entryPath)) {
|
if (!fs.existsSync(entryPath)) {
|
||||||
throw new Error(`No electron app entry file found: ${entryPath}`)
|
throw new Error(`No electron app entry file found: ${entryPath}`)
|
||||||
}
|
}
|
||||||
@ -36,6 +36,16 @@ const getElectronMajorVer = (): string => {
|
|||||||
return majorVer
|
return majorVer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function supportESM(): boolean {
|
||||||
|
const majorVer = getElectronMajorVer()
|
||||||
|
return parseInt(majorVer) >= 28
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supportImportMetaPaths(): boolean {
|
||||||
|
const majorVer = getElectronMajorVer()
|
||||||
|
return parseInt(majorVer) >= 30
|
||||||
|
}
|
||||||
|
|
||||||
export function getElectronPath(): string {
|
export function getElectronPath(): string {
|
||||||
let electronExecPath = process.env.ELECTRON_EXEC_PATH || ''
|
let electronExecPath = process.env.ELECTRON_EXEC_PATH || ''
|
||||||
if (!electronExecPath) {
|
if (!electronExecPath) {
|
||||||
@ -59,20 +69,29 @@ export function getElectronNodeTarget(): string {
|
|||||||
const electronVer = getElectronMajorVer()
|
const electronVer = getElectronMajorVer()
|
||||||
|
|
||||||
const nodeVer = {
|
const nodeVer = {
|
||||||
|
'44': '24.18',
|
||||||
|
'43': '24.17',
|
||||||
|
'42': '24.15',
|
||||||
|
'41': '24.14',
|
||||||
|
'40': '24.14',
|
||||||
|
'39': '22.20',
|
||||||
|
'38': '22.19',
|
||||||
|
'37': '22.16',
|
||||||
|
'36': '22.14',
|
||||||
|
'35': '22.14',
|
||||||
|
'34': '20.18',
|
||||||
|
'33': '20.18',
|
||||||
|
'32': '20.16',
|
||||||
|
'31': '20.14',
|
||||||
|
'30': '20.11',
|
||||||
|
'29': '20.9',
|
||||||
|
'28': '18.18',
|
||||||
|
'27': '18.17',
|
||||||
|
'26': '18.16',
|
||||||
|
'25': '18.15',
|
||||||
'24': '18.14',
|
'24': '18.14',
|
||||||
'23': '18.12',
|
'23': '18.12',
|
||||||
'22': '16.17',
|
'22': '16.17'
|
||||||
'21': '16.16',
|
|
||||||
'20': '16.15',
|
|
||||||
'19': '16.14',
|
|
||||||
'18': '16.13',
|
|
||||||
'17': '16.13',
|
|
||||||
'16': '16.9',
|
|
||||||
'15': '16.5',
|
|
||||||
'14': '14.17',
|
|
||||||
'13': '14.17',
|
|
||||||
'12': '14.16',
|
|
||||||
'11': '12.18'
|
|
||||||
}
|
}
|
||||||
if (electronVer && parseInt(electronVer) > 10) {
|
if (electronVer && parseInt(electronVer) > 10) {
|
||||||
let target = nodeVer[electronVer]
|
let target = nodeVer[electronVer]
|
||||||
@ -86,20 +105,29 @@ export function getElectronChromeTarget(): string {
|
|||||||
const electronVer = getElectronMajorVer()
|
const electronVer = getElectronMajorVer()
|
||||||
|
|
||||||
const chromeVer = {
|
const chromeVer = {
|
||||||
|
'44': '152',
|
||||||
|
'43': '150',
|
||||||
|
'42': '148',
|
||||||
|
'41': '146',
|
||||||
|
'40': '144',
|
||||||
|
'39': '142',
|
||||||
|
'38': '140',
|
||||||
|
'37': '138',
|
||||||
|
'36': '136',
|
||||||
|
'35': '134',
|
||||||
|
'34': '132',
|
||||||
|
'33': '130',
|
||||||
|
'32': '128',
|
||||||
|
'31': '126',
|
||||||
|
'30': '124',
|
||||||
|
'29': '122',
|
||||||
|
'28': '120',
|
||||||
|
'27': '118',
|
||||||
|
'26': '116',
|
||||||
|
'25': '114',
|
||||||
'24': '112',
|
'24': '112',
|
||||||
'23': '110',
|
'23': '110',
|
||||||
'22': '108',
|
'22': '108'
|
||||||
'21': '106',
|
|
||||||
'20': '104',
|
|
||||||
'19': '102',
|
|
||||||
'18': '100',
|
|
||||||
'17': '98',
|
|
||||||
'16': '96',
|
|
||||||
'15': '94',
|
|
||||||
'14': '93',
|
|
||||||
'13': '91',
|
|
||||||
'12': '89',
|
|
||||||
'11': '87'
|
|
||||||
}
|
}
|
||||||
if (electronVer && parseInt(electronVer) > 10) {
|
if (electronVer && parseInt(electronVer) > 10) {
|
||||||
let target = chromeVer[electronVer]
|
let target = chromeVer[electronVer]
|
||||||
@ -109,26 +137,64 @@ export function getElectronChromeTarget(): string {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startElectron(root: string | undefined, logger: Logger): ChildProcessWithoutNullStreams {
|
export function startElectron(root: string | undefined): ChildProcess {
|
||||||
ensureElectronEntryFile(root)
|
ensureElectronEntryFile(root)
|
||||||
|
|
||||||
const electronPath = getElectronPath()
|
const electronPath = getElectronPath()
|
||||||
|
|
||||||
const inspect = !!process.env.VSCODE_INSPECTOR_OPTIONS
|
const isDev = process.env.NODE_ENV_ELECTRON_VITE === 'development'
|
||||||
|
|
||||||
const args: string[] = []
|
const args: string[] = process.env.ELECTRON_CLI_ARGS ? JSON.parse(process.env.ELECTRON_CLI_ARGS) : []
|
||||||
if (!!process.env.REMOTE_DEBUGGING_PORT && process.env.NODE_ENV_ELECTRON_VITE === 'development') {
|
|
||||||
|
if (!!process.env.REMOTE_DEBUGGING_PORT && isDev) {
|
||||||
args.push(`--remote-debugging-port=${process.env.REMOTE_DEBUGGING_PORT}`)
|
args.push(`--remote-debugging-port=${process.env.REMOTE_DEBUGGING_PORT}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ps = spawn(electronPath, ['.'].concat(args))
|
if (!!process.env.V8_INSPECTOR_PORT && isDev) {
|
||||||
ps.stdout.on('data', chunk => {
|
args.push(`--inspect=${process.env.V8_INSPECTOR_PORT}`)
|
||||||
!inspect && chunk.toString().trim() && logger.info(chunk.toString())
|
}
|
||||||
})
|
|
||||||
ps.stderr.on('data', chunk => {
|
if (!!process.env.V8_INSPECTOR_BRK_PORT && isDev) {
|
||||||
!inspect && chunk.toString().trim() && logger.error(chunk.toString())
|
args.push(`--inspect-brk=${process.env.V8_INSPECTOR_BRK_PORT}`)
|
||||||
})
|
}
|
||||||
ps.on('close', process.exit)
|
|
||||||
|
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' })
|
||||||
|
// `code` is null when the child was terminated by a signal, and `process.exit` ignores
|
||||||
|
// its second argument, so passing the listener directly would report every crash as a
|
||||||
|
// successful exit. Map a signal death to a non-zero status instead.
|
||||||
|
ps.on('close', (code, signal) => process.exit(code ?? (signal ? 1 : 0)))
|
||||||
|
|
||||||
|
setupElectronSignalHandlers(ps)
|
||||||
|
|
||||||
return ps
|
return ps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let electronPs: ChildProcess | undefined
|
||||||
|
let signalHandlersRegistered = false
|
||||||
|
|
||||||
|
function setupElectronSignalHandlers(child: ChildProcess): void {
|
||||||
|
electronPs = child
|
||||||
|
|
||||||
|
if (signalHandlersRegistered) return
|
||||||
|
|
||||||
|
const signals: NodeJS.Signals[] =
|
||||||
|
process.platform === 'win32' ? ['SIGINT', 'SIGTERM'] : ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGUSR2']
|
||||||
|
|
||||||
|
for (const signal of signals) {
|
||||||
|
process.on(signal, () => {
|
||||||
|
const child = electronPs
|
||||||
|
if (!child?.pid || child.exitCode !== null || child.signalCode !== null) return
|
||||||
|
|
||||||
|
// A previously sent signal does not mean the child has exited.
|
||||||
|
child.kill(signal)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
signalHandlersRegistered = true
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export { type LogLevel, createLogger, splitVendorChunkPlugin, splitVendorChunk } from 'vite'
|
export { type LogLevel, createLogger, mergeConfig } from 'vite'
|
||||||
export * from './config'
|
export * from './config'
|
||||||
export { createServer } from './server'
|
export { createServer } from './server'
|
||||||
export { build } from './build'
|
export { build } from './build'
|
||||||
|
|||||||
130
src/load.ts
Normal file
130
src/load.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import { pathToFileURL } from 'node:url'
|
||||||
|
import { createRequire } from 'node:module'
|
||||||
|
import { rolldown } from 'rolldown'
|
||||||
|
import type { RolldownOutput, OutputChunk } from 'rolldown'
|
||||||
|
|
||||||
|
const CONFIG_FILE_NAME = 'electron.vite.config'
|
||||||
|
|
||||||
|
export function findConfigFile(
|
||||||
|
configRoot: string,
|
||||||
|
extensions: string[] = ['js', 'ts', 'mjs', 'cjs', 'mts', 'cts']
|
||||||
|
): string {
|
||||||
|
for (const ext of extensions) {
|
||||||
|
const configFile = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${ext}`)
|
||||||
|
if (fs.existsSync(configFile)) {
|
||||||
|
return configFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bundleConfigFile(
|
||||||
|
fileName: string,
|
||||||
|
isESM: boolean
|
||||||
|
): Promise<{ code: string; dependencies: string[] }> {
|
||||||
|
const dirnameVarName = '__electron_vite_injected_dirname'
|
||||||
|
const filenameVarName = '__electron_vite_injected_filename'
|
||||||
|
const importMetaUrlVarName = '__electron_vite_injected_import_meta_url'
|
||||||
|
|
||||||
|
const bundle = await rolldown({
|
||||||
|
input: path.resolve(fileName),
|
||||||
|
platform: 'node',
|
||||||
|
transform: {
|
||||||
|
target: 'node20',
|
||||||
|
define: {
|
||||||
|
__dirname: dirnameVarName,
|
||||||
|
__filename: filenameVarName,
|
||||||
|
'import.meta.url': importMetaUrlVarName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'externalize-deps',
|
||||||
|
resolveId(id) {
|
||||||
|
if (id[0] !== '.' && !path.isAbsolute(id)) {
|
||||||
|
return { id, external: true }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'inject-file-scope-variables',
|
||||||
|
transform: {
|
||||||
|
filter: { id: /\.[cm]?[jt]s$/ },
|
||||||
|
handler(code, id) {
|
||||||
|
const injectValues =
|
||||||
|
`const ${dirnameVarName} = ${JSON.stringify(path.dirname(id))};` +
|
||||||
|
`const ${filenameVarName} = ${JSON.stringify(id)};` +
|
||||||
|
`const ${importMetaUrlVarName} = ${JSON.stringify(pathToFileURL(id).href)};`
|
||||||
|
return { code: injectValues + code, map: null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
let result: RolldownOutput
|
||||||
|
try {
|
||||||
|
result = await bundle.generate({
|
||||||
|
format: isESM ? 'esm' : 'cjs',
|
||||||
|
sourcemap: 'inline',
|
||||||
|
sourcemapPathTransform(relative, sourcemapPath) {
|
||||||
|
return path.resolve(path.dirname(sourcemapPath), relative)
|
||||||
|
},
|
||||||
|
codeSplitting: false
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
await bundle.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryChunk = result.output.find((chunk): chunk is OutputChunk => chunk.type === 'chunk' && chunk.isEntry)
|
||||||
|
if (!entryChunk) {
|
||||||
|
throw new Error(`Failed to bundle config file: ${fileName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: entryChunk.code,
|
||||||
|
dependencies: entryChunk.moduleIds.filter(id => !id.startsWith('\0'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeModuleWithCompile extends NodeModule {
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
|
_compile(code: string, filename: string): any
|
||||||
|
}
|
||||||
|
|
||||||
|
const _require = createRequire(import.meta.url)
|
||||||
|
|
||||||
|
export async function loadConfigFormBundledFile<T>(fileName: string, bundledCode: string, isESM: boolean): Promise<T> {
|
||||||
|
if (isESM) {
|
||||||
|
const fileNameTmp = `${fileName}.${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`
|
||||||
|
fs.writeFileSync(fileNameTmp, bundledCode)
|
||||||
|
|
||||||
|
const fileUrl = pathToFileURL(fileNameTmp)
|
||||||
|
try {
|
||||||
|
return (await import(fileUrl.href)).default
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(fileNameTmp)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const extension = path.extname(fileName)
|
||||||
|
const realFileName = fs.realpathSync(fileName)
|
||||||
|
const loaderExt = extension in _require.extensions ? extension : '.js'
|
||||||
|
const defaultLoader = _require.extensions[loaderExt]!
|
||||||
|
_require.extensions[loaderExt] = (module: NodeModule, filename: string): void => {
|
||||||
|
if (filename === realFileName) {
|
||||||
|
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
|
||||||
|
} else {
|
||||||
|
defaultLoader(module, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete _require.cache[_require.resolve(fileName)]
|
||||||
|
const raw = _require(fileName)
|
||||||
|
_require.extensions[loaderExt] = defaultLoader
|
||||||
|
return raw.__esModule ? raw.default : raw
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,50 +1,17 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import fs from 'node:fs/promises'
|
import fs from 'node:fs/promises'
|
||||||
import type { SourceMapInput } from 'rollup'
|
import { type Plugin, type Environment, normalizePath } from 'vite'
|
||||||
import { type Plugin, normalizePath } from 'vite'
|
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { cleanUrl, parseRequest, getHash, toRelativePath } from '../utils'
|
import { cleanUrl, getHash, toRelativePath } from '../utils'
|
||||||
|
import { supportImportMetaPaths } from '../electron'
|
||||||
|
|
||||||
interface AssetResolved {
|
const nodeAssetRE = /__VITE_NODE_ASSET__([\w$]+)__/g
|
||||||
type: 'asset' | 'native' | 'wasm'
|
|
||||||
file: string
|
|
||||||
query: Record<string, string> | null
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveAsset(id: string): AssetResolved | null {
|
|
||||||
const file = cleanUrl(id)
|
|
||||||
const query = parseRequest(id)
|
|
||||||
|
|
||||||
if (query && typeof query.asset === 'string') {
|
|
||||||
return {
|
|
||||||
type: 'asset',
|
|
||||||
file,
|
|
||||||
query
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.endsWith('.node')) {
|
|
||||||
return {
|
|
||||||
type: 'native',
|
|
||||||
file,
|
|
||||||
query
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.endsWith('.wasm?loader')) {
|
|
||||||
return {
|
|
||||||
type: 'wasm',
|
|
||||||
file,
|
|
||||||
query
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeAssetRE = /__VITE_NODE_ASSET__([a-z\d]{8})__/g
|
|
||||||
const nodePublicAssetRE = /__VITE_NODE_PUBLIC_ASSET__([a-z\d]{8})__/g
|
const nodePublicAssetRE = /__VITE_NODE_PUBLIC_ASSET__([a-z\d]{8})__/g
|
||||||
|
|
||||||
|
const assetImportRE = /(?:[?|&]asset(?:&|$)|\.wasm\?loader$|\.node$)/
|
||||||
|
const assetRE = /[?|&]asset(?:&|$)/
|
||||||
|
const assetUnpackRE = /[?|&]asset&asarUnpack$/
|
||||||
|
|
||||||
const wasmHelperId = '\0__electron-vite-wasm-helper'
|
const wasmHelperId = '\0__electron-vite-wasm-helper'
|
||||||
|
|
||||||
const wasmHelperCode = `
|
const wasmHelperCode = `
|
||||||
@ -58,20 +25,18 @@ export default async function loadWasm(file, importObject = {}) {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const assetCache = new WeakMap<Environment, Map<string, string>>()
|
||||||
|
const publicAssetPathCache = new WeakMap<Environment, Map<string, string>>()
|
||||||
|
|
||||||
export default function assetPlugin(): Plugin {
|
export default function assetPlugin(): Plugin {
|
||||||
let sourcemap: boolean | 'inline' | 'hidden' = false
|
const isImportMetaPathSupported = supportImportMetaPaths()
|
||||||
let publicDir = ''
|
|
||||||
let outDir = ''
|
|
||||||
const publicAssetPathCache = new Map<string, string>()
|
|
||||||
const assetCache = new Map<string, string>()
|
|
||||||
return {
|
return {
|
||||||
name: 'vite:node-asset',
|
name: 'vite:node-asset',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
configResolved(config): void {
|
buildStart(): void {
|
||||||
sourcemap = config.build.sourcemap
|
publicAssetPathCache.set(this.environment, new Map())
|
||||||
publicDir = normalizePath(config.publicDir)
|
assetCache.set(this.environment, new Map())
|
||||||
outDir = normalizePath(path.resolve(config.root, config.build.outDir))
|
|
||||||
},
|
},
|
||||||
resolveId(id): string | void {
|
resolveId(id): string | void {
|
||||||
if (id === wasmHelperId) {
|
if (id === wasmHelperId) {
|
||||||
@ -83,21 +48,26 @@ export default function assetPlugin(): Plugin {
|
|||||||
return wasmHelperCode
|
return wasmHelperCode
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetResolved = resolveAsset(id)
|
if (id.startsWith('\0') || !assetImportRE.test(id)) {
|
||||||
if (!assetResolved) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let referenceId: string
|
let referenceId: string
|
||||||
const file = assetResolved.file
|
|
||||||
|
const file = cleanUrl(id)
|
||||||
|
|
||||||
|
const { publicDir } = this.environment.config
|
||||||
|
|
||||||
if (publicDir && file.startsWith(publicDir)) {
|
if (publicDir && file.startsWith(publicDir)) {
|
||||||
const hash = getHash(file)
|
const hash = getHash(file)
|
||||||
if (!publicAssetPathCache.get(hash)) {
|
const publicAssetPathMap = publicAssetPathCache.get(this.environment)!
|
||||||
publicAssetPathCache.set(hash, file)
|
if (!publicAssetPathMap.get(hash)) {
|
||||||
|
publicAssetPathMap.set(hash, file)
|
||||||
}
|
}
|
||||||
referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__`
|
referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__`
|
||||||
} else {
|
} else {
|
||||||
const cached = assetCache.get(file)
|
const cache = assetCache.get(this.environment)!
|
||||||
|
const cached = cache.get(file)
|
||||||
if (cached) {
|
if (cached) {
|
||||||
referenceId = cached
|
referenceId = cached
|
||||||
} else {
|
} else {
|
||||||
@ -105,75 +75,74 @@ export default function assetPlugin(): Plugin {
|
|||||||
const hash = this.emitFile({
|
const hash = this.emitFile({
|
||||||
type: 'asset',
|
type: 'asset',
|
||||||
name: path.basename(file),
|
name: path.basename(file),
|
||||||
source
|
source: source as unknown as Uint8Array
|
||||||
})
|
})
|
||||||
referenceId = `__VITE_NODE_ASSET__${hash}__`
|
referenceId = `__VITE_NODE_ASSET__${hash}__`
|
||||||
assetCache.set(file, referenceId)
|
cache.set(file, referenceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetResolved.type === 'asset') {
|
if (assetRE.test(id)) {
|
||||||
if (assetResolved.query && typeof assetResolved.query.asarUnpack === 'string') {
|
const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname'
|
||||||
|
if (assetUnpackRE.test(id)) {
|
||||||
return `
|
return `
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
export default join(__dirname, ${referenceId}).replace('app.asar', 'app.asar.unpacked')`
|
export default join(${dirnameExpr}, ${referenceId}).replace('app.asar', 'app.asar.unpacked')`
|
||||||
} else {
|
} else {
|
||||||
return `
|
return `
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
export default join(__dirname, ${referenceId})`
|
export default join(${dirnameExpr}, ${referenceId})`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetResolved.type === 'native') {
|
if (id.endsWith('.node')) {
|
||||||
return `export default require(${referenceId})`
|
return `export default require(${referenceId})`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetResolved.type === 'wasm') {
|
if (id.endsWith('.wasm?loader')) {
|
||||||
return `
|
return `
|
||||||
import loadWasm from ${JSON.stringify(wasmHelperId)}
|
import loadWasm from ${JSON.stringify(wasmHelperId)}
|
||||||
export default importObject => loadWasm(${referenceId}, importObject)`
|
export default importObject => loadWasm(${referenceId}, importObject)`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null {
|
renderChunk(code, chunk, { sourcemap, dir }) {
|
||||||
let match: RegExpExecArray | null
|
let match: RegExpExecArray | null
|
||||||
let s: MagicString | undefined
|
let s: MagicString | undefined
|
||||||
|
|
||||||
nodeAssetRE.lastIndex = 0
|
nodeAssetRE.lastIndex = 0
|
||||||
if (code.match(nodeAssetRE)) {
|
while ((match = nodeAssetRE.exec(code))) {
|
||||||
while ((match = nodeAssetRE.exec(code))) {
|
s ||= new MagicString(code)
|
||||||
s ||= new MagicString(code)
|
const [full, hash] = match
|
||||||
const [full, hash] = match
|
const filename = this.getFileName(hash)
|
||||||
const filename = this.getFileName(hash)
|
const outputFilepath = toRelativePath(filename, chunk.fileName)
|
||||||
const outputFilepath = toRelativePath(filename, chunk.fileName)
|
const replacement = JSON.stringify(outputFilepath)
|
||||||
const replacement = JSON.stringify(outputFilepath)
|
s.overwrite(match.index, match.index + full.length, replacement, {
|
||||||
s.overwrite(match.index, match.index + full.length, replacement, {
|
contentOnly: true
|
||||||
contentOnly: true
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const publicAssetPathMap = publicAssetPathCache.get(this.environment)!
|
||||||
|
|
||||||
nodePublicAssetRE.lastIndex = 0
|
nodePublicAssetRE.lastIndex = 0
|
||||||
if (code.match(nodePublicAssetRE)) {
|
while ((match = nodePublicAssetRE.exec(code))) {
|
||||||
while ((match = nodePublicAssetRE.exec(code))) {
|
s ||= new MagicString(code)
|
||||||
s ||= new MagicString(code)
|
const [full, hash] = match
|
||||||
const [full, hash] = match
|
const filename = publicAssetPathMap.get(hash)!
|
||||||
const filename = publicAssetPathCache.get(hash)!
|
const outputFilepath = toRelativePath(filename, normalizePath(path.join(dir!, chunk.fileName)))
|
||||||
const outputFilepath = toRelativePath(filename, normalizePath(path.join(outDir, chunk.fileName)))
|
const replacement = JSON.stringify(outputFilepath)
|
||||||
const replacement = JSON.stringify(outputFilepath)
|
s.overwrite(match.index, match.index + full.length, replacement, {
|
||||||
s.overwrite(match.index, match.index + full.length, replacement, {
|
contentOnly: true
|
||||||
contentOnly: true
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s) {
|
if (s) {
|
||||||
return {
|
return {
|
||||||
code: s.toString(),
|
code: s.toString(),
|
||||||
map: sourcemap ? s.generateMap({ hires: true }) : null
|
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,155 +1,89 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import fs from 'node:fs'
|
import { randomBytes } from 'node:crypto'
|
||||||
import { spawn } from 'node:child_process'
|
|
||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
import colors from 'picocolors'
|
import colors from 'picocolors'
|
||||||
import { type Plugin, type ResolvedConfig, normalizePath, createFilter } from 'vite'
|
import { type Plugin, type Rolldown, normalizePath } from 'vite'
|
||||||
import * as babel from '@babel/core'
|
import type * as Babel from '@babel/core'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import type { SourceMapInput, OutputChunk } from 'rollup'
|
import { toRelativePath, resolveBuildOutputs } from '../utils'
|
||||||
import { getElectronPath } from '../electron'
|
import type { BytecodeCompiler, BytecodeTarget } from '../bytecodeCompiler'
|
||||||
import { toRelativePath } from '../utils'
|
|
||||||
|
|
||||||
// Inspired by https://github.com/bytenode/bytenode
|
// Inspired by https://github.com/bytenode/bytenode
|
||||||
|
|
||||||
const _require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
|
let babel: typeof Babel | undefined
|
||||||
|
|
||||||
function getBytecodeCompilerPath(): string {
|
function getBytecodeCompiler(plugins: readonly Plugin[]): BytecodeCompiler | undefined {
|
||||||
return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', 'electron-bytecode.js')
|
const plugin = plugins.find(plugin => plugin.name === 'vite:electron-bytecode-build')
|
||||||
|
return (plugin?.api as { compiler?: BytecodeCompiler } | undefined)?.compiler
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileToBytecode(code: string): Promise<Buffer> {
|
// Versioned envelope: magic/version (8), UTF-16 source length (4), module ID (16), V8 cache.
|
||||||
return new Promise((resolve, reject) => {
|
// Keep our metadata separate from the private V8 cache header.
|
||||||
let data = Buffer.from([])
|
async function compileToBytecode(compiler: BytecodeCompiler, target: BytecodeTarget, code: string): Promise<Buffer> {
|
||||||
|
const id = randomBytes(16)
|
||||||
const electronPath = getElectronPath()
|
// Reserve enough source space for an untruncated, per-artifact placeholder identity.
|
||||||
const bytecodePath = getBytecodeCompilerPath()
|
const source = code + '\n/*' + id.toString('hex') + '*/'
|
||||||
|
const result = await compiler.compile(target, source)
|
||||||
const proc = spawn(electronPath, [bytecodePath], {
|
const header = Buffer.alloc(28)
|
||||||
env: { ELECTRON_RUN_AS_NODE: '1' },
|
header.write('EVBC0001', 0, 'ascii')
|
||||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
|
header.writeUInt32LE(result.sourceLength, 8)
|
||||||
})
|
id.copy(header, 12)
|
||||||
|
return Buffer.concat([header, Buffer.from(result.cachedData, 'base64')])
|
||||||
if (proc.stdin) {
|
|
||||||
proc.stdin.write(code)
|
|
||||||
proc.stdin.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (proc.stdout) {
|
|
||||||
proc.stdout.on('data', chunk => {
|
|
||||||
data = Buffer.concat([data, chunk])
|
|
||||||
})
|
|
||||||
proc.stdout.on('error', err => {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
proc.stdout.on('end', () => {
|
|
||||||
resolve(data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (proc.stderr) {
|
|
||||||
proc.stderr.on('data', chunk => {
|
|
||||||
console.error('Error: ', chunk.toString())
|
|
||||||
})
|
|
||||||
proc.stderr.on('error', err => {
|
|
||||||
console.error('Error: ', err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
proc.addListener('message', message => console.log(message))
|
|
||||||
proc.addListener('error', err => console.error(err))
|
|
||||||
|
|
||||||
proc.on('error', err => reject(err))
|
|
||||||
proc.on('exit', () => {
|
|
||||||
resolve(data)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bytecodeModuleLoaderCode = [
|
const bytecodeModuleLoaderCode = [
|
||||||
`"use strict";`,
|
'"use strict";',
|
||||||
`const fs = require("fs");`,
|
'const fs = require("node:fs");',
|
||||||
`const path = require("path");`,
|
'const path = require("node:path");',
|
||||||
`const vm = require("vm");`,
|
'const vm = require("node:vm");',
|
||||||
`const v8 = require("v8");`,
|
'const v8 = require("node:v8");',
|
||||||
`const Module = require("module");`,
|
'const Module = require("node:module");',
|
||||||
`v8.setFlagsFromString("--no-lazy");`,
|
'v8.setFlagsFromString("--no-lazy");',
|
||||||
`v8.setFlagsFromString("--no-flush-bytecode");`,
|
'v8.setFlagsFromString("--no-flush-bytecode");',
|
||||||
`const FLAG_HASH_OFFSET = 12;`,
|
'const params = ["exports", "require", "module", "__filename", "__dirname"];',
|
||||||
`const SOURCE_HASH_OFFSET = 8;`,
|
'Module._extensions[".jsc"] = Module._extensions[".cjsc"] = function (module, filename) {',
|
||||||
`let dummyBytecode;`,
|
' const data = fs.readFileSync(filename);',
|
||||||
`function setFlagHashHeader(bytecodeBuffer) {`,
|
' if (data.length <= 28 || data.toString("ascii", 0, 8) !== "EVBC0001") {',
|
||||||
` if (!dummyBytecode) {`,
|
' throw new Error("Invalid electron-vite bytecode format; rebuild the application: " + filename);',
|
||||||
` const script = new vm.Script("", {`,
|
' }',
|
||||||
` produceCachedData: true`,
|
' const length = data.readUInt32LE(8);',
|
||||||
` });`,
|
' const tag = "/*" + data.subarray(12, 28).toString("hex") + "*/";',
|
||||||
` dummyBytecode = script.createCachedData();`,
|
' if (length < tag.length || length > 0x1fffffff) {',
|
||||||
` }`,
|
' throw new Error("Invalid bytecode source length: " + filename);',
|
||||||
` dummyBytecode.slice(FLAG_HASH_OFFSET, FLAG_HASH_OFFSET + 4).copy(bytecodeBuffer, FLAG_HASH_OFFSET);`,
|
' }',
|
||||||
`};`,
|
' const placeholder = tag + " ".repeat(length - tag.length);',
|
||||||
`function getSourceHashHeader(bytecodeBuffer) {`,
|
' const compiledWrapper = vm.compileFunction(placeholder, params, {',
|
||||||
` return bytecodeBuffer.slice(SOURCE_HASH_OFFSET, SOURCE_HASH_OFFSET + 4);`,
|
' filename,',
|
||||||
`};`,
|
' cachedData: data.subarray(28)',
|
||||||
`function buffer2Number(buffer) {`,
|
' });',
|
||||||
` let ret = 0;`,
|
' if (compiledWrapper.cachedDataRejected) {',
|
||||||
` ret |= buffer[3] << 24;`,
|
' throw new Error("Invalid or incompatible cached data (cachedDataRejected): " + filename);',
|
||||||
` ret |= buffer[2] << 16;`,
|
' }',
|
||||||
` ret |= buffer[1] << 8;`,
|
' const require = function (id) { return module.require(id); };',
|
||||||
` ret |= buffer[0];`,
|
' require.resolve = function (request, options) {',
|
||||||
` return ret;`,
|
' return Module._resolveFilename(request, module, false, options);',
|
||||||
`};`,
|
' };',
|
||||||
`Module._extensions[".jsc"] = function (module, filename) {`,
|
' if (process.mainModule) require.main = process.mainModule;',
|
||||||
` const bytecodeBuffer = fs.readFileSync(filename);`,
|
' require.extensions = Module._extensions;',
|
||||||
` if (!Buffer.isBuffer(bytecodeBuffer)) {`,
|
' require.cache = Module._cache;',
|
||||||
` throw new Error("BytecodeBuffer must be a buffer object.");`,
|
' return compiledWrapper.call(module.exports, module.exports, require, module, filename, path.dirname(filename));',
|
||||||
` }`,
|
'};'
|
||||||
` setFlagHashHeader(bytecodeBuffer);`,
|
|
||||||
` const length = buffer2Number(getSourceHashHeader(bytecodeBuffer));`,
|
|
||||||
` let dummyCode = "";`,
|
|
||||||
` if (length > 1) {`,
|
|
||||||
` dummyCode = "\\"" + "\\u200b".repeat(length - 2) + "\\"";`,
|
|
||||||
` }`,
|
|
||||||
` const script = new vm.Script(dummyCode, {`,
|
|
||||||
` filename: filename,`,
|
|
||||||
` lineOffset: 0,`,
|
|
||||||
` displayErrors: true,`,
|
|
||||||
` cachedData: bytecodeBuffer`,
|
|
||||||
` });`,
|
|
||||||
` if (script.cachedDataRejected) {`,
|
|
||||||
` throw new Error("Invalid or incompatible cached data (cachedDataRejected)");`,
|
|
||||||
` }`,
|
|
||||||
` const require = function (id) {`,
|
|
||||||
` return module.require(id);`,
|
|
||||||
` };`,
|
|
||||||
` require.resolve = function (request, options) {`,
|
|
||||||
` return Module._resolveFilename(request, module, false, options);`,
|
|
||||||
` };`,
|
|
||||||
` if (process.mainModule) {`,
|
|
||||||
` require.main = process.mainModule;`,
|
|
||||||
` }`,
|
|
||||||
` require.extensions = Module._extensions;`,
|
|
||||||
` require.cache = Module._cache;`,
|
|
||||||
` const compiledWrapper = script.runInThisContext({`,
|
|
||||||
` filename: filename,`,
|
|
||||||
` lineOffset: 0,`,
|
|
||||||
` columnOffset: 0,`,
|
|
||||||
` displayErrors: true`,
|
|
||||||
` });`,
|
|
||||||
` const dirname = path.dirname(filename);`,
|
|
||||||
` const args = [module.exports, require, module, filename, dirname, process, global];`,
|
|
||||||
` return compiledWrapper.apply(module.exports, args);`,
|
|
||||||
`};`
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const bytecodeChunkExtensionRE = /.(jsc|cjsc)$/
|
||||||
|
|
||||||
export interface BytecodeOptions {
|
export interface BytecodeOptions {
|
||||||
chunkAlias?: string | string[]
|
chunkAlias?: string | string[] | RegExp
|
||||||
transformArrowFunctions?: boolean
|
transformArrowFunctions?: boolean
|
||||||
removeBundleJS?: boolean
|
removeBundleJS?: boolean
|
||||||
protectedStrings?: string[]
|
protectedStrings?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile to v8 bytecode to protect source code.
|
* Compile source code to v8 bytecode.
|
||||||
|
*
|
||||||
|
* @deprecated use `build.bytecode` config option instead
|
||||||
*/
|
*/
|
||||||
export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
||||||
if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') {
|
if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') {
|
||||||
@ -157,91 +91,182 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options
|
const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options
|
||||||
const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
|
const _chunkAlias = chunkAlias instanceof RegExp ? chunkAlias : Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
|
||||||
|
|
||||||
const filter = createFilter(/\.(m?[jt]s|[jt]sx)$/)
|
|
||||||
|
|
||||||
const escapeRegExpString = (str: string): string => {
|
|
||||||
return str
|
|
||||||
.replace(/\\/g, '\\\\\\\\')
|
|
||||||
.replace(/[|{}()[\]^$+*?.]/g, '\\$&')
|
|
||||||
.replace(/-/g, '\\u002d')
|
|
||||||
}
|
|
||||||
|
|
||||||
const transformAllChunks = _chunkAlias.length === 0
|
|
||||||
const isBytecodeChunk = (chunkName: string): boolean => {
|
const isBytecodeChunk = (chunkName: string): boolean => {
|
||||||
return transformAllChunks || _chunkAlias.some(alias => alias === chunkName)
|
if (_chunkAlias instanceof RegExp) {
|
||||||
|
_chunkAlias.lastIndex = 0
|
||||||
|
return _chunkAlias.test(chunkName)
|
||||||
|
}
|
||||||
|
return _chunkAlias.length === 0 || _chunkAlias.some(alias => alias === chunkName)
|
||||||
}
|
}
|
||||||
|
|
||||||
const _transform = (code: string): string => {
|
const plugins: Babel.PluginItem[] = []
|
||||||
const re = babel.transform(code, {
|
|
||||||
plugins: ['@babel/plugin-transform-arrow-functions']
|
if (transformArrowFunctions) {
|
||||||
})
|
plugins.push('@babel/plugin-transform-arrow-functions')
|
||||||
return re.code || ''
|
}
|
||||||
|
|
||||||
|
if (protectedStrings.length > 0) {
|
||||||
|
plugins.push([protectStringsPlugin as Babel.PluginTarget, { protectedStrings: new Set(protectedStrings) }])
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldTransformBytecodeChunk = plugins.length !== 0
|
||||||
|
|
||||||
|
const _transform = (
|
||||||
|
code: string,
|
||||||
|
sourceMaps: boolean = false
|
||||||
|
): { code: string; map?: Rolldown.SourceMapInput } | null => {
|
||||||
|
babel ??= require('@babel/core') as typeof Babel
|
||||||
|
const re = babel.transformSync(code, { plugins, sourceMaps })
|
||||||
|
return re ? { code: re.code || '', map: re.map as Rolldown.SourceMapInput } : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStrict = '"use strict";'
|
const useStrict = '"use strict";'
|
||||||
const bytecodeModuleLoader = 'bytecode-loader.js'
|
const bytecodeModuleLoader = 'bytecode-loader.cjs'
|
||||||
|
|
||||||
let config: ResolvedConfig
|
let supported = false
|
||||||
let useInRenderer = false
|
let isPreload = false
|
||||||
let bytecodeRequired = false
|
let bytecodeCompiler: BytecodeCompiler | undefined
|
||||||
let bytecodeFiles: { name: string; size: number }[] = []
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'vite:bytecode',
|
name: 'vite:bytecode',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
configResolved(resolvedConfig): void {
|
configResolved(config): void {
|
||||||
config = resolvedConfig
|
bytecodeCompiler = getBytecodeCompiler(config.plugins)
|
||||||
useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config')
|
if (supported) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isPreload = config.plugins.some(p => p.name === 'vite:electron-preload-config-preset')
|
||||||
|
const useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-config-preset')
|
||||||
if (useInRenderer) {
|
if (useInRenderer) {
|
||||||
config.logger.warn(colors.yellow('bytecodePlugin is not support renderers'))
|
config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const build = config.build
|
||||||
|
const outputs = resolveBuildOutputs(build.rolldownOptions.output, build.lib)
|
||||||
|
const output = outputs[0]
|
||||||
|
if (output) {
|
||||||
|
if (output.format === 'es') {
|
||||||
|
config.logger.warn(
|
||||||
|
colors.yellow(
|
||||||
|
'bytecodePlugin does not support ES module, please remove "type": "module" ' +
|
||||||
|
'in package.json or set build.rollupOptions.output.format (or build.rolldownOptions.output.format) to "cjs".'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
supported = output.format === 'cjs' && !useInRenderer
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
transform(code, id): void | { code: string; map: SourceMapInput } {
|
renderChunk(code, chunk, { sourcemap }): { code: string; map?: Rolldown.SourceMapInput } | null {
|
||||||
if (protectedStrings.length === 0 || !filter(id)) return
|
if (supported && isBytecodeChunk(chunk.name) && shouldTransformBytecodeChunk) {
|
||||||
|
return _transform(code, !!sourcemap)
|
||||||
let match: RegExpExecArray | null
|
|
||||||
let s: MagicString | undefined
|
|
||||||
|
|
||||||
protectedStrings.forEach(str => {
|
|
||||||
const escapedStr = escapeRegExpString(str)
|
|
||||||
const re = new RegExp(`\\u0027${escapedStr}\\u0027|\\u0022${escapedStr}\\u0022`, 'g')
|
|
||||||
const charCodes = Array.from(str).map(s => s.charCodeAt(0))
|
|
||||||
const replacement = `String.fromCharCode(${charCodes.toString()})`
|
|
||||||
while ((match = re.exec(code))) {
|
|
||||||
s ||= new MagicString(code)
|
|
||||||
const [full] = match
|
|
||||||
s.overwrite(match.index, match.index + full.length, replacement, {
|
|
||||||
contentOnly: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (s) {
|
|
||||||
return {
|
|
||||||
code: s.toString(),
|
|
||||||
map: config.build.sourcemap ? s.generateMap({ hires: true }) : null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderChunk(code, chunk): { code: string } | null {
|
|
||||||
if (useInRenderer) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (chunk.type === 'chunk' && isBytecodeChunk(chunk.name)) {
|
|
||||||
bytecodeRequired = true
|
|
||||||
if (transformArrowFunctions) {
|
|
||||||
return {
|
|
||||||
code: _transform(code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
generateBundle(): void {
|
async generateBundle(_, output): Promise<void> {
|
||||||
if (!useInRenderer && bytecodeRequired) {
|
if (!supported) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const _chunks = Object.values(output)
|
||||||
|
const chunks = _chunks.filter(
|
||||||
|
chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name)
|
||||||
|
) as Rolldown.OutputChunk[]
|
||||||
|
|
||||||
|
if (chunks.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytecodeChunks = chunks.map(chunk => chunk.fileName)
|
||||||
|
const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName))
|
||||||
|
|
||||||
|
const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|')
|
||||||
|
const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null
|
||||||
|
|
||||||
|
const getBytecodeLoaderBlock = (chunkFileName: string): string => {
|
||||||
|
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytecodeChunkCount = 0
|
||||||
|
|
||||||
|
const bundles = Object.keys(output)
|
||||||
|
|
||||||
|
if (!bytecodeCompiler) {
|
||||||
|
throw new Error('Bytecode compilation requires an electron-vite build')
|
||||||
|
}
|
||||||
|
for (const name of bundles) {
|
||||||
|
const chunk = output[name]
|
||||||
|
if (chunk.type === 'chunk') {
|
||||||
|
let _code = chunk.code
|
||||||
|
if (bytecodeRE) {
|
||||||
|
let match: RegExpExecArray | null
|
||||||
|
let s: MagicString | undefined
|
||||||
|
while ((match = bytecodeRE.exec(_code))) {
|
||||||
|
s ||= new MagicString(_code)
|
||||||
|
const [prefix, chunkName] = match
|
||||||
|
const len = prefix.length + chunkName.length
|
||||||
|
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
|
||||||
|
contentOnly: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (s) {
|
||||||
|
_code = s.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bytecodeChunks.includes(name)) {
|
||||||
|
const bytecodeBuffer = await compileToBytecode(bytecodeCompiler, isPreload ? 'preload' : 'main', _code)
|
||||||
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: name + 'c',
|
||||||
|
source: bytecodeBuffer
|
||||||
|
})
|
||||||
|
if (!removeBundleJS) {
|
||||||
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: '_' + chunk.fileName,
|
||||||
|
source: chunk.code
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (chunk.isEntry) {
|
||||||
|
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
|
||||||
|
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
|
||||||
|
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
|
||||||
|
chunk.code = code
|
||||||
|
} else {
|
||||||
|
delete output[chunk.fileName]
|
||||||
|
}
|
||||||
|
bytecodeChunkCount += 1
|
||||||
|
} else {
|
||||||
|
if (chunk.isEntry) {
|
||||||
|
let hasBytecodeMoudle = false
|
||||||
|
const idsToHandle = new Set([...chunk.imports, ...chunk.dynamicImports])
|
||||||
|
for (const moduleId of idsToHandle) {
|
||||||
|
if (bytecodeChunks.includes(moduleId)) {
|
||||||
|
hasBytecodeMoudle = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const moduleInfo = this.getModuleInfo(moduleId)
|
||||||
|
if (moduleInfo) {
|
||||||
|
const { importers, dynamicImporters } = moduleInfo
|
||||||
|
for (const importerId of importers) idsToHandle.add(importerId)
|
||||||
|
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasBytecodeMoudle) {
|
||||||
|
const loader = getBytecodeLoaderBlock(chunk.fileName)
|
||||||
|
const strictRE = /^(#![^\n]*\n)?(["'])use strict\2;/
|
||||||
|
_code = strictRE.test(_code)
|
||||||
|
? _code.replace(strictRE, `$&\n${loader}`)
|
||||||
|
: _code.replace(/^(#![^\n]*\n)?/, `$&${loader}\n`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunk.code = _code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytecodeChunkCount && !_chunks.some(ass => ass.type === 'asset' && ass.fileName === bytecodeModuleLoader)) {
|
||||||
this.emitFile({
|
this.emitFile({
|
||||||
type: 'asset',
|
type: 'asset',
|
||||||
source: bytecodeModuleLoaderCode.join('\n') + '\n',
|
source: bytecodeModuleLoaderCode.join('\n') + '\n',
|
||||||
@ -250,115 +275,85 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async writeBundle(options, output): Promise<void> {
|
writeBundle(_, output): void {
|
||||||
if (useInRenderer || !bytecodeRequired) {
|
if (supported) {
|
||||||
return
|
const bytecodeChunkCount = Object.keys(output).filter(chunk => bytecodeChunkExtensionRE.test(chunk)).length
|
||||||
}
|
this.environment.logger.info(`${colors.green(`✓`)} ${bytecodeChunkCount} chunks compiled into bytecode.`)
|
||||||
|
}
|
||||||
const outDir = options.dir!
|
}
|
||||||
|
}
|
||||||
bytecodeFiles = []
|
}
|
||||||
|
|
||||||
const bundles = Object.keys(output)
|
interface ProtectStringsPluginState extends Babel.PluginPass {
|
||||||
const chunks = Object.values(output).filter(
|
opts: { protectedStrings: Set<string> }
|
||||||
chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name) && chunk.fileName !== bytecodeModuleLoader
|
}
|
||||||
) as OutputChunk[]
|
|
||||||
const bytecodeChunks = chunks.map(chunk => chunk.fileName)
|
function protectStringsPlugin(api: typeof Babel & Babel.ConfigAPI): Babel.PluginObj<ProtectStringsPluginState> {
|
||||||
const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName))
|
const { types: t } = api
|
||||||
|
|
||||||
const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|')
|
function createFromCharCodeFunction(value: string): Babel.types.CallExpression {
|
||||||
const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null
|
const charCodes = Array.from(value).map(s => s.charCodeAt(0))
|
||||||
|
const charCodeLiterals = charCodes.map(code => t.numericLiteral(code))
|
||||||
const keepBundle = (chunkFileName: string): void => {
|
|
||||||
const newFileName = path.resolve(path.dirname(chunkFileName), `_${path.basename(chunkFileName)}`)
|
// String.fromCharCode
|
||||||
fs.renameSync(chunkFileName, newFileName)
|
const memberExpression = t.memberExpression(t.identifier('String'), t.identifier('fromCharCode'))
|
||||||
}
|
// String.fromCharCode(...arr)
|
||||||
|
const callExpression = t.callExpression(memberExpression, [t.spreadElement(t.identifier('arr'))])
|
||||||
const getBytecodeLoaderBlock = (chunkFileName: string): string => {
|
// return String.fromCharCode(...arr)
|
||||||
return `require("${toRelativePath(bytecodeModuleLoader, chunkFileName)}");`
|
const returnStatement = t.returnStatement(callExpression)
|
||||||
}
|
// function (arr) { return ... }
|
||||||
|
const functionExpression = t.functionExpression(null, [t.identifier('arr')], t.blockStatement([returnStatement]))
|
||||||
await Promise.all(
|
|
||||||
bundles.map(async name => {
|
// (function(...) { ... })([x, x, x])
|
||||||
const chunk = output[name]
|
return t.callExpression(functionExpression, [t.arrayExpression(charCodeLiterals)])
|
||||||
if (chunk.type === 'chunk') {
|
}
|
||||||
let _code = chunk.code
|
|
||||||
if (bytecodeRE && _code.match(bytecodeRE)) {
|
return {
|
||||||
let match: RegExpExecArray | null
|
name: 'protect-strings-plugin',
|
||||||
const s = new MagicString(_code)
|
visitor: {
|
||||||
while ((match = bytecodeRE.exec(_code))) {
|
StringLiteral(path, state) {
|
||||||
const [prefix, chunkName] = match
|
// obj['property']
|
||||||
const len = prefix.length + chunkName.length
|
if (path.parentPath.isMemberExpression({ property: path.node, computed: true })) {
|
||||||
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
|
return
|
||||||
contentOnly: true
|
}
|
||||||
})
|
|
||||||
}
|
// { 'key': value }
|
||||||
_code = s.toString()
|
if (path.parentPath.isObjectProperty({ key: path.node, computed: false })) {
|
||||||
}
|
return
|
||||||
const chunkFileName = path.resolve(outDir, name)
|
}
|
||||||
if (bytecodeChunks.includes(name)) {
|
|
||||||
const bytecodeBuffer = await compileToBytecode(_code)
|
// require('fs')
|
||||||
fs.writeFileSync(path.resolve(outDir, name + 'c'), bytecodeBuffer)
|
if (
|
||||||
if (chunk.isEntry) {
|
path.parentPath.isCallExpression() &&
|
||||||
if (!removeBundleJS) {
|
t.isIdentifier(path.parentPath.node.callee) &&
|
||||||
keepBundle(chunkFileName)
|
path.parentPath.node.callee.name === 'require' &&
|
||||||
}
|
path.parentPath.node.arguments[0] === path.node
|
||||||
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
|
) {
|
||||||
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
|
return
|
||||||
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
|
}
|
||||||
fs.writeFileSync(chunkFileName, code)
|
|
||||||
} else {
|
// Only CommonJS is supported, import declaration and export declaration checks are ignored
|
||||||
if (removeBundleJS) {
|
|
||||||
fs.unlinkSync(chunkFileName)
|
const { value } = path.node
|
||||||
} else {
|
if (state.opts.protectedStrings.has(value)) {
|
||||||
keepBundle(chunkFileName)
|
path.replaceWith(createFromCharCodeFunction(value))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
bytecodeFiles.push({ name: name + 'c', size: bytecodeBuffer.length })
|
TemplateLiteral(path, state) {
|
||||||
} else {
|
// Must be a pure static template literal
|
||||||
if (chunk.isEntry) {
|
// expressions must be empty (no ${variables})
|
||||||
let hasBytecodeMoudle = false
|
// quasis must have only one element (meaning the entire string is a single static part).
|
||||||
const idsToHandle = new Set([...chunk.imports, ...chunk.dynamicImports])
|
if (path.node.expressions.length > 0 || path.node.quasis.length !== 1) {
|
||||||
for (const moduleId of idsToHandle) {
|
return
|
||||||
if (bytecodeChunks.includes(moduleId)) {
|
}
|
||||||
hasBytecodeMoudle = true
|
|
||||||
break
|
// Extract the raw value of the template literal
|
||||||
}
|
// path.node.quasis[0].value.raw is used to get the raw string, including escape sequences
|
||||||
const moduleInfo = this.getModuleInfo(moduleId)
|
// path.node.quasis[0].value.cooked is used to get the processed/cooked string (with escape sequences handled)
|
||||||
if (moduleInfo && !moduleInfo.isExternal) {
|
const value = path.node.quasis[0].value.cooked
|
||||||
const { importers, dynamicImporters } = moduleInfo
|
if (value && state.opts.protectedStrings.has(value)) {
|
||||||
for (const importerId of importers) idsToHandle.add(importerId)
|
path.replaceWith(createFromCharCodeFunction(value))
|
||||||
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
|
|
||||||
_code = hasBytecodeMoudle ? _code.replace(useStrict, `${useStrict}\n${bytecodeLoaderBlock}`) : _code
|
|
||||||
}
|
|
||||||
fs.writeFileSync(chunkFileName, _code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
|
||||||
closeBundle(): void {
|
|
||||||
if (!useInRenderer) {
|
|
||||||
const chunkLimit = config.build.chunkSizeWarningLimit
|
|
||||||
const outDir = normalizePath(path.relative(config.root, path.resolve(config.root, config.build.outDir))) + '/'
|
|
||||||
config.logger.info(`${colors.green(`✓`)} ${bytecodeFiles.length} bundles compiled into bytecode.`)
|
|
||||||
let longest = 0
|
|
||||||
bytecodeFiles.forEach(file => {
|
|
||||||
const len = file.name.length
|
|
||||||
if (len > longest) longest = len
|
|
||||||
})
|
|
||||||
bytecodeFiles.forEach(file => {
|
|
||||||
const kbs = file.size / 1000
|
|
||||||
config.logger.info(
|
|
||||||
`${colors.gray(colors.white(colors.dim(outDir)))}${colors.green(file.name.padEnd(longest + 2))} ${
|
|
||||||
kbs > chunkLimit ? colors.yellow(`${kbs.toFixed(2)} kB`) : colors.dim(`${kbs.toFixed(2)} kB`)
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
bytecodeFiles = []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,14 +2,15 @@ import path from 'node:path'
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import { builtinModules } from 'node:module'
|
import { builtinModules } from 'node:module'
|
||||||
import colors from 'picocolors'
|
import colors from 'picocolors'
|
||||||
import { type Plugin, mergeConfig, normalizePath } from 'vite'
|
import { type Plugin, type UserConfig, mergeConfig, normalizePath } from 'vite'
|
||||||
import { getElectronNodeTarget, getElectronChromeTarget } from '../electron'
|
import { getElectronNodeTarget, getElectronChromeTarget, supportESM } from '../electron'
|
||||||
|
import { loadPackageData, resolveBuildOutputs } from '../utils'
|
||||||
|
|
||||||
export interface ElectronPluginOptions {
|
export interface ElectronPluginOptions {
|
||||||
root?: string
|
root?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function findLibEntry(root: string, scope: string): string {
|
function findLibEntry(root: string, scope: string): string | undefined {
|
||||||
for (const name of ['index', scope]) {
|
for (const name of ['index', scope]) {
|
||||||
for (const ext of ['js', 'ts', 'mjs', 'cjs']) {
|
for (const ext of ['js', 'ts', 'mjs', 'cjs']) {
|
||||||
const entryFile = path.resolve(root, 'src', scope, `${name}.${ext}`)
|
const entryFile = path.resolve(root, 'src', scope, `${name}.${ext}`)
|
||||||
@ -18,7 +19,7 @@ function findLibEntry(root: string, scope: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ''
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function findInput(root: string, scope = 'renderer'): string {
|
function findInput(root: string, scope = 'renderer'): string {
|
||||||
@ -29,280 +30,370 @@ function findInput(root: string, scope = 'renderer'): string {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export function electronMainVitePlugin(options?: ElectronPluginOptions): Plugin[] {
|
function processEnvDefine(): Record<string, string> {
|
||||||
return [
|
return {
|
||||||
{
|
'process.env': `process.env`,
|
||||||
name: 'vite:electron-main-preset-config',
|
'global.process.env': `global.process.env`,
|
||||||
apply: 'build',
|
'globalThis.process.env': `globalThis.process.env`
|
||||||
enforce: 'pre',
|
}
|
||||||
config(config): void {
|
}
|
||||||
const root = options?.root || process.cwd()
|
|
||||||
|
|
||||||
const nodeTarget = getElectronNodeTarget()
|
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 defaultConfig = {
|
const nodeTarget = getElectronNodeTarget()
|
||||||
build: {
|
|
||||||
outDir: path.resolve(root, 'out', 'main'),
|
const pkg = loadPackageData() || { type: 'commonjs' }
|
||||||
target: nodeTarget,
|
|
||||||
assetsDir: 'chunks',
|
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
|
||||||
rollupOptions: {
|
|
||||||
external: ['electron', ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
const defaultConfig = {
|
||||||
output: {
|
build: {
|
||||||
entryFileNames: '[name].js'
|
outDir: path.resolve(root, 'out', 'main'),
|
||||||
}
|
target: nodeTarget,
|
||||||
},
|
assetsDir: 'chunks',
|
||||||
reportCompressedSize: false,
|
rolldownOptions: {
|
||||||
minify: false
|
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
||||||
}
|
output: {}
|
||||||
|
},
|
||||||
|
reportCompressedSize: false,
|
||||||
|
minify: false
|
||||||
}
|
}
|
||||||
|
} satisfies UserConfig
|
||||||
|
|
||||||
const build = config.build || {}
|
const build = config.build || {}
|
||||||
const rollupOptions = build.rollupOptions || {}
|
const rolldownOptions = build.rolldownOptions || {}
|
||||||
if (!rollupOptions.input) {
|
if (!rolldownOptions.input) {
|
||||||
defaultConfig.build['lib'] = {
|
const libOptions = build.lib
|
||||||
entry: findLibEntry(root, 'main'),
|
const outputOptions = rolldownOptions.output
|
||||||
formats: ['cjs']
|
defaultConfig.build['lib'] = {
|
||||||
}
|
entry: findLibEntry(root, 'main'),
|
||||||
} else {
|
formats:
|
||||||
defaultConfig.build.rollupOptions.output['format'] = 'cjs'
|
libOptions && libOptions.formats && libOptions.formats.length > 0
|
||||||
|
? []
|
||||||
|
: [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
defaultConfig.build.rolldownOptions.output['format'] = format
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
|
defaultConfig.build.rolldownOptions.output['assetFileNames'] = path.posix.join(
|
||||||
build.assetsDir || defaultConfig.build.assetsDir,
|
build.assetsDir || defaultConfig.build.assetsDir,
|
||||||
'[name]-[hash].[ext]'
|
'[name]-[hash].[ext]'
|
||||||
|
)
|
||||||
|
|
||||||
|
const buildConfig = mergeConfig(defaultConfig.build, build)
|
||||||
|
config.build = buildConfig
|
||||||
|
|
||||||
|
config.define = config.define || {}
|
||||||
|
config.define = { ...processEnvDefine(), ...config.define }
|
||||||
|
|
||||||
|
config.envPrefix = config.envPrefix || ['MAIN_VITE_', 'VITE_']
|
||||||
|
|
||||||
|
config.publicDir = config.publicDir || 'resources'
|
||||||
|
// do not copy public dir
|
||||||
|
config.build.copyPublicDir = false
|
||||||
|
// module preload polyfill does not apply to nodejs (main process)
|
||||||
|
config.build.modulePreload = false
|
||||||
|
// enable ssr build
|
||||||
|
config.build.ssr = true
|
||||||
|
config.build.ssrEmitAssets = true
|
||||||
|
config.ssr = { ...config.ssr, ...{ noExternal: true } }
|
||||||
|
|
||||||
|
config.build.rollupOptions = config.build.rolldownOptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function electronMainConfigValidatorPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite:electron-main-config-validator',
|
||||||
|
apply: 'build',
|
||||||
|
enforce: 'post',
|
||||||
|
configResolved(config): void {
|
||||||
|
const build = config.build
|
||||||
|
if (!build.target) {
|
||||||
|
throw new Error('build.target option is required in the electron vite main config.')
|
||||||
|
} else {
|
||||||
|
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
||||||
|
if (targets.some(t => !t.startsWith('node'))) {
|
||||||
|
throw new Error('The electron vite main config build.target option must be "node?".')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const libOptions = build.lib
|
||||||
|
const rolldownOptions = build.rolldownOptions
|
||||||
|
|
||||||
|
if (!(libOptions && libOptions.entry) && !rolldownOptions?.input) {
|
||||||
|
throw new Error(
|
||||||
|
'An entry point is required in the electron vite main config, ' +
|
||||||
|
'which can be specified using build.lib.entry, build.rollupOptions.input, ' +
|
||||||
|
'or build.rolldownOptions.input.'
|
||||||
)
|
)
|
||||||
|
|
||||||
const buildConfig = mergeConfig(defaultConfig.build, build)
|
|
||||||
config.build = buildConfig
|
|
||||||
|
|
||||||
config.envPrefix = config.envPrefix || 'MAIN_VITE_'
|
|
||||||
|
|
||||||
config.publicDir = config.publicDir || 'resources'
|
|
||||||
// do not copy public dir
|
|
||||||
config.build.copyPublicDir = false
|
|
||||||
// module preload polyfill does not apply to nodejs (main process)
|
|
||||||
config.build.modulePreload = false
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'vite:electron-main-resolved-config',
|
|
||||||
apply: 'build',
|
|
||||||
enforce: 'post',
|
|
||||||
configResolved(config): void {
|
|
||||||
const build = config.build
|
|
||||||
if (!build.target) {
|
|
||||||
throw new Error('build target required for the electron vite main config')
|
|
||||||
} else {
|
|
||||||
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
|
||||||
if (targets.some(t => !t.startsWith('node'))) {
|
|
||||||
throw new Error('the electron vite main config build target must be node')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lib = build.lib
|
const outputs = resolveBuildOutputs(rolldownOptions.output, libOptions)
|
||||||
if (!lib) {
|
|
||||||
const rollupOptions = build.rollupOptions
|
if (outputs.length > 0) {
|
||||||
if (!rollupOptions?.input) {
|
if (outputs.length > 1) {
|
||||||
throw new Error('build lib field required for the electron vite main config')
|
throw new Error('The electron vite main config does not support multiple outputs.')
|
||||||
} else {
|
|
||||||
const output = rollupOptions?.output
|
|
||||||
if (output) {
|
|
||||||
const formats = Array.isArray(output) ? output : [output]
|
|
||||||
if (formats.some(f => f.format !== 'cjs')) {
|
|
||||||
throw new Error('the electron vite main config output format must be cjs')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!lib.entry) {
|
const output = outputs[0]
|
||||||
throw new Error('build entry field required for the electron vite main config')
|
if (['es', 'cjs'].includes(output.format || '')) {
|
||||||
}
|
if (output.format === 'es' && !supportESM()) {
|
||||||
if (!lib.formats) {
|
throw new Error(
|
||||||
throw new Error('build format field required for the electron vite main config')
|
'The electron vite main config output format does not support "es", ' +
|
||||||
} else if (!lib.formats.includes('cjs')) {
|
'you can upgrade electron to the latest version or switch to "cjs" format.'
|
||||||
throw new Error('the electron vite main config build lib format must be cjs')
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`The electron vite main config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function electronPreloadVitePlugin(options?: ElectronPluginOptions): Plugin[] {
|
export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
|
||||||
return [
|
return {
|
||||||
{
|
name: 'vite:electron-preload-config-preset',
|
||||||
name: 'vite:electron-preload-preset-config',
|
apply: 'build',
|
||||||
apply: 'build',
|
enforce: 'pre',
|
||||||
enforce: 'pre',
|
config(config): void {
|
||||||
config(config): void {
|
const root = options?.root || process.cwd()
|
||||||
const root = options?.root || process.cwd()
|
|
||||||
|
|
||||||
const nodeTarget = getElectronNodeTarget()
|
const nodeTarget = getElectronNodeTarget()
|
||||||
|
|
||||||
const defaultConfig = {
|
const pkg = loadPackageData() || { type: 'commonjs' }
|
||||||
build: {
|
|
||||||
outDir: path.resolve(root, 'out', 'preload'),
|
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
|
||||||
target: nodeTarget,
|
|
||||||
assetsDir: 'chunks',
|
const defaultConfig = {
|
||||||
rollupOptions: {
|
ssr: {
|
||||||
external: ['electron', ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
resolve: {
|
||||||
output: {
|
conditions: ['module', 'browser', 'development|production'],
|
||||||
entryFileNames: '[name].js'
|
mainFields: ['browser', 'module', 'jsnext:main', 'jsnext']
|
||||||
}
|
|
||||||
},
|
|
||||||
reportCompressedSize: false,
|
|
||||||
minify: false
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(root, 'out', 'preload'),
|
||||||
|
target: nodeTarget,
|
||||||
|
assetsDir: 'chunks',
|
||||||
|
rolldownOptions: {
|
||||||
|
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
||||||
|
output: {}
|
||||||
|
},
|
||||||
|
reportCompressedSize: false,
|
||||||
|
minify: false
|
||||||
}
|
}
|
||||||
|
} satisfies UserConfig
|
||||||
|
|
||||||
const build = config.build || {}
|
const build = config.build || {}
|
||||||
const rollupOptions = build.rollupOptions || {}
|
const rolldownOptions = build.rolldownOptions || {}
|
||||||
if (!rollupOptions.input) {
|
if (!rolldownOptions.input) {
|
||||||
defaultConfig.build['lib'] = {
|
const libOptions = build.lib
|
||||||
entry: findLibEntry(root, 'preload'),
|
const outputOptions = rolldownOptions.output
|
||||||
formats: ['cjs']
|
defaultConfig.build['lib'] = {
|
||||||
}
|
entry: findLibEntry(root, 'preload'),
|
||||||
|
formats:
|
||||||
|
libOptions && libOptions.formats && libOptions.formats.length > 0
|
||||||
|
? []
|
||||||
|
: [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defaultConfig.build.rolldownOptions.output['format'] = format
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig.build.rolldownOptions.output['assetFileNames'] = path.posix.join(
|
||||||
|
build.assetsDir || defaultConfig.build.assetsDir,
|
||||||
|
'[name]-[hash].[ext]'
|
||||||
|
)
|
||||||
|
|
||||||
|
const buildConfig = mergeConfig(defaultConfig.build, build)
|
||||||
|
config.build = buildConfig
|
||||||
|
|
||||||
|
const outputs = resolveBuildOutputs(config.build.rolldownOptions!.output, config.build.lib)
|
||||||
|
|
||||||
|
if (outputs.some(({ format }) => format === 'es')) {
|
||||||
|
if (Array.isArray(config.build.rolldownOptions!.output)) {
|
||||||
|
config.build.rolldownOptions!.output.forEach(output => {
|
||||||
|
if (output.format === 'es') {
|
||||||
|
output['entryFileNames'] = '[name].mjs'
|
||||||
|
output['chunkFileNames'] = '[name]-[hash].mjs'
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
defaultConfig.build.rollupOptions.output['format'] = 'cjs'
|
config.build.rolldownOptions!.output!['entryFileNames'] = '[name].mjs'
|
||||||
|
config.build.rolldownOptions!.output!['chunkFileNames'] = config.build.lib
|
||||||
|
? '[name]-[hash].mjs'
|
||||||
|
: path.posix.join(build.assetsDir || defaultConfig.build.assetsDir, '[name]-[hash].mjs')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
|
config.define = config.define || {}
|
||||||
build.assetsDir || defaultConfig.build.assetsDir,
|
config.define = { ...processEnvDefine(), ...config.define }
|
||||||
'[name]-[hash].[ext]'
|
|
||||||
|
config.envPrefix = config.envPrefix || ['PRELOAD_VITE_', 'VITE_']
|
||||||
|
|
||||||
|
config.publicDir = config.publicDir || 'resources'
|
||||||
|
// do not copy public dir
|
||||||
|
config.build.copyPublicDir = false
|
||||||
|
// module preload polyfill does not apply to nodejs (preload scripts)
|
||||||
|
config.build.modulePreload = false
|
||||||
|
// enable ssr build
|
||||||
|
config.build.ssr = true
|
||||||
|
config.build.ssrEmitAssets = true
|
||||||
|
config.ssr = mergeConfig(defaultConfig.ssr, config.ssr || {})
|
||||||
|
config.ssr.noExternal = true
|
||||||
|
|
||||||
|
config.build.rollupOptions = config.build.rolldownOptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function electronPreloadConfigValidatorPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite:electron-preload-config-validator',
|
||||||
|
apply: 'build',
|
||||||
|
enforce: 'post',
|
||||||
|
configResolved(config): void {
|
||||||
|
const build = config.build
|
||||||
|
if (!build.target) {
|
||||||
|
throw new Error('build.target option is required in the electron vite preload config.')
|
||||||
|
} else {
|
||||||
|
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
||||||
|
if (targets.some(t => !t.startsWith('node'))) {
|
||||||
|
throw new Error('The electron vite preload config build.target must be "node?".')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const libOptions = build.lib
|
||||||
|
const rolldownOptions = build.rolldownOptions
|
||||||
|
|
||||||
|
if (!(libOptions && libOptions.entry) && !rolldownOptions?.input) {
|
||||||
|
throw new Error(
|
||||||
|
'An entry point is required in the electron vite preload config, ' +
|
||||||
|
'which can be specified using build.lib.entry, build.rollupOptions.input, ' +
|
||||||
|
' or build.rolldownOptions.input.'
|
||||||
)
|
)
|
||||||
|
|
||||||
const buildConfig = mergeConfig(defaultConfig.build, build)
|
|
||||||
config.build = buildConfig
|
|
||||||
|
|
||||||
config.envPrefix = config.envPrefix || 'PRELOAD_VITE_'
|
|
||||||
|
|
||||||
config.publicDir = config.publicDir || 'resources'
|
|
||||||
// do not copy public dir
|
|
||||||
config.build.copyPublicDir = false
|
|
||||||
// module preload polyfill does not apply to nodejs (preload scripts)
|
|
||||||
config.build.modulePreload = false
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'vite:electron-preload-resolved-config',
|
|
||||||
apply: 'build',
|
|
||||||
enforce: 'post',
|
|
||||||
configResolved(config): void {
|
|
||||||
const build = config.build
|
|
||||||
if (!build.target) {
|
|
||||||
throw new Error('build target required for the electron vite preload config')
|
|
||||||
} else {
|
|
||||||
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
|
||||||
if (targets.some(t => !t.startsWith('node'))) {
|
|
||||||
throw new Error('the electron vite preload config build target must be node')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lib = build.lib
|
const outputs = resolveBuildOutputs(rolldownOptions.output, libOptions)
|
||||||
if (!lib) {
|
|
||||||
const rollupOptions = build.rollupOptions
|
if (outputs.length > 0) {
|
||||||
if (!rollupOptions?.input) {
|
if (outputs.length > 1) {
|
||||||
throw new Error('build lib field required for the electron vite preload config')
|
throw new Error('The electron vite preload config does not support multiple outputs.')
|
||||||
|
} else {
|
||||||
|
const output = outputs[0]
|
||||||
|
if (['es', 'cjs'].includes(output.format || '')) {
|
||||||
|
if (output.format === 'es' && !supportESM()) {
|
||||||
|
throw new Error(
|
||||||
|
'The electron vite preload config output format does not support "es", ' +
|
||||||
|
'you can upgrade electron to the latest version or switch to "cjs" format.'
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const output = rollupOptions?.output
|
throw new Error(
|
||||||
if (output) {
|
`The electron vite preload config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
|
||||||
const formats = Array.isArray(output) ? output : [output]
|
)
|
||||||
if (formats.some(f => f.format !== 'cjs')) {
|
|
||||||
throw new Error('the electron vite preload config output format must be cjs')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!lib.entry) {
|
|
||||||
throw new Error('build entry field required for the electron vite preload config')
|
|
||||||
}
|
|
||||||
if (!lib.formats) {
|
|
||||||
throw new Error('build format field required for the electron vite preload config')
|
|
||||||
} else if (!lib.formats.includes('cjs')) {
|
|
||||||
throw new Error('the electron vite preload config lib format must be cjs')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function electronRendererVitePlugin(options?: ElectronPluginOptions): Plugin[] {
|
export function electronRendererConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
|
||||||
return [
|
return {
|
||||||
{
|
name: 'vite:electron-renderer-config-preset',
|
||||||
name: 'vite:electron-renderer-preset-config',
|
enforce: 'pre',
|
||||||
enforce: 'pre',
|
config(config): void {
|
||||||
config(config): void {
|
const root = options?.root || process.cwd()
|
||||||
const root = options?.root || process.cwd()
|
|
||||||
|
|
||||||
config.base =
|
config.base =
|
||||||
config.mode === 'production' || process.env.NODE_ENV_ELECTRON_VITE === 'production' ? './' : config.base
|
config.mode === 'production' || process.env.NODE_ENV_ELECTRON_VITE === 'production' ? './' : config.base
|
||||||
config.root = config.root || './src/renderer'
|
config.root = config.root || './src/renderer'
|
||||||
|
|
||||||
const chromeTarget = getElectronChromeTarget()
|
const chromeTarget = getElectronChromeTarget()
|
||||||
|
|
||||||
const emptyOutDir = (): boolean => {
|
const emptyOutDir = (): boolean => {
|
||||||
let outDir = config.build?.outDir
|
let outDir = config.build?.outDir
|
||||||
if (outDir) {
|
if (outDir) {
|
||||||
if (!path.isAbsolute(outDir)) {
|
if (!path.isAbsolute(outDir)) {
|
||||||
outDir = path.resolve(root, outDir)
|
outDir = path.resolve(root, outDir)
|
||||||
}
|
|
||||||
const resolvedRoot = normalizePath(path.resolve(root))
|
|
||||||
return normalizePath(outDir).startsWith(resolvedRoot + '/')
|
|
||||||
}
|
}
|
||||||
return true
|
const resolvedRoot = normalizePath(path.resolve(root))
|
||||||
|
return normalizePath(outDir).startsWith(resolvedRoot + '/')
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
const defaultConfig = {
|
|
||||||
build: {
|
|
||||||
outDir: path.resolve(root, 'out', 'renderer'),
|
|
||||||
target: chromeTarget,
|
|
||||||
modulePreload: { polyfill: false },
|
|
||||||
rollupOptions: {
|
|
||||||
input: findInput(root)
|
|
||||||
},
|
|
||||||
reportCompressedSize: false,
|
|
||||||
minify: false,
|
|
||||||
emptyOutDir: emptyOutDir()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.build?.outDir) {
|
|
||||||
config.build.outDir = path.resolve(root, config.build.outDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
|
|
||||||
config.build = buildConfig
|
|
||||||
|
|
||||||
config.envDir = config.envDir || path.resolve(root)
|
|
||||||
|
|
||||||
config.envPrefix = config.envPrefix || 'RENDERER_VITE_'
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'vite:electron-renderer-resolved-config',
|
|
||||||
enforce: 'post',
|
|
||||||
configResolved(config): void {
|
|
||||||
if (config.base !== './' && config.base !== '/') {
|
|
||||||
config.logger.warn(colors.yellow('should not set base field for the electron vite renderer config'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const build = config.build
|
const defaultConfig = {
|
||||||
if (!build.target) {
|
build: {
|
||||||
throw new Error('build target required for the electron vite renderer config')
|
outDir: path.resolve(root, 'out', 'renderer'),
|
||||||
} else {
|
target: chromeTarget,
|
||||||
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
modulePreload: { polyfill: false },
|
||||||
if (targets.some(t => !t.startsWith('chrome'))) {
|
rolldownOptions: {
|
||||||
throw new Error('the electron vite renderer config build target must be chrome')
|
input: findInput(root)
|
||||||
}
|
},
|
||||||
|
reportCompressedSize: false,
|
||||||
|
minify: false,
|
||||||
|
emptyOutDir: emptyOutDir()
|
||||||
}
|
}
|
||||||
|
} satisfies UserConfig
|
||||||
|
|
||||||
const rollupOptions = build.rollupOptions
|
if (config.build?.outDir) {
|
||||||
if (!rollupOptions.input) {
|
config.build.outDir = path.resolve(root, config.build.outDir)
|
||||||
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory`))
|
}
|
||||||
throw new Error('build rollupOptions input field required for the electron vite renderer config')
|
|
||||||
|
const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
|
||||||
|
config.build = buildConfig
|
||||||
|
|
||||||
|
config.envDir = config.envDir || path.resolve(root)
|
||||||
|
|
||||||
|
config.envPrefix = config.envPrefix || ['RENDERER_VITE_', 'VITE_']
|
||||||
|
|
||||||
|
config.build.rollupOptions = config.build.rolldownOptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function electronRendererConfigValidatorPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite:electron-renderer-config-validator',
|
||||||
|
enforce: 'post',
|
||||||
|
configResolved(config): void {
|
||||||
|
if (config.base !== './' && config.base !== '/') {
|
||||||
|
config.logger.warn(colors.yellow('(!) Should not set "base" option for the electron vite renderer config.'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const build = config.build
|
||||||
|
if (!build.target) {
|
||||||
|
throw new Error('build.target option is required in the electron vite renderer config.')
|
||||||
|
} else {
|
||||||
|
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
||||||
|
if (targets.some(t => !t.startsWith('chrome') && !/^es((202\d{1})|next)$/.test(t))) {
|
||||||
|
config.logger.warn(
|
||||||
|
'The electron vite renderer config build.target is not "chrome?" or "es?". This could be a mistake.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rolldownOptions = build.rolldownOptions
|
||||||
|
if (!rolldownOptions.input) {
|
||||||
|
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory.`))
|
||||||
|
throw new Error(
|
||||||
|
'build.rollupOptions.input or build.rolldownOptions.input option is required in the electron vite renderer config.'
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
src/plugins/esmShim.ts
Normal file
55
src/plugins/esmShim.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import MagicString from 'magic-string'
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
|
import { supportImportMetaPaths } from '../electron'
|
||||||
|
|
||||||
|
const CJSyntaxRe = /__filename|__dirname|require\(|require\.resolve\(/
|
||||||
|
|
||||||
|
const CJSShim_normal = `
|
||||||
|
// -- CommonJS Shims --
|
||||||
|
import __shims_url__ from 'node:url';
|
||||||
|
import __shims_path__ from 'node:path';
|
||||||
|
import __shims_module__ from 'node:module';
|
||||||
|
const __filename = __shims_url__.fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = __shims_path__.dirname(__filename);
|
||||||
|
const require = __shims_module__.createRequire(import.meta.url);
|
||||||
|
`
|
||||||
|
|
||||||
|
const CJSShim_node_20_11 = `
|
||||||
|
// -- CommonJS Shims --
|
||||||
|
import __shims_module__ from 'node:module';
|
||||||
|
const __filename = import.meta.filename;
|
||||||
|
const __dirname = import.meta.dirname;
|
||||||
|
const require = __shims_module__.createRequire(import.meta.url);
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function esmShimPlugin(): Plugin {
|
||||||
|
const CJSShim = supportImportMetaPaths() ? CJSShim_node_20_11 : CJSShim_normal
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'vite:esm-shim',
|
||||||
|
apply: 'build',
|
||||||
|
enforce: 'post',
|
||||||
|
renderChunk(code, _chunk, { format, sourcemap }) {
|
||||||
|
if (format === 'es') {
|
||||||
|
// Rolldown may remove unused shim bindings after this hook; referenced bindings remain available.
|
||||||
|
if (code.includes(CJSShim) || !CJSyntaxRe.test(code)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastESMImport = this.parse(code)
|
||||||
|
.body.filter(node => node.type === 'ImportDeclaration')
|
||||||
|
.pop()
|
||||||
|
const indexToAppend = lastESMImport ? lastESMImport.end : 0
|
||||||
|
const s = new MagicString(code)
|
||||||
|
s.appendRight(indexToAppend, CJSShim)
|
||||||
|
return {
|
||||||
|
code: s.toString(),
|
||||||
|
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import path from 'node:path'
|
|
||||||
import { createRequire } from 'node:module'
|
|
||||||
import { type Plugin, mergeConfig } from 'vite'
|
import { type Plugin, mergeConfig } from 'vite'
|
||||||
|
import { loadPackageData } from '../utils'
|
||||||
|
|
||||||
export interface ExternalOptions {
|
export interface ExternalOptions {
|
||||||
exclude?: string[]
|
exclude?: string[]
|
||||||
@ -8,37 +7,41 @@ export interface ExternalOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically externalize dependencies
|
* Automatically externalize dependencies.
|
||||||
|
*
|
||||||
|
* @deprecated use `build.externalizeDeps` config option instead
|
||||||
*/
|
*/
|
||||||
export function externalizeDepsPlugin(options: ExternalOptions = {}): Plugin | null {
|
export function externalizeDepsPlugin(options: ExternalOptions = {}): Plugin | null {
|
||||||
const { exclude = [], include = [] } = options
|
const { exclude = [], include = [] } = options
|
||||||
|
|
||||||
const packagePath = path.resolve(process.cwd(), 'package.json')
|
const pkg = loadPackageData() || {}
|
||||||
const require = createRequire(import.meta.url)
|
|
||||||
const pkg = require(packagePath)
|
|
||||||
let deps = Object.keys(pkg.dependencies || {})
|
let deps = Object.keys(pkg.dependencies || {})
|
||||||
|
|
||||||
if (include.length) {
|
if (include.length) {
|
||||||
deps = deps.concat(include)
|
deps = deps.concat(include.filter(dep => dep.trim() !== ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exclude.length) {
|
if (exclude.length) {
|
||||||
deps = deps.filter(dep => !exclude.includes(dep))
|
deps = deps.filter(dep => !exclude.includes(dep))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deps = [...new Set(deps)]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'vite:externalize-deps',
|
name: 'vite:externalize-deps',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
config(config): void {
|
config(config): void {
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rolldownOptions: {
|
||||||
external: [...new Set(deps)]
|
external: deps.length > 0 ? [...deps, new RegExp(`^(${deps.join('|')})/.+`)] : []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
|
const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
|
||||||
config.build = buildConfig
|
config.build = buildConfig
|
||||||
|
|
||||||
|
config.build.rollupOptions = config.build.rolldownOptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/plugins/importMeta.ts
Normal file
34
src/plugins/importMeta.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { type Plugin, version } from 'vite'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Vite 8 (Rolldown) now provides native support for these
|
||||||
|
* `import.meta` properties. These properties are polyfilled when the output
|
||||||
|
* format is CJS.
|
||||||
|
*
|
||||||
|
* @see https://rolldown.rs/in-depth/non-esm-output-formats#well-known-import-meta-properties
|
||||||
|
*/
|
||||||
|
export default function importMetaPlugin(): Plugin | null {
|
||||||
|
const currentVersion = version.split('.')
|
||||||
|
const major = parseInt(currentVersion[0], 10)
|
||||||
|
if (major >= 8) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: 'vite:import-meta',
|
||||||
|
apply: 'build',
|
||||||
|
enforce: 'pre',
|
||||||
|
resolveImportMeta(property, { format }): string | null {
|
||||||
|
if (property === 'url' && format === 'cjs') {
|
||||||
|
return `require("url").pathToFileURL(__filename).href`
|
||||||
|
}
|
||||||
|
if (property === 'filename' && format === 'cjs') {
|
||||||
|
return `__filename`
|
||||||
|
}
|
||||||
|
if (property === 'dirname' && format === 'cjs') {
|
||||||
|
return `__dirname`
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} as any
|
||||||
|
}
|
||||||
199
src/plugins/isolateEntries.ts
Normal file
199
src/plugins/isolateEntries.ts
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */
|
||||||
|
import path from 'node:path'
|
||||||
|
import { type InlineConfig, type Plugin, type LogLevel, type Rolldown, build as viteBuild, mergeConfig } from 'vite'
|
||||||
|
import colors from 'picocolors'
|
||||||
|
import { cleanUrl } from '../utils'
|
||||||
|
import type { ConfigFactory, PreloadViteConfig, RendererViteConfig } from '../config'
|
||||||
|
|
||||||
|
const VIRTUAL_ENTRY_ID = '\0virtual:isolate-entries'
|
||||||
|
|
||||||
|
const LogLevels: Record<LogLevel, number> = {
|
||||||
|
silent: 0,
|
||||||
|
error: 1,
|
||||||
|
warn: 2,
|
||||||
|
info: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function isolateEntriesPlugin(factory: ConfigFactory<PreloadViteConfig | RendererViteConfig>): Plugin {
|
||||||
|
let entries: string[] | { [x: string]: string }[]
|
||||||
|
|
||||||
|
let transformedCount = 0
|
||||||
|
|
||||||
|
const assetCache = new Set<string>()
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'vite:isolate-entries',
|
||||||
|
apply: 'build',
|
||||||
|
|
||||||
|
options(opts): Rolldown.InputOptions | void {
|
||||||
|
const { input } = opts
|
||||||
|
if (input && typeof input === 'object') {
|
||||||
|
if ((Array.isArray(input) && input.length > 0) || Object.keys(input).length > 1) {
|
||||||
|
opts.input = VIRTUAL_ENTRY_ID
|
||||||
|
entries = Array.isArray(input) ? input : Object.entries(input).map(([key, value]) => ({ [key]: value }))
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
buildStart(): void {
|
||||||
|
transformedCount = 0
|
||||||
|
assetCache.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveId(id): string | null {
|
||||||
|
if (id === VIRTUAL_ENTRY_ID) {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
async load(id): Promise<string | void> {
|
||||||
|
if (id === VIRTUAL_ENTRY_ID) {
|
||||||
|
const userConfig = await factory.build(true)
|
||||||
|
const shouldLog = LogLevels[userConfig.logLevel || 'info'] >= LogLevels.info
|
||||||
|
|
||||||
|
const watchFiles = new Set<string>()
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const re = await bundleEntryFile(entry, userConfig, shouldLog, transformedCount)
|
||||||
|
|
||||||
|
const outputChunks = re.bundles.output
|
||||||
|
for (const chunk of outputChunks) {
|
||||||
|
if (assetCache.has(chunk.fileName)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: chunk.fileName,
|
||||||
|
source: chunk.type === 'chunk' ? chunk.code : chunk.source
|
||||||
|
})
|
||||||
|
assetCache.add(chunk.fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.meta.watchMode) {
|
||||||
|
const moduleIds = new Set<string>()
|
||||||
|
for (const chunk of re.bundles.output) {
|
||||||
|
if (chunk.type === 'chunk') {
|
||||||
|
for (const id of chunk.moduleIds) {
|
||||||
|
if (!id.startsWith('\0')) {
|
||||||
|
moduleIds.add(cleanUrl(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const id of moduleIds) {
|
||||||
|
this.addWatchFile(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedCount += re.transformedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of watchFiles) {
|
||||||
|
this.addWatchFile(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
// This is the virtual entry file
|
||||||
|
console.log(1)`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStart(): void {
|
||||||
|
clearLine(-1)
|
||||||
|
this.environment.logger.info(`${colors.green(`✓`)} ${transformedCount} modules transformed.`)
|
||||||
|
},
|
||||||
|
|
||||||
|
generateBundle(_, bundle): void {
|
||||||
|
for (const chunkName in bundle) {
|
||||||
|
if (chunkName.includes('virtual_isolate-entries')) {
|
||||||
|
delete bundle[chunkName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bundleEntryFile(
|
||||||
|
input: string | Record<string, string>,
|
||||||
|
config: InlineConfig,
|
||||||
|
shouldLog: boolean,
|
||||||
|
preTransformedCount: number
|
||||||
|
): Promise<{ bundles: Rolldown.RolldownOutput; transformedCount: number }> {
|
||||||
|
const transformReporter = transformReporterPlugin(preTransformedCount, shouldLog)
|
||||||
|
|
||||||
|
const viteConfig = mergeConfig(config, {
|
||||||
|
build: {
|
||||||
|
write: false,
|
||||||
|
watch: false
|
||||||
|
},
|
||||||
|
plugins: [transformReporter],
|
||||||
|
logLevel: 'warn',
|
||||||
|
configFile: false
|
||||||
|
}) as InlineConfig
|
||||||
|
|
||||||
|
// rewrite the input instead of merging
|
||||||
|
viteConfig.build!.rolldownOptions!.input = input
|
||||||
|
|
||||||
|
const bundles = (await viteBuild(viteConfig)) as Rolldown.RolldownOutput
|
||||||
|
|
||||||
|
return {
|
||||||
|
bundles: bundles,
|
||||||
|
transformedCount: transformReporter?.api?.getTransformedCount() || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformReporterPlugin(
|
||||||
|
preTransformedCount = 0,
|
||||||
|
shouldLog = true
|
||||||
|
): Plugin<{ getTransformedCount: () => number }> {
|
||||||
|
let transformedCount = 0
|
||||||
|
const log = throttle((id, root) => {
|
||||||
|
writeLine(`transforming (${preTransformedCount + transformedCount}) ${colors.dim(path.relative(root, id))}`)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
name: 'vite:transform-reporter',
|
||||||
|
transform(_, id) {
|
||||||
|
transformedCount++
|
||||||
|
if (!shouldLog) return
|
||||||
|
if (id.includes('?')) return
|
||||||
|
log(id, this.environment.config.root)
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
getTransformedCount() {
|
||||||
|
return transformedCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLine(output: string): void {
|
||||||
|
if (!process.stdout.isTTY || process.env.CI) return
|
||||||
|
clearLine()
|
||||||
|
if (output.length < process.stdout.columns) {
|
||||||
|
process.stdout.write(output)
|
||||||
|
} else {
|
||||||
|
process.stdout.write(output.substring(0, process.stdout.columns - 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLine(move: number = 0): void {
|
||||||
|
if (!process.stdout.isTTY || process.env.CI) return
|
||||||
|
if (move < 0) {
|
||||||
|
process.stdout.moveCursor(0, move)
|
||||||
|
}
|
||||||
|
process.stdout.clearLine(0)
|
||||||
|
process.stdout.cursorTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function throttle(fn: Function) {
|
||||||
|
let timerHandle: NodeJS.Timeout | null = null
|
||||||
|
return (...args: any[]) => {
|
||||||
|
if (timerHandle) return
|
||||||
|
fn(...args)
|
||||||
|
timerHandle = setTimeout(() => {
|
||||||
|
timerHandle = null
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/plugins/modulePath.ts
Normal file
126
src/plugins/modulePath.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { type Plugin, type InlineConfig, type Rolldown, build as viteBuild, mergeConfig } from 'vite'
|
||||||
|
import MagicString from 'magic-string'
|
||||||
|
import { cleanUrl, toRelativePath } from '../utils'
|
||||||
|
import { supportImportMetaPaths } from '../electron'
|
||||||
|
import type { ConfigFactory, MainViteConfig } from '../config'
|
||||||
|
|
||||||
|
const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve `?modulePath` import and return the module bundle path.
|
||||||
|
*/
|
||||||
|
export default function modulePathPlugin(factory: ConfigFactory<MainViteConfig>): Plugin {
|
||||||
|
const isImportMetaPathSupported = supportImportMetaPaths()
|
||||||
|
const assetCache = new Set<string>()
|
||||||
|
return {
|
||||||
|
name: 'vite:module-path',
|
||||||
|
apply: 'build',
|
||||||
|
enforce: 'pre',
|
||||||
|
buildStart(): void {
|
||||||
|
assetCache.clear()
|
||||||
|
},
|
||||||
|
async load(id): Promise<string | void> {
|
||||||
|
if (id.endsWith('?modulePath')) {
|
||||||
|
// id resolved by Vite resolve plugin
|
||||||
|
const config = await factory.build(true)
|
||||||
|
const bundles = await bundleEntryFile(cleanUrl(id), config)
|
||||||
|
const [outputChunk, ...outputChunks] = bundles.output
|
||||||
|
const hash = this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: outputChunk.fileName,
|
||||||
|
source: outputChunk.code
|
||||||
|
})
|
||||||
|
for (const chunk of outputChunks) {
|
||||||
|
if (assetCache.has(chunk.fileName)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: chunk.fileName,
|
||||||
|
source: chunk.type === 'chunk' ? chunk.code : chunk.source
|
||||||
|
})
|
||||||
|
assetCache.add(chunk.fileName)
|
||||||
|
}
|
||||||
|
if (this.meta.watchMode) {
|
||||||
|
const moduleIds = new Set<string>()
|
||||||
|
for (const chunk of bundles.output) {
|
||||||
|
if (chunk.type === 'chunk') {
|
||||||
|
for (const id of chunk.moduleIds) {
|
||||||
|
if (!id.startsWith('\0')) {
|
||||||
|
moduleIds.add(cleanUrl(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const id of moduleIds) {
|
||||||
|
this.addWatchFile(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const refId = `__VITE_MODULE_PATH__${hash}__`
|
||||||
|
const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname'
|
||||||
|
return `
|
||||||
|
import { join } from 'path'
|
||||||
|
export default join(${dirnameExpr}, ${refId})`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderChunk(code, chunk, { sourcemap }): { code: string; map: Rolldown.SourceMapInput } | null {
|
||||||
|
let match: RegExpExecArray | null
|
||||||
|
let s: MagicString | undefined
|
||||||
|
|
||||||
|
modulePathRE.lastIndex = 0
|
||||||
|
while ((match = modulePathRE.exec(code))) {
|
||||||
|
s ||= new MagicString(code)
|
||||||
|
const [full, hash] = match
|
||||||
|
const filename = this.getFileName(hash)
|
||||||
|
const outputFilepath = toRelativePath(filename, chunk.fileName)
|
||||||
|
const replacement = JSON.stringify(outputFilepath)
|
||||||
|
s.overwrite(match.index, match.index + full.length, replacement, {
|
||||||
|
contentOnly: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s) {
|
||||||
|
return {
|
||||||
|
code: s.toString(),
|
||||||
|
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bundleEntryFile(input: string, config: InlineConfig): Promise<Rolldown.RolldownOutput> {
|
||||||
|
const viteConfig = mergeConfig(config, {
|
||||||
|
build: {
|
||||||
|
write: false,
|
||||||
|
watch: false
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'vite:entry-file-name',
|
||||||
|
outputOptions(output): Rolldown.OutputOptions {
|
||||||
|
// Prevent output conflicts
|
||||||
|
output.hashCharacters = 'hex'
|
||||||
|
// Assign chunkFileNames to entryFileNames to treat sub-entries as internal chunks
|
||||||
|
output.entryFileNames = output.chunkFileNames
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
] as Plugin[],
|
||||||
|
logLevel: 'warn',
|
||||||
|
configFile: false
|
||||||
|
}) as InlineConfig
|
||||||
|
|
||||||
|
// rewrite the input instead of merging
|
||||||
|
const buildOptions = viteConfig.build!
|
||||||
|
buildOptions.rolldownOptions = {
|
||||||
|
...buildOptions.rolldownOptions,
|
||||||
|
input
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundles = await viteBuild(viteConfig)
|
||||||
|
|
||||||
|
return bundles as Rolldown.RolldownOutput
|
||||||
|
}
|
||||||
@ -79,12 +79,16 @@ export type SwcOptions = {
|
|||||||
export function swcPlugin(options: SwcOptions = {}): Plugin {
|
export function swcPlugin(options: SwcOptions = {}): Plugin {
|
||||||
const filter = createFilter(options.include || /\.(m?ts|[jt]sx)$/, options.exclude || /\.js$/)
|
const filter = createFilter(options.include || /\.(m?ts|[jt]sx)$/, options.exclude || /\.js$/)
|
||||||
let sourcemap: boolean | 'inline' = false
|
let sourcemap: boolean | 'inline' = false
|
||||||
let minify: boolean | 'esbuild' | 'terser' = false
|
let minify: boolean | 'esbuild' | 'oxc' | 'terser' = false
|
||||||
return {
|
return {
|
||||||
name: 'vite:swc',
|
name: 'vite:swc',
|
||||||
config(): UserConfig {
|
config(): UserConfig {
|
||||||
|
// Vite 8 defaults to Oxc for transform; `esbuild: false` is ignored and
|
||||||
|
// warns. SWC replaces both paths when this plugin is active.
|
||||||
|
// See https://github.com/alex8088/electron-vite/issues/916
|
||||||
return {
|
return {
|
||||||
esbuild: false
|
esbuild: false,
|
||||||
|
oxc: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async configResolved(resolvedConfig): Promise<void> {
|
async configResolved(resolvedConfig): Promise<void> {
|
||||||
@ -101,7 +105,7 @@ export function swcPlugin(options: SwcOptions = {}): Plugin {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async renderChunk(code, chunk): Promise<null | { code: string; map: SourceMap }> {
|
async renderChunk(code, chunk): Promise<null | { code: string; map: SourceMap }> {
|
||||||
if (!minify || minify === 'terser') {
|
if (!minify || minify === 'terser' || minify === 'oxc') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const result = await transformWithSWC(code, chunk.fileName, {
|
const result = await transformWithSWC(code, chunk.fileName, {
|
||||||
|
|||||||
@ -1,61 +1,60 @@
|
|||||||
import type { Plugin } from 'vite'
|
import type { Plugin } from 'vite'
|
||||||
import type { SourceMapInput } from 'rollup'
|
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { cleanUrl, parseRequest, toRelativePath } from '../utils'
|
import { cleanUrl, toRelativePath } from '../utils'
|
||||||
|
|
||||||
const nodeWorkerAssetUrlRE = /__VITE_NODE_WORKER_ASSET__([a-z\d]{8})__/g
|
const nodeWorkerAssetUrlRE = /__VITE_NODE_WORKER_ASSET__([\w$]+)__/g
|
||||||
|
const nodeWorkerRE = /\?nodeWorker(?:&|$)/
|
||||||
|
const nodeWorkerImporterRE = /(?:\?)nodeWorker&importer=([^&]+)(?:&|$)/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve `?nodeWorker` import and automatically generate `Worker` wrapper.
|
* Resolve `?nodeWorker` import and automatically generate `Worker` wrapper.
|
||||||
*/
|
*/
|
||||||
export default function workerPlugin(): Plugin {
|
export default function workerPlugin(): Plugin {
|
||||||
let sourcemap: boolean | 'inline' | 'hidden' = false
|
|
||||||
return {
|
return {
|
||||||
name: 'vite:node-worker',
|
name: 'vite:node-worker',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
configResolved(config): void {
|
|
||||||
sourcemap = config.build.sourcemap
|
|
||||||
},
|
|
||||||
resolveId(id, importer): string | void {
|
resolveId(id, importer): string | void {
|
||||||
const query = parseRequest(id)
|
if (id.endsWith('?nodeWorker')) {
|
||||||
if (query && typeof query.nodeWorker === 'string') {
|
|
||||||
return id + `&importer=${importer}`
|
return id + `&importer=${importer}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
load(id): string | void {
|
load(id): string | void {
|
||||||
const query = parseRequest(id)
|
if (nodeWorkerRE.test(id)) {
|
||||||
if (query && typeof query.nodeWorker === 'string' && typeof query.importer === 'string') {
|
const match = nodeWorkerImporterRE.exec(id)
|
||||||
const cleanPath = cleanUrl(id)
|
if (match) {
|
||||||
const hash = this.emitFile({
|
const hash = this.emitFile({
|
||||||
type: 'chunk',
|
type: 'chunk',
|
||||||
id: cleanPath,
|
id: cleanUrl(id),
|
||||||
importer: query.importer
|
importer: match[1]
|
||||||
})
|
})
|
||||||
const assetRefId = `__VITE_NODE_WORKER_ASSET__${hash}__`
|
const assetRefId = `__VITE_NODE_WORKER_ASSET__${hash}__`
|
||||||
return `
|
return `
|
||||||
import { Worker } from 'node:worker_threads';
|
import { Worker } from 'node:worker_threads';
|
||||||
export default function (options) { return new Worker(require.resolve(${assetRefId}), options); }`
|
export default function (options) { return new Worker(new URL(${assetRefId}, import.meta.url), options); }`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null {
|
renderChunk(code, chunk, { sourcemap }) {
|
||||||
if (code.match(nodeWorkerAssetUrlRE)) {
|
let match: RegExpExecArray | null
|
||||||
let match: RegExpExecArray | null
|
let s: MagicString | undefined
|
||||||
const s = new MagicString(code)
|
|
||||||
|
|
||||||
while ((match = nodeWorkerAssetUrlRE.exec(code))) {
|
nodeWorkerAssetUrlRE.lastIndex = 0
|
||||||
const [full, hash] = match
|
while ((match = nodeWorkerAssetUrlRE.exec(code))) {
|
||||||
const filename = this.getFileName(hash)
|
s ||= new MagicString(code)
|
||||||
const outputFilepath = toRelativePath(filename, chunk.fileName)
|
const [full, hash] = match
|
||||||
const replacement = JSON.stringify(outputFilepath)
|
const filename = this.getFileName(hash)
|
||||||
s.overwrite(match.index, match.index + full.length, replacement, {
|
const outputFilepath = toRelativePath(filename, chunk.fileName)
|
||||||
contentOnly: true
|
const replacement = JSON.stringify(outputFilepath)
|
||||||
})
|
s.overwrite(match.index, match.index + full.length, replacement, {
|
||||||
}
|
contentOnly: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s) {
|
||||||
return {
|
return {
|
||||||
code: s.toString(),
|
code: s.toString(),
|
||||||
map: sourcemap ? s.generateMap({ hires: true }) : null
|
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export async function preview(inlineConfig: InlineConfig = {}, options: { skipBu
|
|||||||
|
|
||||||
const logger = createLogger(inlineConfig.logLevel)
|
const logger = createLogger(inlineConfig.logLevel)
|
||||||
|
|
||||||
startElectron(inlineConfig.root, logger)
|
startElectron(inlineConfig.root)
|
||||||
|
|
||||||
logger.info(colors.green(`\nstart electron app...\n`))
|
logger.info(colors.green(`\nstarting electron app...\n`))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import type { ChildProcessWithoutNullStreams } from 'node:child_process'
|
import type { ChildProcess } from 'node:child_process'
|
||||||
import {
|
import {
|
||||||
type UserConfig as ViteConfig,
|
type UserConfig as ViteConfig,
|
||||||
type ViteDevServer,
|
type ViteDevServer,
|
||||||
createServer as ViteCreateServer,
|
createServer as viteCreateServer,
|
||||||
build as viteBuild,
|
build as viteBuild,
|
||||||
createLogger,
|
createLogger,
|
||||||
mergeConfig
|
mergeConfig
|
||||||
@ -22,27 +22,29 @@ export async function createServer(
|
|||||||
const logger = createLogger(inlineConfig.logLevel)
|
const logger = createLogger(inlineConfig.logLevel)
|
||||||
|
|
||||||
let server: ViteDevServer | undefined
|
let server: ViteDevServer | undefined
|
||||||
let ps: ChildProcessWithoutNullStreams | undefined
|
let ps: ChildProcess | undefined
|
||||||
|
|
||||||
|
const errorHook = (e): void => {
|
||||||
|
logger.error(`${colors.bgRed(colors.white(' ERROR '))} ${colors.red(e.message)}`)
|
||||||
|
}
|
||||||
|
|
||||||
const mainViteConfig = config.config?.main
|
const mainViteConfig = config.config?.main
|
||||||
if (mainViteConfig && !options.rendererOnly) {
|
if (mainViteConfig && !options.rendererOnly) {
|
||||||
const watchHook = (): void => {
|
const watchHook = (): void => {
|
||||||
logger.info(colors.green(`\nrebuild the electron main process successfully`))
|
logger.info(colors.green(`\nelectron main process rebuilt successfully`))
|
||||||
|
|
||||||
if (ps) {
|
if (ps) {
|
||||||
logger.info(colors.cyan(`\n waiting for electron to exit...`))
|
|
||||||
|
|
||||||
ps.removeAllListeners()
|
ps.removeAllListeners()
|
||||||
ps.kill()
|
ps.kill()
|
||||||
ps = startElectron(inlineConfig.root, logger)
|
ps = startElectron(inlineConfig.root)
|
||||||
|
|
||||||
logger.info(colors.green(`\nrestart electron app...`))
|
logger.info(colors.green(`\nrestarting electron app...\n`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await doBuild(mainViteConfig, watchHook)
|
await doBuild(mainViteConfig, watchHook, errorHook)
|
||||||
|
|
||||||
logger.info(colors.green(`\nbuild the electron main process successfully`))
|
logger.info(colors.green(`\nelectron main process built successfully`))
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadViteConfig = config.config?.preload
|
const preloadViteConfig = config.config?.preload
|
||||||
@ -50,25 +52,23 @@ export async function createServer(
|
|||||||
logger.info(colors.gray(`\n-----\n`))
|
logger.info(colors.gray(`\n-----\n`))
|
||||||
|
|
||||||
const watchHook = (): void => {
|
const watchHook = (): void => {
|
||||||
logger.info(colors.green(`\nrebuild the electron preload files successfully`))
|
logger.info(colors.green(`\nelectron preload scripts rebuilt successfully`))
|
||||||
|
|
||||||
if (server) {
|
if (server) {
|
||||||
logger.info(colors.cyan(`\n trigger renderer reload`))
|
logger.info(colors.cyan(`\nreloading electron renderer...\n`))
|
||||||
|
|
||||||
server.ws.send({ type: 'full-reload' })
|
server.ws.send({ type: 'full-reload' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await doBuild(preloadViteConfig, watchHook)
|
await doBuild(preloadViteConfig, watchHook, errorHook)
|
||||||
|
|
||||||
logger.info(colors.green(`\nbuild the electron preload files successfully`))
|
logger.info(colors.green(`\nelectron preload scripts built successfully`))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.rendererOnly) {
|
if (options.rendererOnly) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`\n${colors.yellow(colors.bold('warn'))}:${colors.yellow(
|
`\n${colors.yellow(colors.bold('(!)'))} ${colors.yellow('skipped building main process and preload scripts (using previous build)')}`
|
||||||
' you have skipped the main process and preload scripts building'
|
|
||||||
)}`
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ export async function createServer(
|
|||||||
if (rendererViteConfig) {
|
if (rendererViteConfig) {
|
||||||
logger.info(colors.gray(`\n-----\n`))
|
logger.info(colors.gray(`\n-----\n`))
|
||||||
|
|
||||||
server = await ViteCreateServer(rendererViteConfig)
|
server = await viteCreateServer(rendererViteConfig)
|
||||||
|
|
||||||
if (!server.httpServer) {
|
if (!server.httpServer) {
|
||||||
throw new Error('HTTP server not available')
|
throw new Error('HTTP server not available')
|
||||||
@ -100,15 +100,15 @@ export async function createServer(
|
|||||||
server.printUrls()
|
server.printUrls()
|
||||||
}
|
}
|
||||||
|
|
||||||
ps = startElectron(inlineConfig.root, logger)
|
ps = startElectron(inlineConfig.root)
|
||||||
|
|
||||||
logger.info(colors.green(`\nstart electron app...\n`))
|
logger.info(colors.green(`\nstarting electron app...\n`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserConfig = ViteConfig & { configFile?: string | false }
|
type UserConfig = ViteConfig & { configFile?: string | false }
|
||||||
|
|
||||||
async function doBuild(config: UserConfig, watchHook: () => void): Promise<void> {
|
async function doBuild(config: UserConfig, watchHook: () => void, errorHook: (e: Error) => void): Promise<void> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (config.build?.watch) {
|
if (config.build?.watch) {
|
||||||
let firstBundle = true
|
let firstBundle = true
|
||||||
@ -131,10 +131,12 @@ async function doBuild(config: UserConfig, watchHook: () => void): Promise<void>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
viteBuild(config).then(() => {
|
viteBuild(config)
|
||||||
if (!config.build?.watch) {
|
.then(() => {
|
||||||
resolve()
|
if (!config.build?.watch) {
|
||||||
}
|
resolve()
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
.catch(e => errorHook(e))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
173
src/utils.ts
173
src/utils.ts
@ -1,14 +1,14 @@
|
|||||||
import { URL, URLSearchParams } from 'node:url'
|
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
import fs from 'node:fs'
|
||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { loadEnv as viteLoadEnv } from 'vite'
|
import { createRequire } from 'node:module'
|
||||||
|
import { type LibraryOptions, type Rolldown, loadEnv as viteLoadEnv } from 'vite'
|
||||||
|
|
||||||
export function isObject(value: unknown): value is Record<string, unknown> {
|
export function isObject(value: unknown): value is Record<string, unknown> {
|
||||||
return Object.prototype.toString.call(value) === '[object Object]'
|
return Object.prototype.toString.call(value) === '[object Object]'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dynamicImport = new Function('file', 'return import(file)')
|
|
||||||
|
|
||||||
export const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000'])
|
export const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000'])
|
||||||
|
|
||||||
export function resolveHostname(optionsHost: string | boolean | undefined): string {
|
export function resolveHostname(optionsHost: string | boolean | undefined): string {
|
||||||
@ -20,16 +20,11 @@ export const hashRE = /#.*$/s
|
|||||||
|
|
||||||
export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '')
|
export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '')
|
||||||
|
|
||||||
export function parseRequest(id: string): Record<string, string> | null {
|
|
||||||
const { search } = new URL(id, 'file:')
|
|
||||||
if (!search) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return Object.fromEntries(new URLSearchParams(search))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHash(text: Buffer | string): string {
|
export function getHash(text: Buffer | string): string {
|
||||||
return createHash('sha256').update(text).digest('hex').substring(0, 8)
|
return createHash('sha256')
|
||||||
|
.update(text as unknown as Uint8Array)
|
||||||
|
.digest('hex')
|
||||||
|
.substring(0, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toRelativePath(filename: string, importer: string): string {
|
export function toRelativePath(filename: string, importer: string): string {
|
||||||
@ -38,14 +33,160 @@ export function toRelativePath(filename: string, importer: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load `.env` files within the `envDir`(default: `process.cwd()`).
|
* Resolve configured build outputs into an array.
|
||||||
* By default, only env variables prefixed with `MAIN_VITE_`, `PRELOAD_VITE_` and
|
*/
|
||||||
|
export function resolveBuildOutputs(
|
||||||
|
outputs: Rolldown.OutputOptions | Rolldown.OutputOptions[] | undefined,
|
||||||
|
libOptions: LibraryOptions | false | undefined
|
||||||
|
): Rolldown.OutputOptions[] {
|
||||||
|
if (Array.isArray(outputs)) {
|
||||||
|
return outputs
|
||||||
|
}
|
||||||
|
if (libOptions) {
|
||||||
|
const libFormats = libOptions.formats || []
|
||||||
|
return libFormats.map(format => ({ ...outputs, format }))
|
||||||
|
}
|
||||||
|
return outputs ? [outputs] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load `.env` files within the `envDir` (default: `process.cwd()`) .
|
||||||
|
* By default, only env variables prefixed with `VITE_`, `MAIN_VITE_`, `PRELOAD_VITE_` and
|
||||||
* `RENDERER_VITE_` are loaded, unless `prefixes` is changed.
|
* `RENDERER_VITE_` are loaded, unless `prefixes` is changed.
|
||||||
*/
|
*/
|
||||||
export function loadEnv(
|
export function loadEnv(
|
||||||
mode: string,
|
mode: string,
|
||||||
envDir: string = process.cwd(),
|
envDir: string = process.cwd(),
|
||||||
prefixes: string | string[] = ['MAIN_VITE_', 'PRELOAD_VITE_', 'RENDERER_VITE_']
|
prefixes: string | string[] = ['VITE_', 'MAIN_VITE_', 'PRELOAD_VITE_', 'RENDERER_VITE_']
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
return viteLoadEnv(mode, envDir, prefixes)
|
return viteLoadEnv(mode, envDir, prefixes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PackageData {
|
||||||
|
main?: string
|
||||||
|
type?: 'module' | 'commonjs'
|
||||||
|
dependencies?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
let packageCached: PackageData | null = null
|
||||||
|
|
||||||
|
export function loadPackageData(root = process.cwd()): PackageData | null {
|
||||||
|
if (packageCached) return packageCached
|
||||||
|
const pkg = path.join(root, 'package.json')
|
||||||
|
if (fs.existsSync(pkg)) {
|
||||||
|
const _require = createRequire(import.meta.url)
|
||||||
|
const data = _require(pkg)
|
||||||
|
packageCached = {
|
||||||
|
main: data.main,
|
||||||
|
type: data.type,
|
||||||
|
dependencies: data.dependencies
|
||||||
|
}
|
||||||
|
return packageCached
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFilePathESM(filePath: string): boolean {
|
||||||
|
if (/\.m[jt]s$/.test(filePath) || filePath.endsWith('.ts')) {
|
||||||
|
return true
|
||||||
|
} else if (/\.c[jt]s$/.test(filePath)) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
const pkg = loadPackageData()
|
||||||
|
return pkg?.type === 'module'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeepWritable<T> =
|
||||||
|
T extends ReadonlyArray<unknown>
|
||||||
|
? { -readonly [P in keyof T]: DeepWritable<T[P]> }
|
||||||
|
: T extends RegExp
|
||||||
|
? RegExp
|
||||||
|
: T[keyof T] extends Function
|
||||||
|
? T
|
||||||
|
: { -readonly [P in keyof T]: DeepWritable<T[P]> }
|
||||||
|
|
||||||
|
export function deepClone<T>(value: T): DeepWritable<T> {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(v => deepClone(v)) as DeepWritable<T>
|
||||||
|
}
|
||||||
|
if (isObject(value)) {
|
||||||
|
const cloned: Record<string, any> = {}
|
||||||
|
for (const key in value) {
|
||||||
|
cloned[key] = deepClone(value[key])
|
||||||
|
}
|
||||||
|
return cloned as DeepWritable<T>
|
||||||
|
}
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
return value as DeepWritable<T>
|
||||||
|
}
|
||||||
|
if (value instanceof RegExp) {
|
||||||
|
return new RegExp(value) as DeepWritable<T>
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && value != null) {
|
||||||
|
throw new Error('Cannot deep clone non-plain object')
|
||||||
|
}
|
||||||
|
return value as DeepWritable<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Isolate configuration data without cloning plugins or runtime instances.
|
||||||
|
* Plugins may capture their own object in hooks, so their identity must survive.
|
||||||
|
*/
|
||||||
|
export function cloneConfig<T>(config: T): T {
|
||||||
|
const seen = new WeakMap<object, unknown>()
|
||||||
|
const pluginArrays = new WeakMap<object, unknown>()
|
||||||
|
|
||||||
|
function clone(value: unknown, plugins = false): unknown {
|
||||||
|
if (value === null || typeof value !== 'object') return value
|
||||||
|
|
||||||
|
const cache = plugins ? pluginArrays : seen
|
||||||
|
if (cache.has(value)) return cache.get(value)
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const result: unknown[] = new Array(value.length)
|
||||||
|
cache.set(value, result)
|
||||||
|
value.forEach((item, index) => {
|
||||||
|
result[index] = clone(item, plugins)
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve plugin objects, promises and other opaque plugin options.
|
||||||
|
if (plugins) return value
|
||||||
|
|
||||||
|
if (value instanceof RegExp) {
|
||||||
|
const result = new RegExp(value)
|
||||||
|
result.lastIndex = value.lastIndex
|
||||||
|
cache.set(value, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const prototype = Object.getPrototypeOf(value)
|
||||||
|
if (prototype !== Object.prototype && prototype !== null) return value
|
||||||
|
|
||||||
|
const result = Object.create(prototype) as Record<PropertyKey, unknown>
|
||||||
|
cache.set(value, result)
|
||||||
|
for (const key of Reflect.ownKeys(value)) {
|
||||||
|
if (!Object.prototype.propertyIsEnumerable.call(value, key)) continue
|
||||||
|
Object.defineProperty(result, key, {
|
||||||
|
value: clone((value as Record<PropertyKey, unknown>)[key], key === 'plugins'),
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone(config) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
type AsyncFlatten<T extends unknown[]> = T extends (infer U)[] ? Exclude<Awaited<U>, U[]>[] : never
|
||||||
|
|
||||||
|
export async function asyncFlatten<T extends unknown[]>(arr: T): Promise<AsyncFlatten<T>> {
|
||||||
|
do {
|
||||||
|
arr = (await Promise.all(arr)).flat(Infinity) as any
|
||||||
|
} while (arr.some((v: any) => v?.then))
|
||||||
|
return arr as unknown[] as AsyncFlatten<T>
|
||||||
|
}
|
||||||
|
|||||||
@ -1,23 +1,20 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2019",
|
"target": "ES2023",
|
||||||
"module": "esnext",
|
"module": "ESNext",
|
||||||
"lib": ["esnext"],
|
"lib": ["ESNext"],
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true
|
||||||
"declaration": true,
|
|
||||||
"declarationDir": "dist/types",
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src", "rollup.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user