mirror of
https://github.com/alex8088/electron-vite.git
synced 2026-11-02 22:56:58 +08:00
Compare commits
No commits in common. "master" and "v1.0.25" have entirely different histories.
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
38
.eslintrc.js
Normal file
38
.eslintrc.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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
1
.github/FUNDING.yml
vendored
@ -1 +0,0 @@
|
|||||||
github: alex8088
|
|
||||||
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,5 +1,6 @@
|
|||||||
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:
|
||||||
@ -44,7 +45,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://electron-vite.org).
|
- label: Read the [docs](https://evite.netlify.app).
|
||||||
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,5 +1 @@
|
|||||||
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://electron-vite.org).
|
- label: Read the [docs](https://evite.netlify.app).
|
||||||
required: true
|
required: true
|
||||||
- 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.
|
- 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
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
.DS_Store
|
|
||||||
.eslintcache
|
|
||||||
*.log*
|
*.log*
|
||||||
|
|||||||
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
301
CHANGELOG.md
301
CHANGELOG.md
@ -1,304 +1,3 @@
|
|||||||
### 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_)
|
### v1.0.25 (_2023-07-11_)
|
||||||
|
|
||||||
- fix: remove node resolve condition for preload [#204](https://github.com/alex8088/electron-vite/issues/204)
|
- fix: remove node resolve condition for preload [#204](https://github.com/alex8088/electron-vite/issues/204)
|
||||||
|
|||||||
16
README.md
16
README.md
@ -28,13 +28,13 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- ⚡️ [Vite](https://vitejs.dev) powered and use the same way.
|
- ⚡️ [Vite](https://vitejs.dev) powered and use the same way.
|
||||||
- 🛠 Pre-configure with sensible defaults optimized for Electron.
|
- 🛠 Pre-configured for Electron, don't worry about configuration.
|
||||||
- 💡 Optimize asset handling for Electron main process.
|
- 💡 Optimize asset handling (Node.js addons, WebAssembly, Worker Thread, etc).
|
||||||
- 🚀 Fast HMR & hot reloading.
|
- 🚀 Fast HMR for renderer processes.
|
||||||
- 🔥 Isolated build for multi-entry application development.
|
- 🔥 Hot reloading for main process and preload scripts.
|
||||||
- ✨ Simplify multi-threading development.
|
- 🔌 Easy to debug in IDEs like VSCode or WebStorm.
|
||||||
- 🔒 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.
|
||||||
- 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more.
|
- 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -83,7 +83,7 @@ export default {
|
|||||||
Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project.
|
Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm create @quick-start/electron@latest
|
npm create @quick-start/electron
|
||||||
```
|
```
|
||||||
|
|
||||||
Currently supported template presets include:
|
Currently supported template presets include:
|
||||||
|
|||||||
52
api-extractor.json
Normal file
52
api-extractor.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +0,0 @@
|
|||||||
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') }
|
|
||||||
}
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
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'))
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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() {
|
||||||
import('../dist/cli.js')
|
require('../dist/cli')
|
||||||
}
|
}
|
||||||
|
|
||||||
run()
|
run()
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
// 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,15 +1,9 @@
|
|||||||
// node worker
|
// node worker
|
||||||
declare module '*?nodeWorker' {
|
declare module '*?nodeWorker' {
|
||||||
import type { Worker, WorkerOptions } from 'node:worker_threads'
|
import { 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
|
||||||
@ -21,11 +15,6 @@ 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 */
|
||||||
@ -38,75 +27,3 @@ 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,17 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "electron-vite",
|
"name": "electron-vite",
|
||||||
"version": "6.0.0",
|
"version": "1.0.25",
|
||||||
"description": "Electron build tooling based on Vite",
|
"description": "Electron build tooling based on Vite",
|
||||||
"type": "module",
|
"main": "dist/index.js",
|
||||||
"main": "./dist/index.js",
|
"types": "dist/index.d.ts",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
@ -21,9 +13,8 @@
|
|||||||
"node.d.ts"
|
"node.d.ts"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^14.18.0 || >=16.0.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": {
|
||||||
@ -33,7 +24,7 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/alex8088/electron-vite/issues"
|
"url": "https://github.com/alex8088/electron-vite/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://electron-vite.org",
|
"homepage": "https://github.com/alex8088/electron-vite#readme",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"electron",
|
"electron",
|
||||||
"vite",
|
"vite",
|
||||||
@ -42,9 +33,9 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "eslint --cache .",
|
"lint": "eslint --ext .ts src/**",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "pnpm run lint && rollup -c rollup.config.ts --configPlugin typescript"
|
"build": "npm run lint && node scripts/build.js"
|
||||||
},
|
},
|
||||||
"simple-git-hooks": {
|
"simple-git-hooks": {
|
||||||
"pre-commit": "npx lint-staged",
|
"pre-commit": "npx lint-staged",
|
||||||
@ -61,7 +52,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@swc/core": "^1.0.0",
|
"@swc/core": "^1.0.0",
|
||||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"vite": "^3.0.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@swc/core": {
|
"@swc/core": {
|
||||||
@ -69,40 +60,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@microsoft/api-extractor": "^7.36.1",
|
||||||
"@rollup/plugin-json": "^6.1.0",
|
"@rollup/plugin-node-resolve": "^15.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
"@rollup/plugin-typescript": "^9.0.2",
|
||||||
"@rollup/plugin-typescript": "^12.3.0",
|
"@swc/core": "^1.3.68",
|
||||||
"@swc/core": "^1.16.2",
|
"@types/node": "^18.16.19",
|
||||||
"@types/babel__core": "^7.20.5",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@types/node": "^22.20.4",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
"eslint": "^10.11.0",
|
"eslint": "^8.44.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"globals": "^17.12.0",
|
"fs-extra": "^11.1.1",
|
||||||
"lint-staged": "^17.5.1",
|
"lint-staged": "^13.2.3",
|
||||||
"prettier": "^3.9.9",
|
"prettier": "^2.8.8",
|
||||||
"rollup": "^4.63.5",
|
"rollup": "^3.26.2",
|
||||||
"rollup-plugin-dts": "^6.5.1",
|
"simple-git-hooks": "^2.8.1",
|
||||||
"rollup-plugin-rm": "^1.0.2",
|
"tslib": "^2.6.0",
|
||||||
"simple-git-hooks": "^2.14.0",
|
"typescript": "5.0.4",
|
||||||
"tslib": "^2.8.1",
|
"vite": "^4.4.2"
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"typescript-eslint": "^8.70.1",
|
|
||||||
"vite": "^8.3.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.29.7",
|
"@babel/core": "^7.22.8",
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.29.7",
|
"@babel/plugin-transform-arrow-functions": "^7.22.5",
|
||||||
"cac": "^7.0.0",
|
"cac": "^6.7.14",
|
||||||
"magic-string": "^1.4.2",
|
"esbuild": "^0.18.11",
|
||||||
"picocolors": "^1.1.1",
|
"magic-string": "^0.30.1",
|
||||||
"rolldown": "~1.2.10"
|
"picocolors": "^1.0.0"
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"onlyBuiltDependencies": [
|
|
||||||
"@swc/core",
|
|
||||||
"simple-git-hooks"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4276
pnpm-lock.yaml
generated
4276
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,42 +0,0 @@
|
|||||||
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')]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
67
scripts/build.js
Normal file
67
scripts/build.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
import colors from 'picocolors'
|
const colors = require('picocolors')
|
||||||
import fs from 'node:fs'
|
const fs = require('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()
|
||||||
|
|||||||
63
src/build.ts
63
src/build.ts
@ -1,56 +1,33 @@
|
|||||||
import { build as viteBuild, mergeConfig } from 'vite'
|
import { build as viteBuild } from 'vite'
|
||||||
import { type InlineConfig, type MainViteConfig, type PreloadViteConfig, resolveConfig } from './config'
|
import { InlineConfig, 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')
|
||||||
const bytecodeCompiler = new BytecodeCompiler()
|
if (config.config) {
|
||||||
const config = await resolveConfig(
|
const mainViteConfig = config.config?.main
|
||||||
mergeConfig(inlineConfig, {
|
if (mainViteConfig) {
|
||||||
plugins: [{ name: 'vite:electron-bytecode-build', api: { compiler: bytecodeCompiler } }]
|
if (mainViteConfig.build?.watch) {
|
||||||
}),
|
mainViteConfig.build.watch = null
|
||||||
'build',
|
|
||||||
'production'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!config.config) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
await viteBuild(mainViteConfig)
|
||||||
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)
|
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,188 +0,0 @@
|
|||||||
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,8 +1,7 @@
|
|||||||
import { cac } from 'cac'
|
import { cac } from 'cac'
|
||||||
import colors from 'picocolors'
|
import colors from 'picocolors'
|
||||||
import { type LogLevel, createLogger } from 'vite'
|
import { LogLevel, createLogger } from 'vite'
|
||||||
import type { InlineConfig } from './config'
|
import { InlineConfig } from './config'
|
||||||
import { version } from '../package.json'
|
|
||||||
|
|
||||||
const cli = cac('electron-vite')
|
const cli = cac('electron-vite')
|
||||||
|
|
||||||
@ -25,20 +24,6 @@ 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 {
|
||||||
@ -67,7 +52,6 @@ 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
|
||||||
@ -75,36 +59,13 @@ 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: DevCLIOptions & GlobalCLIOptions) => {
|
.action(async (root: string, options: { remoteDebuggingPort?: string; rendererOnly: boolean } & 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)
|
||||||
|
|
||||||
@ -125,10 +86,6 @@ 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) {
|
||||||
@ -141,24 +98,11 @@ 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: PreviewCLIOptions & GlobalCLIOptions) => {
|
.action(async (root: string, options: { skipBuild?: boolean } & 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) {
|
||||||
@ -169,6 +113,6 @@ cli
|
|||||||
})
|
})
|
||||||
|
|
||||||
cli.help()
|
cli.help()
|
||||||
cli.version(version)
|
cli.version(require('../package.json').version)
|
||||||
|
|
||||||
cli.parse()
|
cli.parse()
|
||||||
|
|||||||
511
src/config.ts
511
src/config.ts
@ -1,124 +1,77 @@
|
|||||||
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 {
|
import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugins/electron'
|
||||||
electronMainConfigPresetPlugin,
|
|
||||||
electronMainConfigValidatorPlugin,
|
|
||||||
electronPreloadConfigPresetPlugin,
|
|
||||||
electronPreloadConfigValidatorPlugin,
|
|
||||||
electronRendererConfigPresetPlugin,
|
|
||||||
electronRendererConfigValidatorPlugin
|
|
||||||
} from './plugins/electron'
|
|
||||||
import assetPlugin from './plugins/asset'
|
import assetPlugin from './plugins/asset'
|
||||||
import workerPlugin from './plugins/worker'
|
import workerPlugin from './plugins/worker'
|
||||||
import importMetaPlugin from './plugins/importMeta'
|
import { isObject, dynamicImport } from './utils'
|
||||||
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
|
||||||
*
|
*
|
||||||
* @see https://vitejs.dev/config/
|
* https://cn.vitejs.dev/config/
|
||||||
*/
|
*/
|
||||||
main?: MainViteConfig
|
main?: ViteConfig & { configFile?: string | false }
|
||||||
/**
|
/**
|
||||||
* Vite config options for electron renderer process
|
* Vite config options for electron renderer process
|
||||||
*
|
*
|
||||||
* @see https://vitejs.dev/config/
|
* https://cn.vitejs.dev/config/
|
||||||
*/
|
*/
|
||||||
renderer?: RendererViteConfig
|
renderer?: ViteConfig & { configFile?: string | false }
|
||||||
/**
|
/**
|
||||||
* Vite config options for electron preload scripts
|
* Vite config options for electron preload files
|
||||||
*
|
*
|
||||||
* @see https://vitejs.dev/config/
|
* https://cn.vitejs.dev/config/
|
||||||
*/
|
*/
|
||||||
preload?: PreloadViteConfig
|
preload?: ViteConfig & { configFile?: string | false }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ElectronViteConfigFnObject = (env: ConfigEnv) => UserConfig
|
export interface UserConfigSchema {
|
||||||
export type ElectronViteConfigFnPromise = (env: ConfigEnv) => Promise<UserConfig>
|
/**
|
||||||
export type ElectronViteConfigFn = (env: ConfigEnv) => UserConfig | Promise<UserConfig>
|
* Vite config options for electron main process
|
||||||
|
*
|
||||||
|
* https://cn.vitejs.dev/config/
|
||||||
|
*/
|
||||||
|
main?: UserViteConfigExport
|
||||||
|
/**
|
||||||
|
* Vite config options for electron renderer process
|
||||||
|
*
|
||||||
|
* https://cn.vitejs.dev/config/
|
||||||
|
*/
|
||||||
|
renderer?: UserViteConfigExport
|
||||||
|
/**
|
||||||
|
* Vite config options for electron preload files
|
||||||
|
*
|
||||||
|
* https://cn.vitejs.dev/config/
|
||||||
|
*/
|
||||||
|
preload?: UserViteConfigExport
|
||||||
|
}
|
||||||
|
|
||||||
export type ElectronViteConfigExport =
|
export type InlineConfig = Omit<ViteConfig, 'base'> & {
|
||||||
UserConfig | Promise<UserConfig> | ElectronViteConfigFnObject | ElectronViteConfigFnPromise | ElectronViteConfigFn
|
configFile?: string | false
|
||||||
|
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.*`
|
||||||
@ -126,21 +79,10 @@ export type ElectronViteConfigExport =
|
|||||||
* 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: UserConfig): UserConfig
|
export function defineConfig(config: UserConfigExport): UserConfigExport {
|
||||||
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
|
||||||
@ -155,10 +97,11 @@ 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
|
||||||
|
|
||||||
const userConfig: UserConfig | undefined = {}
|
let userConfig: UserConfig | undefined
|
||||||
|
|
||||||
let configFileDependencies: string[] = []
|
let configFileDependencies: string[] = []
|
||||||
|
|
||||||
let { configFile } = config
|
let { configFile } = config
|
||||||
@ -167,7 +110,6 @@ export async function resolveConfig(
|
|||||||
mode,
|
mode,
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadResult = await loadConfigFromFile(
|
const loadResult = await loadConfigFromFile(
|
||||||
configEnv,
|
configEnv,
|
||||||
configFile,
|
configFile,
|
||||||
@ -175,30 +117,52 @@ 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
|
||||||
|
|
||||||
const { main, preload, renderer } = loadResult.config
|
if (loadResult.config.main) {
|
||||||
|
const mainViteConfig: ViteConfig = mergeConfig(loadResult.config.main, deepClone(config))
|
||||||
|
|
||||||
if (main) {
|
if (outDir) {
|
||||||
userConfig.main = await new MainConfigFactory(main, config, { outDir, root }).build()
|
resetOutDir(mainViteConfig, outDir, 'main')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preload) {
|
mergePlugins(mainViteConfig, [...electronMainVitePlugin({ root }), assetPlugin(), workerPlugin()])
|
||||||
userConfig.preload = await new PreloadConfigFactory(preload, config, { outDir, root }).build()
|
|
||||||
|
loadResult.config.main = mainViteConfig
|
||||||
|
loadResult.config.main.configFile = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (renderer) {
|
if (loadResult.config.preload) {
|
||||||
userConfig.renderer = await new RendererConfigFactory(renderer, config, { outDir, root }).build()
|
const preloadViteConfig: ViteConfig = mergeConfig(loadResult.config.preload, deepClone(config))
|
||||||
|
|
||||||
|
if (outDir) {
|
||||||
|
resetOutDir(preloadViteConfig, outDir, 'preload')
|
||||||
|
}
|
||||||
|
mergePlugins(preloadViteConfig, [...electronPreloadVitePlugin({ root }), assetPlugin()])
|
||||||
|
|
||||||
|
loadResult.config.preload = preloadViteConfig
|
||||||
|
loadResult.config.preload.configFile = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loadResult.config.renderer) {
|
||||||
|
const rendererViteConfig: ViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config))
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@ -213,109 +177,8 @@ export async function resolveConfig(
|
|||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class ConfigFactory<T extends MainViteConfig | PreloadViteConfig | RendererViteConfig> {
|
function deepClone<T>(data: T): T {
|
||||||
constructor(
|
return JSON.parse(JSON.stringify(data))
|
||||||
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 {
|
||||||
@ -330,37 +193,12 @@ function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): voi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveConfigDrivenPlugins(config: MainViteConfig | PreloadViteConfig): Promise<PluginOption[]> {
|
function mergePlugins(config: ViteConfig, plugins: Plugin[]): void {
|
||||||
const userPlugins = (await asyncFlatten(config.plugins || [])).filter(Boolean) as Plugin[]
|
const userPlugins = config.plugins || []
|
||||||
|
config.plugins = userPlugins.concat(plugins)
|
||||||
const configDrivenPlugins: PluginOption[] = []
|
|
||||||
|
|
||||||
const hasExternalizeDepsPlugin = userPlugins.some(p => p.name === 'vite:externalize-deps')
|
|
||||||
if (!hasExternalizeDepsPlugin) {
|
|
||||||
const externalOptions = config.build?.externalizeDeps ?? true
|
|
||||||
if (externalOptions) {
|
|
||||||
isOptions<ExternalOptions>(externalOptions)
|
|
||||||
? configDrivenPlugins.push(externalizeDepsPlugin(externalOptions))
|
|
||||||
: configDrivenPlugins.push(externalizeDepsPlugin())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasBytecodePlugin = userPlugins.some(p => p.name === 'vite:bytecode')
|
|
||||||
if (!hasBytecodePlugin) {
|
|
||||||
const bytecodeOptions = config.build?.bytecode
|
|
||||||
if (bytecodeOptions) {
|
|
||||||
isOptions<BytecodeOptions>(bytecodeOptions)
|
|
||||||
? configDrivenPlugins.push(bytecodePlugin(bytecodeOptions))
|
|
||||||
: configDrivenPlugins.push(bytecodePlugin())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return configDrivenPlugins
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOptions<T extends object>(value: boolean | T): value is T {
|
const CONFIG_FILE_NAME = 'electron.vite.config'
|
||||||
return typeof value === 'object' && value !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadConfigFromFile(
|
export async function loadConfigFromFile(
|
||||||
configEnv: ConfigEnv,
|
configEnv: ConfigEnv,
|
||||||
@ -377,7 +215,9 @@ 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 ? path.resolve(configFile) : findConfigFile(configRoot)
|
const resolvedPath = configFile
|
||||||
|
? path.resolve(configFile)
|
||||||
|
: findConfigFile(configRoot, ['js', 'ts', 'mjs', 'cjs', 'mts', 'cts'])
|
||||||
|
|
||||||
if (!resolvedPath) {
|
if (!resolvedPath) {
|
||||||
return {
|
return {
|
||||||
@ -387,33 +227,186 @@ export async function loadConfigFromFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isESM = isFilePathESM(resolvedPath)
|
// electron does not support adding type: "module" to package.json
|
||||||
|
let isESM = false
|
||||||
|
if (/\.m[jt]s$/.test(resolvedPath) || resolvedPath.endsWith('.ts')) {
|
||||||
|
isESM = true
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { code, dependencies } = await bundleConfigFile(resolvedPath, isESM)
|
const bundled = await bundleConfigFile(resolvedPath, isESM)
|
||||||
const configExport = await loadConfigFormBundledFile<ElectronViteConfigExport>(resolvedPath, code, isESM)
|
const userConfig = await loadConfigFormBundledFile(configRoot, resolvedPath, bundled.code, isESM)
|
||||||
|
|
||||||
const config = await (typeof configExport === 'function' ? configExport(configEnv) : configExport)
|
const config = await (typeof userConfig === 'function' ? userConfig(configEnv) : userConfig)
|
||||||
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`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ignoreConfigWarning) {
|
const configRequired: string[] = []
|
||||||
const missingFields = ['main', 'renderer', 'preload'].filter(field => !config[field])
|
|
||||||
if (missingFields.length > 0) {
|
let mainConfig
|
||||||
createLogger(logLevel).warn(
|
if (config.main) {
|
||||||
`${colors.yellow(colors.bold('(!)'))} ${colors.yellow(`${missingFields.join(' and ')} config is missing`)}\n`
|
const mainViteConfig = config.main
|
||||||
)
|
mainConfig = await (typeof mainViteConfig === 'function' ? mainViteConfig(configEnv) : mainViteConfig)
|
||||||
|
if (!isObject(mainConfig)) {
|
||||||
|
throw new Error(`main config must export or return an object`)
|
||||||
}
|
}
|
||||||
|
} 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: {
|
||||||
dependencies
|
main: mainConfig,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
150
src/electron.ts
150
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 ChildProcess, spawn } from 'node:child_process'
|
import { type ChildProcessWithoutNullStreams, spawn } from 'node:child_process'
|
||||||
import { loadPackageData } from './utils'
|
import { type Logger } from 'vite'
|
||||||
|
|
||||||
const _require = createRequire(import.meta.url)
|
const _require = createRequire(import.meta.url)
|
||||||
|
|
||||||
const ensureElectronEntryFile = (root = process.cwd()): void => {
|
const ensureElectronEntryFile = (root = process.cwd()): void => {
|
||||||
if (process.env.ELECTRON_ENTRY) return
|
const pkg = path.join(root, 'package.json')
|
||||||
const pkg = loadPackageData()
|
if (fs.existsSync(pkg)) {
|
||||||
if (pkg) {
|
const main = require(pkg).main
|
||||||
if (!pkg.main) {
|
if (!main) {
|
||||||
throw new Error('No entry point found for electron app, please add a "main" field to package.json')
|
throw new Error('No entry point found for electron app, please add a "main" field to package.json')
|
||||||
} else {
|
} else {
|
||||||
const entryPath = path.resolve(root, pkg.main)
|
const entryPath = path.resolve(root, 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,16 +36,6 @@ 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) {
|
||||||
@ -69,29 +59,21 @@ 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',
|
'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]
|
||||||
@ -105,29 +87,21 @@ 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',
|
'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]
|
||||||
@ -137,64 +111,26 @@ export function getElectronChromeTarget(): string {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startElectron(root: string | undefined): ChildProcess {
|
export function startElectron(root: string | undefined, logger: Logger): ChildProcessWithoutNullStreams {
|
||||||
ensureElectronEntryFile(root)
|
ensureElectronEntryFile(root)
|
||||||
|
|
||||||
const electronPath = getElectronPath()
|
const electronPath = getElectronPath()
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV_ELECTRON_VITE === 'development'
|
const inspect = !!process.env.VSCODE_INSPECTOR_OPTIONS
|
||||||
|
|
||||||
const args: string[] = process.env.ELECTRON_CLI_ARGS ? JSON.parse(process.env.ELECTRON_CLI_ARGS) : []
|
const args: string[] = []
|
||||||
|
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}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!process.env.V8_INSPECTOR_PORT && isDev) {
|
const ps = spawn(electronPath, ['.'].concat(args))
|
||||||
args.push(`--inspect=${process.env.V8_INSPECTOR_PORT}`)
|
ps.stdout.on('data', chunk => {
|
||||||
}
|
!inspect && chunk.toString().trim() && logger.info(chunk.toString())
|
||||||
|
})
|
||||||
if (!!process.env.V8_INSPECTOR_BRK_PORT && isDev) {
|
ps.stderr.on('data', chunk => {
|
||||||
args.push(`--inspect-brk=${process.env.V8_INSPECTOR_BRK_PORT}`)
|
!inspect && chunk.toString().trim() && logger.error(chunk.toString())
|
||||||
}
|
})
|
||||||
|
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, mergeConfig } from 'vite'
|
export { type LogLevel, createLogger, splitVendorChunkPlugin, splitVendorChunk } 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
130
src/load.ts
@ -1,130 +0,0 @@
|
|||||||
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,17 +1,50 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import fs from 'node:fs/promises'
|
import fs from 'node:fs/promises'
|
||||||
import { type Plugin, type Environment, normalizePath } from 'vite'
|
import type { SourceMapInput } from 'rollup'
|
||||||
|
import { type Plugin, normalizePath } from 'vite'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { cleanUrl, getHash, toRelativePath } from '../utils'
|
import { cleanUrl, parseRequest, getHash, toRelativePath } from '../utils'
|
||||||
import { supportImportMetaPaths } from '../electron'
|
|
||||||
|
|
||||||
const nodeAssetRE = /__VITE_NODE_ASSET__([\w$]+)__/g
|
interface AssetResolved {
|
||||||
|
type: 'asset' | 'native' | 'wasm'
|
||||||
|
file: string
|
||||||
|
query: Record<string, string> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAsset(id: string): AssetResolved | null {
|
||||||
|
const file = cleanUrl(id)
|
||||||
|
const query = parseRequest(id)
|
||||||
|
|
||||||
|
if (query && typeof query.asset === 'string') {
|
||||||
|
return {
|
||||||
|
type: 'asset',
|
||||||
|
file,
|
||||||
|
query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.endsWith('.node')) {
|
||||||
|
return {
|
||||||
|
type: 'native',
|
||||||
|
file,
|
||||||
|
query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.endsWith('.wasm?loader')) {
|
||||||
|
return {
|
||||||
|
type: 'wasm',
|
||||||
|
file,
|
||||||
|
query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeAssetRE = /__VITE_NODE_ASSET__([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 = `
|
||||||
@ -25,18 +58,24 @@ 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 {
|
||||||
const isImportMetaPathSupported = supportImportMetaPaths()
|
let sourcemap: boolean | 'inline' | 'hidden' = false
|
||||||
|
let publicDir = ''
|
||||||
|
let outDir = ''
|
||||||
|
const publicAssetPathCache = new Map<string, string>()
|
||||||
|
const assetCache = new Map<string, string>()
|
||||||
return {
|
return {
|
||||||
name: 'vite:node-asset',
|
name: 'vite:node-asset',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
buildStart(): void {
|
buildStart(): void {
|
||||||
publicAssetPathCache.set(this.environment, new Map())
|
publicAssetPathCache.clear()
|
||||||
assetCache.set(this.environment, new Map())
|
assetCache.clear()
|
||||||
|
},
|
||||||
|
configResolved(config): void {
|
||||||
|
sourcemap = config.build.sourcemap
|
||||||
|
publicDir = normalizePath(config.publicDir)
|
||||||
|
outDir = normalizePath(path.resolve(config.root, config.build.outDir))
|
||||||
},
|
},
|
||||||
resolveId(id): string | void {
|
resolveId(id): string | void {
|
||||||
if (id === wasmHelperId) {
|
if (id === wasmHelperId) {
|
||||||
@ -48,26 +87,21 @@ export default function assetPlugin(): Plugin {
|
|||||||
return wasmHelperCode
|
return wasmHelperCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.startsWith('\0') || !assetImportRE.test(id)) {
|
const assetResolved = resolveAsset(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)
|
||||||
const publicAssetPathMap = publicAssetPathCache.get(this.environment)!
|
if (!publicAssetPathCache.get(hash)) {
|
||||||
if (!publicAssetPathMap.get(hash)) {
|
publicAssetPathCache.set(hash, file)
|
||||||
publicAssetPathMap.set(hash, file)
|
|
||||||
}
|
}
|
||||||
referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__`
|
referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__`
|
||||||
} else {
|
} else {
|
||||||
const cache = assetCache.get(this.environment)!
|
const cached = assetCache.get(file)
|
||||||
const cached = cache.get(file)
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
referenceId = cached
|
referenceId = cached
|
||||||
} else {
|
} else {
|
||||||
@ -75,41 +109,41 @@ 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 as unknown as Uint8Array
|
source
|
||||||
})
|
})
|
||||||
referenceId = `__VITE_NODE_ASSET__${hash}__`
|
referenceId = `__VITE_NODE_ASSET__${hash}__`
|
||||||
cache.set(file, referenceId)
|
assetCache.set(file, referenceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetRE.test(id)) {
|
if (assetResolved.type === 'asset') {
|
||||||
const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname'
|
if (assetResolved.query && typeof assetResolved.query.asarUnpack === 'string') {
|
||||||
if (assetUnpackRE.test(id)) {
|
|
||||||
return `
|
return `
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
export default join(${dirnameExpr}, ${referenceId}).replace('app.asar', 'app.asar.unpacked')`
|
export default join(__dirname, ${referenceId}).replace('app.asar', 'app.asar.unpacked')`
|
||||||
} else {
|
} else {
|
||||||
return `
|
return `
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
export default join(${dirnameExpr}, ${referenceId})`
|
export default join(__dirname, ${referenceId})`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.endsWith('.node')) {
|
if (assetResolved.type === 'native') {
|
||||||
return `export default require(${referenceId})`
|
return `export default require(${referenceId})`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.endsWith('.wasm?loader')) {
|
if (assetResolved.type === 'wasm') {
|
||||||
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, { sourcemap, dir }) {
|
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null {
|
||||||
let match: RegExpExecArray | null
|
let match: RegExpExecArray | null
|
||||||
let s: MagicString | undefined
|
let s: MagicString | undefined
|
||||||
|
|
||||||
nodeAssetRE.lastIndex = 0
|
nodeAssetRE.lastIndex = 0
|
||||||
|
if (code.match(nodeAssetRE)) {
|
||||||
while ((match = nodeAssetRE.exec(code))) {
|
while ((match = nodeAssetRE.exec(code))) {
|
||||||
s ||= new MagicString(code)
|
s ||= new MagicString(code)
|
||||||
const [full, hash] = match
|
const [full, hash] = match
|
||||||
@ -120,29 +154,30 @@ export default function assetPlugin(): Plugin {
|
|||||||
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: 'boundary' }) : null
|
map: sourcemap ? s.generateMap({ hires: true }) : null
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,89 +1,155 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { randomBytes } from 'node:crypto'
|
import fs from 'node:fs'
|
||||||
|
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 Rolldown, normalizePath } from 'vite'
|
import { type Plugin, type ResolvedConfig, normalizePath, createFilter } from 'vite'
|
||||||
import type * as Babel from '@babel/core'
|
import * as babel from '@babel/core'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { toRelativePath, resolveBuildOutputs } from '../utils'
|
import type { SourceMapInput, OutputChunk } from 'rollup'
|
||||||
import type { BytecodeCompiler, BytecodeTarget } from '../bytecodeCompiler'
|
import { getElectronPath } from '../electron'
|
||||||
|
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 getBytecodeCompiler(plugins: readonly Plugin[]): BytecodeCompiler | undefined {
|
function getBytecodeCompilerPath(): string {
|
||||||
const plugin = plugins.find(plugin => plugin.name === 'vite:electron-bytecode-build')
|
return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', 'electron-bytecode.js')
|
||||||
return (plugin?.api as { compiler?: BytecodeCompiler } | undefined)?.compiler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Versioned envelope: magic/version (8), UTF-16 source length (4), module ID (16), V8 cache.
|
function compileToBytecode(code: string): Promise<Buffer> {
|
||||||
// Keep our metadata separate from the private V8 cache header.
|
return new Promise((resolve, reject) => {
|
||||||
async function compileToBytecode(compiler: BytecodeCompiler, target: BytecodeTarget, code: string): Promise<Buffer> {
|
let data = Buffer.from([])
|
||||||
const id = randomBytes(16)
|
|
||||||
// Reserve enough source space for an untruncated, per-artifact placeholder identity.
|
const electronPath = getElectronPath()
|
||||||
const source = code + '\n/*' + id.toString('hex') + '*/'
|
const bytecodePath = getBytecodeCompilerPath()
|
||||||
const result = await compiler.compile(target, source)
|
|
||||||
const header = Buffer.alloc(28)
|
const proc = spawn(electronPath, [bytecodePath], {
|
||||||
header.write('EVBC0001', 0, 'ascii')
|
env: { ELECTRON_RUN_AS_NODE: '1' },
|
||||||
header.writeUInt32LE(result.sourceLength, 8)
|
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
|
||||||
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("node:fs");',
|
`const fs = require("fs");`,
|
||||||
'const path = require("node:path");',
|
`const path = require("path");`,
|
||||||
'const vm = require("node:vm");',
|
`const vm = require("vm");`,
|
||||||
'const v8 = require("node:v8");',
|
`const v8 = require("v8");`,
|
||||||
'const Module = require("node:module");',
|
`const Module = require("module");`,
|
||||||
'v8.setFlagsFromString("--no-lazy");',
|
`v8.setFlagsFromString("--no-lazy");`,
|
||||||
'v8.setFlagsFromString("--no-flush-bytecode");',
|
`v8.setFlagsFromString("--no-flush-bytecode");`,
|
||||||
'const params = ["exports", "require", "module", "__filename", "__dirname"];',
|
`const FLAG_HASH_OFFSET = 12;`,
|
||||||
'Module._extensions[".jsc"] = Module._extensions[".cjsc"] = function (module, filename) {',
|
`const SOURCE_HASH_OFFSET = 8;`,
|
||||||
' const data = fs.readFileSync(filename);',
|
`let dummyBytecode;`,
|
||||||
' if (data.length <= 28 || data.toString("ascii", 0, 8) !== "EVBC0001") {',
|
`function setFlagHashHeader(bytecodeBuffer) {`,
|
||||||
' throw new Error("Invalid electron-vite bytecode format; rebuild the application: " + filename);',
|
` if (!dummyBytecode) {`,
|
||||||
' }',
|
` const script = new vm.Script("", {`,
|
||||||
' const length = data.readUInt32LE(8);',
|
` produceCachedData: true`,
|
||||||
' const tag = "/*" + data.subarray(12, 28).toString("hex") + "*/";',
|
` });`,
|
||||||
' if (length < tag.length || length > 0x1fffffff) {',
|
` dummyBytecode = script.createCachedData();`,
|
||||||
' 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);',
|
`};`,
|
||||||
' const compiledWrapper = vm.compileFunction(placeholder, params, {',
|
`function getSourceHashHeader(bytecodeBuffer) {`,
|
||||||
' filename,',
|
` return bytecodeBuffer.slice(SOURCE_HASH_OFFSET, SOURCE_HASH_OFFSET + 4);`,
|
||||||
' cachedData: data.subarray(28)',
|
`};`,
|
||||||
' });',
|
`function buffer2Number(buffer) {`,
|
||||||
' if (compiledWrapper.cachedDataRejected) {',
|
` let ret = 0;`,
|
||||||
' throw new Error("Invalid or incompatible cached data (cachedDataRejected): " + filename);',
|
` ret |= buffer[3] << 24;`,
|
||||||
' }',
|
` ret |= buffer[2] << 16;`,
|
||||||
' const require = function (id) { return module.require(id); };',
|
` ret |= buffer[1] << 8;`,
|
||||||
' require.resolve = function (request, options) {',
|
` ret |= buffer[0];`,
|
||||||
' return Module._resolveFilename(request, module, false, options);',
|
` return ret;`,
|
||||||
' };',
|
`};`,
|
||||||
' if (process.mainModule) require.main = process.mainModule;',
|
`Module._extensions[".jsc"] = function (module, filename) {`,
|
||||||
' require.extensions = Module._extensions;',
|
` const bytecodeBuffer = fs.readFileSync(filename);`,
|
||||||
' require.cache = Module._cache;',
|
` if (!Buffer.isBuffer(bytecodeBuffer)) {`,
|
||||||
' return compiledWrapper.call(module.exports, module.exports, require, module, filename, path.dirname(filename));',
|
` throw new Error("BytecodeBuffer must be a buffer object.");`,
|
||||||
'};'
|
` }`,
|
||||||
|
` setFlagHashHeader(bytecodeBuffer);`,
|
||||||
|
` const length = buffer2Number(getSourceHashHeader(bytecodeBuffer));`,
|
||||||
|
` let dummyCode = "";`,
|
||||||
|
` if (length > 1) {`,
|
||||||
|
` dummyCode = "\\"" + "\\u200b".repeat(length - 2) + "\\"";`,
|
||||||
|
` }`,
|
||||||
|
` const script = new vm.Script(dummyCode, {`,
|
||||||
|
` filename: filename,`,
|
||||||
|
` lineOffset: 0,`,
|
||||||
|
` displayErrors: true,`,
|
||||||
|
` cachedData: bytecodeBuffer`,
|
||||||
|
` });`,
|
||||||
|
` if (script.cachedDataRejected) {`,
|
||||||
|
` throw new Error("Invalid or incompatible cached data (cachedDataRejected)");`,
|
||||||
|
` }`,
|
||||||
|
` const require = function (id) {`,
|
||||||
|
` return module.require(id);`,
|
||||||
|
` };`,
|
||||||
|
` require.resolve = function (request, options) {`,
|
||||||
|
` return Module._resolveFilename(request, module, false, options);`,
|
||||||
|
` };`,
|
||||||
|
` if (process.mainModule) {`,
|
||||||
|
` require.main = process.mainModule;`,
|
||||||
|
` }`,
|
||||||
|
` require.extensions = Module._extensions;`,
|
||||||
|
` require.cache = Module._cache;`,
|
||||||
|
` const compiledWrapper = script.runInThisContext({`,
|
||||||
|
` filename: filename,`,
|
||||||
|
` lineOffset: 0,`,
|
||||||
|
` columnOffset: 0,`,
|
||||||
|
` displayErrors: true`,
|
||||||
|
` });`,
|
||||||
|
` const dirname = path.dirname(filename);`,
|
||||||
|
` const args = [module.exports, require, module, filename, dirname, process, global];`,
|
||||||
|
` return compiledWrapper.apply(module.exports, args);`,
|
||||||
|
`};`
|
||||||
]
|
]
|
||||||
|
|
||||||
const bytecodeChunkExtensionRE = /.(jsc|cjsc)$/
|
|
||||||
|
|
||||||
export interface BytecodeOptions {
|
export interface BytecodeOptions {
|
||||||
chunkAlias?: string | string[] | RegExp
|
chunkAlias?: string | string[]
|
||||||
transformArrowFunctions?: boolean
|
transformArrowFunctions?: boolean
|
||||||
removeBundleJS?: boolean
|
removeBundleJS?: boolean
|
||||||
protectedStrings?: string[]
|
protectedStrings?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile source code to v8 bytecode.
|
* Compile to v8 bytecode to protect source code.
|
||||||
*
|
|
||||||
* @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') {
|
||||||
@ -91,152 +157,164 @@ 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 = chunkAlias instanceof RegExp ? chunkAlias : Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
|
const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
|
||||||
|
|
||||||
|
const filter = createFilter(/\.(m?[jt]s|[jt]sx)$/)
|
||||||
|
|
||||||
|
const escapeRegExpString = (str: string): string => {
|
||||||
|
return str
|
||||||
|
.replace(/\\/g, '\\\\\\\\')
|
||||||
|
.replace(/[|{}()[\]^$+*?.]/g, '\\$&')
|
||||||
|
.replace(/-/g, '\\u002d')
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformAllChunks = _chunkAlias.length === 0
|
||||||
const isBytecodeChunk = (chunkName: string): boolean => {
|
const isBytecodeChunk = (chunkName: string): boolean => {
|
||||||
if (_chunkAlias instanceof RegExp) {
|
return transformAllChunks || _chunkAlias.some(alias => alias === chunkName)
|
||||||
_chunkAlias.lastIndex = 0
|
|
||||||
return _chunkAlias.test(chunkName)
|
|
||||||
}
|
|
||||||
return _chunkAlias.length === 0 || _chunkAlias.some(alias => alias === chunkName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugins: Babel.PluginItem[] = []
|
const _transform = (code: string): string => {
|
||||||
|
const re = babel.transform(code, {
|
||||||
if (transformArrowFunctions) {
|
plugins: ['@babel/plugin-transform-arrow-functions']
|
||||||
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.cjs'
|
const bytecodeModuleLoader = 'bytecode-loader.js'
|
||||||
|
|
||||||
let supported = false
|
let config: ResolvedConfig
|
||||||
let isPreload = false
|
let useInRenderer = false
|
||||||
let bytecodeCompiler: BytecodeCompiler | undefined
|
let bytecodeRequired = false
|
||||||
|
let bytecodeFiles: { name: string; size: number }[] = []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'vite:bytecode',
|
name: 'vite:bytecode',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
configResolved(config): void {
|
configResolved(resolvedConfig): void {
|
||||||
bytecodeCompiler = getBytecodeCompiler(config.plugins)
|
config = resolvedConfig
|
||||||
if (supported) {
|
useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config')
|
||||||
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 does not support renderer.'))
|
config.logger.warn(colors.yellow('bytecodePlugin is not support renderers'))
|
||||||
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
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderChunk(code, chunk, { sourcemap }): { code: string; map?: Rolldown.SourceMapInput } | null {
|
transform(code, id): void | { code: string; map: SourceMapInput } {
|
||||||
if (supported && isBytecodeChunk(chunk.name) && shouldTransformBytecodeChunk) {
|
if (protectedStrings.length === 0 || !filter(id)) return
|
||||||
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
|
||||||
},
|
},
|
||||||
async generateBundle(_, output): Promise<void> {
|
generateBundle(): void {
|
||||||
if (!supported) {
|
if (!useInRenderer && bytecodeRequired) {
|
||||||
return
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
source: bytecodeModuleLoaderCode.join('\n') + '\n',
|
||||||
|
name: 'Bytecode Loader File',
|
||||||
|
fileName: bytecodeModuleLoader
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const _chunks = Object.values(output)
|
},
|
||||||
const chunks = _chunks.filter(
|
async writeBundle(options, output): Promise<void> {
|
||||||
chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name)
|
if (useInRenderer || !bytecodeRequired) {
|
||||||
) as Rolldown.OutputChunk[]
|
|
||||||
|
|
||||||
if (chunks.length === 0) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const outDir = options.dir!
|
||||||
|
|
||||||
|
bytecodeFiles = []
|
||||||
|
|
||||||
|
const bundles = Object.keys(output)
|
||||||
|
const chunks = Object.values(output).filter(
|
||||||
|
chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name) && chunk.fileName !== bytecodeModuleLoader
|
||||||
|
) as OutputChunk[]
|
||||||
const bytecodeChunks = chunks.map(chunk => chunk.fileName)
|
const bytecodeChunks = chunks.map(chunk => chunk.fileName)
|
||||||
const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName))
|
const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName))
|
||||||
|
|
||||||
const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|')
|
const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|')
|
||||||
const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null
|
const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null
|
||||||
|
|
||||||
|
const keepBundle = (chunkFileName: string): void => {
|
||||||
|
const newFileName = path.resolve(path.dirname(chunkFileName), `_${path.basename(chunkFileName)}`)
|
||||||
|
fs.renameSync(chunkFileName, newFileName)
|
||||||
|
}
|
||||||
|
|
||||||
const getBytecodeLoaderBlock = (chunkFileName: string): string => {
|
const getBytecodeLoaderBlock = (chunkFileName: string): string => {
|
||||||
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
|
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytecodeChunkCount = 0
|
await Promise.all(
|
||||||
|
bundles.map(async name => {
|
||||||
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]
|
const chunk = output[name]
|
||||||
if (chunk.type === 'chunk') {
|
if (chunk.type === 'chunk') {
|
||||||
let _code = chunk.code
|
let _code = chunk.code
|
||||||
if (bytecodeRE) {
|
if (bytecodeRE && _code.match(bytecodeRE)) {
|
||||||
let match: RegExpExecArray | null
|
let match: RegExpExecArray | null
|
||||||
let s: MagicString | undefined
|
const s = new MagicString(_code)
|
||||||
while ((match = bytecodeRE.exec(_code))) {
|
while ((match = bytecodeRE.exec(_code))) {
|
||||||
s ||= new MagicString(_code)
|
|
||||||
const [prefix, chunkName] = match
|
const [prefix, chunkName] = match
|
||||||
const len = prefix.length + chunkName.length
|
const len = prefix.length + chunkName.length
|
||||||
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
|
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
|
||||||
contentOnly: true
|
contentOnly: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (s) {
|
|
||||||
_code = s.toString()
|
_code = s.toString()
|
||||||
}
|
}
|
||||||
}
|
const chunkFileName = path.resolve(outDir, name)
|
||||||
if (bytecodeChunks.includes(name)) {
|
if (bytecodeChunks.includes(name)) {
|
||||||
const bytecodeBuffer = await compileToBytecode(bytecodeCompiler, isPreload ? 'preload' : 'main', _code)
|
const bytecodeBuffer = await compileToBytecode(_code)
|
||||||
this.emitFile({
|
fs.writeFileSync(path.resolve(outDir, name + 'c'), bytecodeBuffer)
|
||||||
type: 'asset',
|
|
||||||
fileName: name + 'c',
|
|
||||||
source: bytecodeBuffer
|
|
||||||
})
|
|
||||||
if (!removeBundleJS) {
|
|
||||||
this.emitFile({
|
|
||||||
type: 'asset',
|
|
||||||
fileName: '_' + chunk.fileName,
|
|
||||||
source: chunk.code
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (chunk.isEntry) {
|
if (chunk.isEntry) {
|
||||||
|
if (!removeBundleJS) {
|
||||||
|
keepBundle(chunkFileName)
|
||||||
|
}
|
||||||
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
|
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
|
||||||
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
|
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
|
||||||
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
|
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
|
||||||
chunk.code = code
|
fs.writeFileSync(chunkFileName, code)
|
||||||
} else {
|
} else {
|
||||||
delete output[chunk.fileName]
|
if (removeBundleJS) {
|
||||||
|
fs.unlinkSync(chunkFileName)
|
||||||
|
} else {
|
||||||
|
keepBundle(chunkFileName)
|
||||||
}
|
}
|
||||||
bytecodeChunkCount += 1
|
}
|
||||||
|
bytecodeFiles.push({ name: name + 'c', size: bytecodeBuffer.length })
|
||||||
} else {
|
} else {
|
||||||
if (chunk.isEntry) {
|
if (chunk.isEntry) {
|
||||||
let hasBytecodeMoudle = false
|
let hasBytecodeMoudle = false
|
||||||
@ -247,113 +325,40 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
const moduleInfo = this.getModuleInfo(moduleId)
|
const moduleInfo = this.getModuleInfo(moduleId)
|
||||||
if (moduleInfo) {
|
if (moduleInfo && !moduleInfo.isExternal) {
|
||||||
const { importers, dynamicImporters } = moduleInfo
|
const { importers, dynamicImporters } = moduleInfo
|
||||||
for (const importerId of importers) idsToHandle.add(importerId)
|
for (const importerId of importers) idsToHandle.add(importerId)
|
||||||
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
|
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasBytecodeMoudle) {
|
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
|
||||||
const loader = getBytecodeLoaderBlock(chunk.fileName)
|
_code = hasBytecodeMoudle ? _code.replace(useStrict, `${useStrict}\n${bytecodeLoaderBlock}`) : _code
|
||||||
const strictRE = /^(#![^\n]*\n)?(["'])use strict\2;/
|
}
|
||||||
_code = strictRE.test(_code)
|
fs.writeFileSync(chunkFileName, _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({
|
|
||||||
type: 'asset',
|
|
||||||
source: bytecodeModuleLoaderCode.join('\n') + '\n',
|
|
||||||
name: 'Bytecode Loader File',
|
|
||||||
fileName: bytecodeModuleLoader
|
|
||||||
})
|
})
|
||||||
}
|
)
|
||||||
},
|
},
|
||||||
writeBundle(_, output): void {
|
closeBundle(): void {
|
||||||
if (supported) {
|
if (!useInRenderer) {
|
||||||
const bytecodeChunkCount = Object.keys(output).filter(chunk => bytecodeChunkExtensionRE.test(chunk)).length
|
const chunkLimit = config.build.chunkSizeWarningLimit
|
||||||
this.environment.logger.info(`${colors.green(`✓`)} ${bytecodeChunkCount} chunks compiled into bytecode.`)
|
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
|
||||||
interface ProtectStringsPluginState extends Babel.PluginPass {
|
})
|
||||||
opts: { protectedStrings: Set<string> }
|
bytecodeFiles.forEach(file => {
|
||||||
}
|
const kbs = file.size / 1000
|
||||||
|
config.logger.info(
|
||||||
function protectStringsPlugin(api: typeof Babel & Babel.ConfigAPI): Babel.PluginObj<ProtectStringsPluginState> {
|
`${colors.gray(colors.white(colors.dim(outDir)))}${colors.green(file.name.padEnd(longest + 2))} ${
|
||||||
const { types: t } = api
|
kbs > chunkLimit ? colors.yellow(`${kbs.toFixed(2)} kB`) : colors.dim(`${kbs.toFixed(2)} kB`)
|
||||||
|
}`
|
||||||
function createFromCharCodeFunction(value: string): Babel.types.CallExpression {
|
)
|
||||||
const charCodes = Array.from(value).map(s => s.charCodeAt(0))
|
})
|
||||||
const charCodeLiterals = charCodes.map(code => t.numericLiteral(code))
|
bytecodeFiles = []
|
||||||
|
|
||||||
// String.fromCharCode
|
|
||||||
const memberExpression = t.memberExpression(t.identifier('String'), t.identifier('fromCharCode'))
|
|
||||||
// String.fromCharCode(...arr)
|
|
||||||
const callExpression = t.callExpression(memberExpression, [t.spreadElement(t.identifier('arr'))])
|
|
||||||
// return String.fromCharCode(...arr)
|
|
||||||
const returnStatement = t.returnStatement(callExpression)
|
|
||||||
// function (arr) { return ... }
|
|
||||||
const functionExpression = t.functionExpression(null, [t.identifier('arr')], t.blockStatement([returnStatement]))
|
|
||||||
|
|
||||||
// (function(...) { ... })([x, x, x])
|
|
||||||
return t.callExpression(functionExpression, [t.arrayExpression(charCodeLiterals)])
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'protect-strings-plugin',
|
|
||||||
visitor: {
|
|
||||||
StringLiteral(path, state) {
|
|
||||||
// obj['property']
|
|
||||||
if (path.parentPath.isMemberExpression({ property: path.node, computed: true })) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// { 'key': value }
|
|
||||||
if (path.parentPath.isObjectProperty({ key: path.node, computed: false })) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// require('fs')
|
|
||||||
if (
|
|
||||||
path.parentPath.isCallExpression() &&
|
|
||||||
t.isIdentifier(path.parentPath.node.callee) &&
|
|
||||||
path.parentPath.node.callee.name === 'require' &&
|
|
||||||
path.parentPath.node.arguments[0] === path.node
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only CommonJS is supported, import declaration and export declaration checks are ignored
|
|
||||||
|
|
||||||
const { value } = path.node
|
|
||||||
if (state.opts.protectedStrings.has(value)) {
|
|
||||||
path.replaceWith(createFromCharCodeFunction(value))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TemplateLiteral(path, state) {
|
|
||||||
// Must be a pure static template literal
|
|
||||||
// expressions must be empty (no ${variables})
|
|
||||||
// quasis must have only one element (meaning the entire string is a single static part).
|
|
||||||
if (path.node.expressions.length > 0 || path.node.quasis.length !== 1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the raw value of the template literal
|
|
||||||
// path.node.quasis[0].value.raw is used to get the raw string, including escape sequences
|
|
||||||
// path.node.quasis[0].value.cooked is used to get the processed/cooked string (with escape sequences handled)
|
|
||||||
const value = path.node.quasis[0].value.cooked
|
|
||||||
if (value && state.opts.protectedStrings.has(value)) {
|
|
||||||
path.replaceWith(createFromCharCodeFunction(value))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,15 +2,14 @@ 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, type UserConfig, mergeConfig, normalizePath } from 'vite'
|
import { type Plugin, mergeConfig, normalizePath } from 'vite'
|
||||||
import { getElectronNodeTarget, getElectronChromeTarget, supportESM } from '../electron'
|
import { getElectronNodeTarget, getElectronChromeTarget } from '../electron'
|
||||||
import { loadPackageData, resolveBuildOutputs } from '../utils'
|
|
||||||
|
|
||||||
export interface ElectronPluginOptions {
|
export interface ElectronPluginOptions {
|
||||||
root?: string
|
root?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function findLibEntry(root: string, scope: string): string | undefined {
|
function findLibEntry(root: string, scope: string): string {
|
||||||
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}`)
|
||||||
@ -19,7 +18,7 @@ function findLibEntry(root: string, scope: string): string | undefined {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function findInput(root: string, scope = 'renderer'): string {
|
function findInput(root: string, scope = 'renderer'): string {
|
||||||
@ -38,9 +37,10 @@ function processEnvDefine(): Record<string, string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
|
export function electronMainVitePlugin(options?: ElectronPluginOptions): Plugin[] {
|
||||||
return {
|
return [
|
||||||
name: 'vite:electron-main-config-preset',
|
{
|
||||||
|
name: 'vite:electron-main-preset-config',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
config(config): void {
|
config(config): void {
|
||||||
@ -48,41 +48,39 @@ export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions):
|
|||||||
|
|
||||||
const nodeTarget = getElectronNodeTarget()
|
const nodeTarget = getElectronNodeTarget()
|
||||||
|
|
||||||
const pkg = loadPackageData() || { type: 'commonjs' }
|
|
||||||
|
|
||||||
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
|
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
|
resolve: {
|
||||||
|
browserField: false,
|
||||||
|
mainFields: ['module', 'jsnext:main', 'jsnext'],
|
||||||
|
conditions: ['node']
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: path.resolve(root, 'out', 'main'),
|
outDir: path.resolve(root, 'out', 'main'),
|
||||||
target: nodeTarget,
|
target: nodeTarget,
|
||||||
assetsDir: 'chunks',
|
assetsDir: 'chunks',
|
||||||
rolldownOptions: {
|
rollupOptions: {
|
||||||
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
external: ['electron', ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
||||||
output: {}
|
output: {
|
||||||
|
entryFileNames: '[name].js'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
reportCompressedSize: false,
|
reportCompressedSize: false,
|
||||||
minify: false
|
minify: false
|
||||||
}
|
}
|
||||||
} satisfies UserConfig
|
}
|
||||||
|
|
||||||
const build = config.build || {}
|
const build = config.build || {}
|
||||||
const rolldownOptions = build.rolldownOptions || {}
|
const rollupOptions = build.rollupOptions || {}
|
||||||
if (!rolldownOptions.input) {
|
if (!rollupOptions.input) {
|
||||||
const libOptions = build.lib
|
|
||||||
const outputOptions = rolldownOptions.output
|
|
||||||
defaultConfig.build['lib'] = {
|
defaultConfig.build['lib'] = {
|
||||||
entry: findLibEntry(root, 'main'),
|
entry: findLibEntry(root, 'main'),
|
||||||
formats:
|
formats: ['cjs']
|
||||||
libOptions && libOptions.formats && libOptions.formats.length > 0
|
|
||||||
? []
|
|
||||||
: [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
defaultConfig.build.rolldownOptions.output['format'] = format
|
defaultConfig.build.rollupOptions.output['format'] = 'cjs'
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig.build.rolldownOptions.output['assetFileNames'] = path.posix.join(
|
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
|
||||||
build.assetsDir || defaultConfig.build.assetsDir,
|
build.assetsDir || defaultConfig.build.assetsDir,
|
||||||
'[name]-[hash].[ext]'
|
'[name]-[hash].[ext]'
|
||||||
)
|
)
|
||||||
@ -90,81 +88,68 @@ export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions):
|
|||||||
const buildConfig = mergeConfig(defaultConfig.build, build)
|
const buildConfig = mergeConfig(defaultConfig.build, build)
|
||||||
config.build = buildConfig
|
config.build = buildConfig
|
||||||
|
|
||||||
|
config.resolve = mergeConfig(defaultConfig.resolve, config.resolve || {})
|
||||||
|
|
||||||
config.define = config.define || {}
|
config.define = config.define || {}
|
||||||
config.define = { ...processEnvDefine(), ...config.define }
|
config.define = { ...processEnvDefine(), ...config.define }
|
||||||
|
|
||||||
config.envPrefix = config.envPrefix || ['MAIN_VITE_', 'VITE_']
|
config.envPrefix = config.envPrefix || 'MAIN_VITE_'
|
||||||
|
|
||||||
config.publicDir = config.publicDir || 'resources'
|
config.publicDir = config.publicDir || 'resources'
|
||||||
// do not copy public dir
|
// do not copy public dir
|
||||||
config.build.copyPublicDir = false
|
config.build.copyPublicDir = false
|
||||||
// module preload polyfill does not apply to nodejs (main process)
|
// module preload polyfill does not apply to nodejs (main process)
|
||||||
config.build.modulePreload = false
|
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
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
{
|
||||||
|
name: 'vite:electron-main-resolved-config',
|
||||||
export function electronMainConfigValidatorPlugin(): Plugin {
|
|
||||||
return {
|
|
||||||
name: 'vite:electron-main-config-validator',
|
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
configResolved(config): void {
|
configResolved(config): void {
|
||||||
const build = config.build
|
const build = config.build
|
||||||
if (!build.target) {
|
if (!build.target) {
|
||||||
throw new Error('build.target option is required in the electron vite main config.')
|
throw new Error('build target required for the electron vite main config')
|
||||||
} else {
|
} else {
|
||||||
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
||||||
if (targets.some(t => !t.startsWith('node'))) {
|
if (targets.some(t => !t.startsWith('node'))) {
|
||||||
throw new Error('The electron vite main config build.target option must be "node?".')
|
throw new Error('the electron vite main config build target must be node')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const libOptions = build.lib
|
const lib = build.lib
|
||||||
const rolldownOptions = build.rolldownOptions
|
if (!lib) {
|
||||||
|
const rollupOptions = build.rollupOptions
|
||||||
if (!(libOptions && libOptions.entry) && !rolldownOptions?.input) {
|
if (!rollupOptions?.input) {
|
||||||
throw new Error(
|
throw new Error('build lib field required for the electron vite main config')
|
||||||
'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 outputs = resolveBuildOutputs(rolldownOptions.output, libOptions)
|
|
||||||
|
|
||||||
if (outputs.length > 0) {
|
|
||||||
if (outputs.length > 1) {
|
|
||||||
throw new Error('The electron vite main config does not support multiple outputs.')
|
|
||||||
} else {
|
} else {
|
||||||
const output = outputs[0]
|
const output = rollupOptions?.output
|
||||||
if (['es', 'cjs'].includes(output.format || '')) {
|
if (output) {
|
||||||
if (output.format === 'es' && !supportESM()) {
|
const formats = Array.isArray(output) ? output : [output]
|
||||||
throw new Error(
|
if (formats.some(f => f.format !== 'cjs')) {
|
||||||
'The electron vite main config output format does not support "es", ' +
|
throw new Error('the electron vite main config output format must be cjs')
|
||||||
'you can upgrade electron to the latest version or switch to "cjs" format.'
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
if (!lib.entry) {
|
||||||
`The electron vite main config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
|
throw new Error('build entry field required for the electron vite main config')
|
||||||
)
|
}
|
||||||
}
|
if (!lib.formats) {
|
||||||
|
throw new Error('build format field required for the electron vite main config')
|
||||||
|
} else if (!lib.formats.includes('cjs')) {
|
||||||
|
throw new Error('the electron vite main config build lib format must be cjs')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
|
export function electronPreloadVitePlugin(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 {
|
||||||
@ -172,47 +157,34 @@ export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOption
|
|||||||
|
|
||||||
const nodeTarget = getElectronNodeTarget()
|
const nodeTarget = getElectronNodeTarget()
|
||||||
|
|
||||||
const pkg = loadPackageData() || { type: 'commonjs' }
|
|
||||||
|
|
||||||
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
|
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
ssr: {
|
|
||||||
resolve: {
|
|
||||||
conditions: ['module', 'browser', 'development|production'],
|
|
||||||
mainFields: ['browser', 'module', 'jsnext:main', 'jsnext']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
build: {
|
build: {
|
||||||
outDir: path.resolve(root, 'out', 'preload'),
|
outDir: path.resolve(root, 'out', 'preload'),
|
||||||
target: nodeTarget,
|
target: nodeTarget,
|
||||||
assetsDir: 'chunks',
|
assetsDir: 'chunks',
|
||||||
rolldownOptions: {
|
rollupOptions: {
|
||||||
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
external: ['electron', ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
||||||
output: {}
|
output: {
|
||||||
|
entryFileNames: '[name].js'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
reportCompressedSize: false,
|
reportCompressedSize: false,
|
||||||
minify: false
|
minify: false
|
||||||
}
|
}
|
||||||
} satisfies UserConfig
|
}
|
||||||
|
|
||||||
const build = config.build || {}
|
const build = config.build || {}
|
||||||
const rolldownOptions = build.rolldownOptions || {}
|
const rollupOptions = build.rollupOptions || {}
|
||||||
if (!rolldownOptions.input) {
|
if (!rollupOptions.input) {
|
||||||
const libOptions = build.lib
|
|
||||||
const outputOptions = rolldownOptions.output
|
|
||||||
defaultConfig.build['lib'] = {
|
defaultConfig.build['lib'] = {
|
||||||
entry: findLibEntry(root, 'preload'),
|
entry: findLibEntry(root, 'preload'),
|
||||||
formats:
|
formats: ['cjs']
|
||||||
libOptions && libOptions.formats && libOptions.formats.length > 0
|
|
||||||
? []
|
|
||||||
: [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
defaultConfig.build.rolldownOptions.output['format'] = format
|
defaultConfig.build.rollupOptions.output['format'] = 'cjs'
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig.build.rolldownOptions.output['assetFileNames'] = path.posix.join(
|
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
|
||||||
build.assetsDir || defaultConfig.build.assetsDir,
|
build.assetsDir || defaultConfig.build.assetsDir,
|
||||||
'[name]-[hash].[ext]'
|
'[name]-[hash].[ext]'
|
||||||
)
|
)
|
||||||
@ -220,100 +192,66 @@ export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOption
|
|||||||
const buildConfig = mergeConfig(defaultConfig.build, build)
|
const buildConfig = mergeConfig(defaultConfig.build, build)
|
||||||
config.build = buildConfig
|
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 {
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.define = config.define || {}
|
config.define = config.define || {}
|
||||||
config.define = { ...processEnvDefine(), ...config.define }
|
config.define = { ...processEnvDefine(), ...config.define }
|
||||||
|
|
||||||
config.envPrefix = config.envPrefix || ['PRELOAD_VITE_', 'VITE_']
|
config.envPrefix = config.envPrefix || 'PRELOAD_VITE_'
|
||||||
|
|
||||||
config.publicDir = config.publicDir || 'resources'
|
config.publicDir = config.publicDir || 'resources'
|
||||||
// do not copy public dir
|
// do not copy public dir
|
||||||
config.build.copyPublicDir = false
|
config.build.copyPublicDir = false
|
||||||
// module preload polyfill does not apply to nodejs (preload scripts)
|
// module preload polyfill does not apply to nodejs (preload scripts)
|
||||||
config.build.modulePreload = false
|
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
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
{
|
||||||
|
name: 'vite:electron-preload-resolved-config',
|
||||||
export function electronPreloadConfigValidatorPlugin(): Plugin {
|
|
||||||
return {
|
|
||||||
name: 'vite:electron-preload-config-validator',
|
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
configResolved(config): void {
|
configResolved(config): void {
|
||||||
const build = config.build
|
const build = config.build
|
||||||
if (!build.target) {
|
if (!build.target) {
|
||||||
throw new Error('build.target option is required in the electron vite preload config.')
|
throw new Error('build target required for the electron vite preload config')
|
||||||
} else {
|
} else {
|
||||||
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
||||||
if (targets.some(t => !t.startsWith('node'))) {
|
if (targets.some(t => !t.startsWith('node'))) {
|
||||||
throw new Error('The electron vite preload config build.target must be "node?".')
|
throw new Error('the electron vite preload config build target must be node')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const libOptions = build.lib
|
const lib = build.lib
|
||||||
const rolldownOptions = build.rolldownOptions
|
if (!lib) {
|
||||||
|
const rollupOptions = build.rollupOptions
|
||||||
if (!(libOptions && libOptions.entry) && !rolldownOptions?.input) {
|
if (!rollupOptions?.input) {
|
||||||
throw new Error(
|
throw new Error('build lib field required for the electron vite preload config')
|
||||||
'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 outputs = resolveBuildOutputs(rolldownOptions.output, libOptions)
|
|
||||||
|
|
||||||
if (outputs.length > 0) {
|
|
||||||
if (outputs.length > 1) {
|
|
||||||
throw new Error('The electron vite preload config does not support multiple outputs.')
|
|
||||||
} else {
|
} else {
|
||||||
const output = outputs[0]
|
const output = rollupOptions?.output
|
||||||
if (['es', 'cjs'].includes(output.format || '')) {
|
if (output) {
|
||||||
if (output.format === 'es' && !supportESM()) {
|
const formats = Array.isArray(output) ? output : [output]
|
||||||
throw new Error(
|
if (formats.some(f => f.format !== 'cjs')) {
|
||||||
'The electron vite preload config output format does not support "es", ' +
|
throw new Error('the electron vite preload config output format must be cjs')
|
||||||
'you can upgrade electron to the latest version or switch to "cjs" format.'
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
if (!lib.entry) {
|
||||||
`The electron vite preload config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
|
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 electronRendererConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
|
export function electronRendererVitePlugin(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()
|
||||||
@ -341,14 +279,14 @@ export function electronRendererConfigPresetPlugin(options?: ElectronPluginOptio
|
|||||||
outDir: path.resolve(root, 'out', 'renderer'),
|
outDir: path.resolve(root, 'out', 'renderer'),
|
||||||
target: chromeTarget,
|
target: chromeTarget,
|
||||||
modulePreload: { polyfill: false },
|
modulePreload: { polyfill: false },
|
||||||
rolldownOptions: {
|
rollupOptions: {
|
||||||
input: findInput(root)
|
input: findInput(root)
|
||||||
},
|
},
|
||||||
reportCompressedSize: false,
|
reportCompressedSize: false,
|
||||||
minify: false,
|
minify: false,
|
||||||
emptyOutDir: emptyOutDir()
|
emptyOutDir: emptyOutDir()
|
||||||
}
|
}
|
||||||
} satisfies UserConfig
|
}
|
||||||
|
|
||||||
if (config.build?.outDir) {
|
if (config.build?.outDir) {
|
||||||
config.build.outDir = path.resolve(root, config.build.outDir)
|
config.build.outDir = path.resolve(root, config.build.outDir)
|
||||||
@ -359,41 +297,33 @@ export function electronRendererConfigPresetPlugin(options?: ElectronPluginOptio
|
|||||||
|
|
||||||
config.envDir = config.envDir || path.resolve(root)
|
config.envDir = config.envDir || path.resolve(root)
|
||||||
|
|
||||||
config.envPrefix = config.envPrefix || ['RENDERER_VITE_', 'VITE_']
|
config.envPrefix = config.envPrefix || 'RENDERER_VITE_'
|
||||||
|
|
||||||
config.build.rollupOptions = config.build.rolldownOptions
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
{
|
||||||
|
name: 'vite:electron-renderer-resolved-config',
|
||||||
export function electronRendererConfigValidatorPlugin(): Plugin {
|
|
||||||
return {
|
|
||||||
name: 'vite:electron-renderer-config-validator',
|
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
configResolved(config): void {
|
configResolved(config): void {
|
||||||
if (config.base !== './' && config.base !== '/') {
|
if (config.base !== './' && config.base !== '/') {
|
||||||
config.logger.warn(colors.yellow('(!) Should not set "base" option for the electron vite renderer config.'))
|
config.logger.warn(colors.yellow('should not set base field for the electron vite renderer config'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const build = config.build
|
const build = config.build
|
||||||
if (!build.target) {
|
if (!build.target) {
|
||||||
throw new Error('build.target option is required in the electron vite renderer config.')
|
throw new Error('build target required for the electron vite renderer config')
|
||||||
} else {
|
} else {
|
||||||
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
||||||
if (targets.some(t => !t.startsWith('chrome') && !/^es((202\d{1})|next)$/.test(t))) {
|
if (targets.some(t => !t.startsWith('chrome') && !/^es((202\d{1})|next)$/.test(t))) {
|
||||||
config.logger.warn(
|
throw new Error('the electron vite renderer config build target must be chrome? or es?')
|
||||||
'The electron vite renderer config build.target is not "chrome?" or "es?". This could be a mistake.'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rolldownOptions = build.rolldownOptions
|
const rollupOptions = build.rollupOptions
|
||||||
if (!rolldownOptions.input) {
|
if (!rollupOptions.input) {
|
||||||
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory.`))
|
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory`))
|
||||||
throw new Error(
|
throw new Error('build rollupOptions input field required for the electron vite renderer config')
|
||||||
'build.rollupOptions.input or build.rolldownOptions.input option is required in the electron vite renderer config.'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
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,5 +1,6 @@
|
|||||||
|
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[]
|
||||||
@ -7,41 +8,37 @@ 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 pkg = loadPackageData() || {}
|
const packagePath = path.resolve(process.cwd(), 'package.json')
|
||||||
|
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.filter(dep => dep.trim() !== ''))
|
deps = deps.concat(include)
|
||||||
}
|
}
|
||||||
|
|
||||||
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: {
|
||||||
rolldownOptions: {
|
rollupOptions: {
|
||||||
external: deps.length > 0 ? [...deps, new RegExp(`^(${deps.join('|')})/.+`)] : []
|
external: [...new Set(deps)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
/* 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
|
|
||||||
}
|
|
||||||
@ -1,199 +0,0 @@
|
|||||||
/* 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
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,16 +79,12 @@ 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' | 'oxc' | 'terser' = false
|
let minify: boolean | 'esbuild' | '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> {
|
||||||
@ -105,7 +101,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' || minify === 'oxc') {
|
if (!minify || minify === 'terser') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const result = await transformWithSWC(code, chunk.fileName, {
|
const result = await transformWithSWC(code, chunk.fileName, {
|
||||||
|
|||||||
@ -1,47 +1,49 @@
|
|||||||
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, toRelativePath } from '../utils'
|
import { cleanUrl, parseRequest, toRelativePath } from '../utils'
|
||||||
|
|
||||||
const nodeWorkerAssetUrlRE = /__VITE_NODE_WORKER_ASSET__([\w$]+)__/g
|
const nodeWorkerAssetUrlRE = /__VITE_NODE_WORKER_ASSET__([a-z\d]{8})__/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 {
|
||||||
if (id.endsWith('?nodeWorker')) {
|
const query = parseRequest(id)
|
||||||
|
if (query && typeof query.nodeWorker === 'string') {
|
||||||
return id + `&importer=${importer}`
|
return id + `&importer=${importer}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
load(id): string | void {
|
load(id): string | void {
|
||||||
if (nodeWorkerRE.test(id)) {
|
const query = parseRequest(id)
|
||||||
const match = nodeWorkerImporterRE.exec(id)
|
if (query && typeof query.nodeWorker === 'string' && typeof query.importer === 'string') {
|
||||||
if (match) {
|
const cleanPath = cleanUrl(id)
|
||||||
const hash = this.emitFile({
|
const hash = this.emitFile({
|
||||||
type: 'chunk',
|
type: 'chunk',
|
||||||
id: cleanUrl(id),
|
id: cleanPath,
|
||||||
importer: match[1]
|
importer: query.importer
|
||||||
})
|
})
|
||||||
const assetRefId = `__VITE_NODE_WORKER_ASSET__${hash}__`
|
const assetRefId = `__VITE_NODE_WORKER_ASSET__${hash}__`
|
||||||
return `
|
return `
|
||||||
import { Worker } from 'node:worker_threads';
|
import { Worker } from 'node:worker_threads';
|
||||||
export default function (options) { return new Worker(new URL(${assetRefId}, import.meta.url), options); }`
|
export default function (options) { return new Worker(require.resolve(${assetRefId}), options); }`
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderChunk(code, chunk, { sourcemap }) {
|
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null {
|
||||||
|
if (code.match(nodeWorkerAssetUrlRE)) {
|
||||||
let match: RegExpExecArray | null
|
let match: RegExpExecArray | null
|
||||||
let s: MagicString | undefined
|
const s = new MagicString(code)
|
||||||
|
|
||||||
nodeWorkerAssetUrlRE.lastIndex = 0
|
|
||||||
while ((match = nodeWorkerAssetUrlRE.exec(code))) {
|
while ((match = nodeWorkerAssetUrlRE.exec(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)
|
||||||
@ -51,10 +53,9 @@ export default function workerPlugin(): Plugin {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s) {
|
|
||||||
return {
|
return {
|
||||||
code: s.toString(),
|
code: s.toString(),
|
||||||
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
map: sourcemap ? s.generateMap({ hires: true }) : 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)
|
startElectron(inlineConfig.root, logger)
|
||||||
|
|
||||||
logger.info(colors.green(`\nstarting electron app...\n`))
|
logger.info(colors.green(`\nstart electron app...\n`))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import type { ChildProcess } from 'node:child_process'
|
import type { ChildProcessWithoutNullStreams } 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,29 +22,27 @@ export async function createServer(
|
|||||||
const logger = createLogger(inlineConfig.logLevel)
|
const logger = createLogger(inlineConfig.logLevel)
|
||||||
|
|
||||||
let server: ViteDevServer | undefined
|
let server: ViteDevServer | undefined
|
||||||
let ps: ChildProcess | undefined
|
let ps: ChildProcessWithoutNullStreams | 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(`\nelectron main process rebuilt successfully`))
|
logger.info(colors.green(`\nrebuild the electron main process successfully`))
|
||||||
|
|
||||||
if (ps) {
|
if (ps) {
|
||||||
|
logger.info(colors.cyan(`\n waiting for electron to exit...`))
|
||||||
|
|
||||||
ps.removeAllListeners()
|
ps.removeAllListeners()
|
||||||
ps.kill()
|
ps.kill()
|
||||||
ps = startElectron(inlineConfig.root)
|
ps = startElectron(inlineConfig.root, logger)
|
||||||
|
|
||||||
logger.info(colors.green(`\nrestarting electron app...\n`))
|
logger.info(colors.green(`\nrestart electron app...`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await doBuild(mainViteConfig, watchHook, errorHook)
|
await doBuild(mainViteConfig, watchHook)
|
||||||
|
|
||||||
logger.info(colors.green(`\nelectron main process built successfully`))
|
logger.info(colors.green(`\nbuild the electron main process successfully`))
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadViteConfig = config.config?.preload
|
const preloadViteConfig = config.config?.preload
|
||||||
@ -52,23 +50,25 @@ 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(`\nelectron preload scripts rebuilt successfully`))
|
logger.info(colors.green(`\nrebuild the electron preload files successfully`))
|
||||||
|
|
||||||
if (server) {
|
if (server) {
|
||||||
logger.info(colors.cyan(`\nreloading electron renderer...\n`))
|
logger.info(colors.cyan(`\n trigger renderer reload`))
|
||||||
|
|
||||||
server.ws.send({ type: 'full-reload' })
|
server.ws.send({ type: 'full-reload' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await doBuild(preloadViteConfig, watchHook, errorHook)
|
await doBuild(preloadViteConfig, watchHook)
|
||||||
|
|
||||||
logger.info(colors.green(`\nelectron preload scripts built successfully`))
|
logger.info(colors.green(`\nbuild the electron preload files successfully`))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.rendererOnly) {
|
if (options.rendererOnly) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`\n${colors.yellow(colors.bold('(!)'))} ${colors.yellow('skipped building main process and preload scripts (using previous build)')}`
|
`\n${colors.yellow(colors.bold('warn'))}:${colors.yellow(
|
||||||
|
' 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)
|
ps = startElectron(inlineConfig.root, logger)
|
||||||
|
|
||||||
logger.info(colors.green(`\nstarting electron app...\n`))
|
logger.info(colors.green(`\nstart electron app...\n`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserConfig = ViteConfig & { configFile?: string | false }
|
type UserConfig = ViteConfig & { configFile?: string | false }
|
||||||
|
|
||||||
async function doBuild(config: UserConfig, watchHook: () => void, errorHook: (e: Error) => void): Promise<void> {
|
async function doBuild(config: UserConfig, watchHook: () => 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,12 +131,10 @@ async function doBuild(config: UserConfig, watchHook: () => void, errorHook: (e:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
viteBuild(config)
|
viteBuild(config).then(() => {
|
||||||
.then(() => {
|
|
||||||
if (!config.build?.watch) {
|
if (!config.build?.watch) {
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(e => errorHook(e))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
173
src/utils.ts
173
src/utils.ts
@ -1,14 +1,14 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */
|
import { URL, URLSearchParams } from 'node:url'
|
||||||
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 { createRequire } from 'node:module'
|
import { loadEnv as viteLoadEnv } from 'vite'
|
||||||
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,11 +20,16 @@ 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')
|
return createHash('sha256').update(text).digest('hex').substring(0, 8)
|
||||||
.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 {
|
||||||
@ -33,160 +38,14 @@ export function toRelativePath(filename: string, importer: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve configured build outputs into an array.
|
* Load `.env` files within the `envDir`(default: `process.cwd()`).
|
||||||
*/
|
* 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[] = ['VITE_', 'MAIN_VITE_', 'PRELOAD_VITE_', 'RENDERER_VITE_']
|
prefixes: string | string[] = ['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,20 +1,23 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2023",
|
"target": "es2019",
|
||||||
"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": "bundler",
|
"moduleResolution": "node",
|
||||||
"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", "rollup.config.ts"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user