Compare commits

..

No commits in common. "master" and "v1.0.4" have entirely different histories.

47 changed files with 3175 additions and 6065 deletions

2
.eslintignore Normal file
View File

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

38
.eslintrc.js Normal file
View 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
View File

@ -1 +0,0 @@
github: alex8088

View File

@ -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://github.com/alex8088/electron-vite#readme).
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

View File

@ -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.

View File

@ -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://github.com/alex8088/electron-vite#readme).
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
View File

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

11
.vscode/settings.json vendored
View File

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

View File

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

330
README.md
View File

@ -1,41 +1,23 @@
<p align="center"> # electron-vite
<img src="https://alex8088.github.io/assets/electron-vite.svg" width="150px" height="150px">
<p>
<img src="https://img.shields.io/badge/node->14.0.0-blue.svg" alt="node" />
<img src="https://img.shields.io/badge/vite->2.6.0-747bff.svg" alt="vite" />
</p> </p>
<div align="center"> English | [简体中文](./README.zh-CN.md)
<h1>electron-vite</h1>
</div>
<p align="center">Next generation Electron build tooling based on Vite</p>
<p align="center"> > An Electron CLI integrated with Vite
<img src="https://img.shields.io/npm/v/electron-vite?color=6988e6&label=version">
<img src="https://img.shields.io/github/license/alex8088/electron-vite?color=blue" alt="license" />
</p>
<p align="center"> ---
<a href="https://electron-vite.org">Documentation</a> |
<a href="https://electron-vite.org/guide">Getting Started</a> |
<a href="https://github.com/alex8088/quick-start/tree/master/packages/create-electron">create-electron</a>
</p>
<p align="center">
<a href="https://cn.electron-vite.org">中文文档</a>
</p>
<br />
<br />
## Features ## Features
- ⚡️ [Vite](https://vitejs.dev) powered and use the same way. - ⚡️Use the same way as [Vite](https://vitejs.dev)
- 🛠 Pre-configure with sensible defaults optimized for Electron. - 🔨Main process, renderer process and preload script source code are built using Vite
- 💡 Optimize asset handling for Electron main process. - 📃Main process, renderer process and preload script Vite configuration combined into one file
- 🚀 Fast HMR & hot reloading. - 📦Preset optimal build configuration
- 🔥 Isolated build for multi-entry application development. - 🚀HMR for renderer processes
- ✨ Simplify multi-threading development.
- 🔒 Compile code to v8 bytecode to protect source code.
- 🔌 Easy to debug in IDEs such as VSCode or WebStorm.
- 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more.
## Usage ## Usage
@ -52,14 +34,66 @@ In a project where `electron-vite` is installed, you can use `electron-vite` bin
```json ```json
{ {
"scripts": { "scripts": {
"start": "electron-vite preview", "start": "electron-vite preview", // start electron app to preview production build
"dev": "electron-vite dev", "dev": "electron-vite dev", // start dev server and electron app
"prebuild": "electron-vite build" "prebuild": "electron-vite build" // build for production
} }
} }
``` ```
### Configuration In order to use the renderer process HMR, you need to use the `environment variables` to determine whether the window browser loads a local html file or a remote URL.
```js
function createWindow() {
// Create the browser window
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js')
}
})
// Load the remote URL for development or the local html file for production
if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}
}
```
**Note**: For development, the renderer process `index.html` file needs to reference your script code via `<script type="module">`.
### Recommended project directory
```shell
├──src
│ ├──main
│ │ ├──index.js
│ │ └──...
│ ├──preload
│ │ ├──index.js
│ │ └──...
│ └──renderer
│ ├──src
│ ├──index.html
│ └──...
├──electron.vite.config.js
└──package.json
```
### Get started
Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project.
```bash
npm init @quick-start/electron
```
## Configure
### Config file
When running `electron-vite` from the command line, electron-vite will automatically try to resolve a config file named `electron.vite.config.js` inside project root. The most basic config file looks like this: When running `electron-vite` from the command line, electron-vite will automatically try to resolve a config file named `electron.vite.config.js` inside project root. The most basic config file looks like this:
@ -78,27 +112,223 @@ export default {
} }
``` ```
### Getting Started You can also explicitly specify a config file to use with the `--config` CLI option (resolved relative to `cwd`):
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. ```sh
electron-vite --config my-config.js
```bash
npm create @quick-start/electron@latest
``` ```
Currently supported template presets include: **Tips**: `electron-vite` also supports `ts` or `mjs` config file.
| JavaScript | TypeScript | ### Config intellisense
| :--------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------: |
| [vanilla](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vanilla) | [vanilla-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vanilla-ts) |
| [vue](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vue) | [vue-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vue-ts) |
| [react](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/react) | [react-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/react-ts) |
| [svelte](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/svelte) | [svelte-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/svelte-ts) |
| [solid](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/solid) | [solid-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/solid-ts) |
## Contribution Since `electron-vite` ships with TypeScript typings, you can leverage your IDE's intellisense with jsdoc type hints:
See [Contributing Guide](CONTRIBUTING.md). ```js
/**
* @type {import('electron-vite').UserConfig}
*/
const config = {
// ...
}
export default config
```
Alternatively, you can use the `defineConfig` and `defineViteConfig` helper which should provide intellisense without the need for jsdoc annotations:
```js
import { defineConfig, defineViteConfig } from 'electron-vite'
export default defineConfig({
main: {
// ...
},
preload: {
// ...
},
renderer: defineViteConfig(({ command, mode }) => {
// conditional config use defineViteConfig
// ...
})
})
```
**Tips**: The `defineViteConfig` exports from `Vite`.
### Config reference
See [vitejs.dev](https://vitejs.dev/config)
### Config presets
#### Build options for `main`:
- **outDir**: `out\main`(relative to project root)
- **target**: `node*`, automatically match node target of `Electron`. For example, the node target of Electron 17 is `node16.13`
- **lib.entry**: `src\main\{index|main}.{js|ts|mjs|cjs}`(relative to project root), empty string if not found
- **lib.formats**: `cjs`
- **rollupOptions.external**: `electron` and all builtin modules
#### Build options for `preload`:
- **outDir**: `out\preload`(relative to project root)
- **target**: the same as `main`
- **lib.entry**: `src\preload\{index|preload}.{js|ts|mjs|cjs}`(relative to project root), empty string if not found
- **lib.formats**: `cjs`
- **rollupOptions.external**: the same as `main`
#### Build options for `renderer`:
- **root**: `src\renderer`(relative to project root)
- **outDir**: `out\renderer`(relative to project root)
- **target**: `chrome*`, automatically match chrome target of `Electron`. For example, the chrome target of Electron 17 is `chrome98`
- **lib.entry**: `src\renderer\index.html`(relative to project root), empty string if not found
- **polyfillModulePreload**: `false`, there is no need to polyfill `Module Preload` for the Electron renderer
- **rollupOptions.external**: the same as `main`
#### Define option for `main` and `preload`
In web development, Vite will transform `'process.env.'` to `'({}).'`. This is reasonable and correct. But in nodejs development, we sometimes need to use `process.env`, so `electron-vite` will automatically add config define field to redefine global variable replacements like this:
```js
export default {
main: {
define: {
'process.env': 'process.env'
}
}
}
```
**Note**: If you want to use these configurations in an existing project, please see the Vite plugin [vite-plugin-electron-config](https://github.com/alex8088/vite-plugin-electron-config)
### Config FAQs
#### How do I configure when the Electron app has multiple windows?
When your electron app has multiple windows, it means there are multiple html files or preload files. You can modify your config file like this:
```js
export default {
main: {},
preload: {
build: {
rollupOptions: {
input: {
browser: resolve(__dirname, 'src/preload/browser.ts'),
webview: resolve(__dirname, 'src/preload/webview.ts')
}
}
}
},
renderer: {
build: {
rollupOptions: {
input: {
browser: resolve(__dirname, 'src/renderer/browser.html'),
webview: resolve(__dirname, 'src/renderer/webview.html')
}
}
}
}
}
```
## CLI options
For the full list of CLI options, you can run `npx electron-vite -h` in your project. The flags listed below are only available via the command line interface:
- `--ignoreConfigWarning`: boolean, allow you ignore warning when config missing
- `--outDir`: string, output directory (default: out)
## API
### build
Type Signature:
```js
async function build(inlineConfig: InlineConfig = {}): Promise<void>
```
Example Usage:
```js
const path = require('path')
const { build } = require('electron-vite')
;(async () => {
await build({
build: {
outDir: 'out'
rollupOptions: {
// ...
}
}
})
})()
```
### createServer
Type Signature:
```js
async function createServer(inlineConfig: InlineConfig = {}): Promise<void>
```
Example Usage:
```js
const { createServer } = require('electron-vite')
;(async () => {
await createServer({
server: {
port: 1337
}
})
})()
```
### preview
Type Signature:
```js
async function preview(inlineConfig: InlineConfig = {}): Promise<void>
```
Example Usage:
```js
const { preview } = require('electron-vite')
;(async () => {
await preview({})
})()
```
### InlineConfig
The InlineConfig interface extends Vite [UserConfig](https://vitejs.dev/guide/api-javascript.html#inlineconfig) with additional properties:
- `ignoreConfigWarning`: set to `false` to ignore warning when config missing
And omit `base` property because it is not necessary to set the base public path in Electron.
### resolveConfig
Type Signature:
```js
async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
defaultMode = 'development'
): Promise<ResolvedConfig>
```
## License ## License

335
README.zh-CN.md Normal file
View File

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

52
api-extractor.json Normal file
View 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"
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

112
node.d.ts vendored
View File

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

View File

@ -1,29 +1,19 @@
{ {
"name": "electron-vite", "name": "electron-vite",
"version": "6.0.0", "version": "1.0.4",
"description": "Electron build tooling based on Vite", "description": "Use vite for your electron app.",
"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"
}, },
"files": [ "files": [
"bin", "bin",
"dist", "dist"
"node.d.ts"
], ],
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": ">=12.2.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 +23,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 +32,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",
@ -60,49 +50,30 @@
] ]
}, },
"peerDependencies": { "peerDependencies": {
"@swc/core": "^1.0.0", "vite": "^2.9.0"
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
}
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@microsoft/api-extractor": "^7.19.4",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^8.3.0",
"@rollup/plugin-typescript": "^12.3.0", "@types/node": "16.11.22",
"@swc/core": "^1.16.2", "@typescript-eslint/eslint-plugin": "^5.11.0",
"@types/babel__core": "^7.20.5", "@typescript-eslint/parser": "^5.11.0",
"@types/node": "^22.20.4", "eslint": "^8.7.0",
"eslint": "^10.11.0", "eslint-config-prettier": "^8.3.0",
"eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-prettier": "^5.5.6", "fs-extra": "^10.0.0",
"globals": "^17.12.0", "lint-staged": "^12.3.6",
"lint-staged": "^17.5.1", "prettier": "^2.5.1",
"prettier": "^3.9.9", "rollup": "^2.64.0",
"rollup": "^4.63.5", "simple-git-hooks": "^2.7.0",
"rollup-plugin-dts": "^6.5.1", "tslib": "^2.3.1",
"rollup-plugin-rm": "^1.0.2", "typescript": "^4.5.5",
"simple-git-hooks": "^2.14.0", "vite": "^2.9.6"
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.70.1",
"vite": "^8.3.1"
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.29.7", "cac": "^6.7.12",
"@babel/plugin-transform-arrow-functions": "^7.29.7", "esbuild": "^0.14.38",
"cac": "^7.0.0", "picocolors": "^1.0.0"
"magic-string": "^1.4.2",
"picocolors": "^1.1.1",
"rolldown": "~1.2.10"
},
"pnpm": {
"onlyBuiltDependencies": [
"@swc/core",
"simple-git-hooks"
]
} }
} }

3918
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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`))
})()

View File

@ -1,13 +1,13 @@
// Invoked on the commit-msg git hook by simple-git-hooks. // Invoked on the commit-msg git hook by simple-git-hooks.
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()

View File

@ -1,56 +1,23 @@
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' const config = await resolveConfig(inlineConfig, 'build', 'production')
if (config.config) {
const bytecodeCompiler = new BytecodeCompiler() const mainViteConfig = config.config?.main
const config = await resolveConfig( if (mainViteConfig) {
mergeConfig(inlineConfig, { await viteBuild(mainViteConfig)
plugins: [{ name: 'vite:electron-bytecode-build', api: { compiler: bytecodeCompiler } }]
}),
'build',
'production'
)
if (!config.config) {
return
} }
const preloadViteConfig = config.config?.preload
const hasBytecode = (await usesBytecode(config.config.main)) || (await usesBytecode(config.config.preload)) if (preloadViteConfig) {
await viteBuild(preloadViteConfig)
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 rendererViteConfig = config.config?.renderer
if (rendererViteConfig) {
await viteBuild(rendererViteConfig)
} }
} }
} }
if (!hasBytecode) return buildTargets()
try {
await bytecodeCompiler.start()
await buildTargets()
} finally {
await bytecodeCompiler.stop()
}
}

View File

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

View File

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

View File

@ -1,138 +1,63 @@
import path from 'node:path' import * as path from 'path'
import * as fs from 'fs'
import colors from 'picocolors' import colors from 'picocolors'
import { import {
type UserConfig as ViteConfig, UserConfig as ViteConfig,
type ConfigEnv, UserConfigExport as UserViteConfigExport,
type PluginOption, ConfigEnv,
type Plugin, Plugin,
type BuildEnvironmentOptions as ViteBuildOptions, LogLevel,
type LogLevel,
createLogger, createLogger,
mergeConfig, mergeConfig,
normalizePath normalizePath
} from 'vite' } from 'vite'
import { build } from 'esbuild'
import { import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugin'
electronMainConfigPresetPlugin, import { isObject, dynamicImport } from './utils'
electronMainConfigValidatorPlugin,
electronPreloadConfigPresetPlugin,
electronPreloadConfigValidatorPlugin,
electronRendererConfigPresetPlugin,
electronRendererConfigValidatorPlugin
} from './plugins/electron'
import assetPlugin from './plugins/asset'
import workerPlugin from './plugins/worker'
import importMetaPlugin from './plugins/importMeta'
import esmShimPlugin from './plugins/esmShim'
import modulePathPlugin from './plugins/modulePath'
import isolateEntriesPlugin from './plugins/isolateEntries'
import { type ExternalOptions, externalizeDepsPlugin } from './plugins/externalizeDeps'
import { type BytecodeOptions, bytecodePlugin } from './plugins/bytecode'
import { isObject, isFilePathESM, 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>
export type ElectronViteConfigExport =
UserConfig | Promise<UserConfig> | ElectronViteConfigFnObject | ElectronViteConfigFnPromise | ElectronViteConfigFn
/** /**
* Type helper to make it easier to use `electron.vite.config.*` * Vite config options for electron main process
* accepts a direct {@link UserConfig} object, or a function that returns it. *
* The function receives a object that exposes two properties: * https://cn.vitejs.dev/config/
* `command` (either `'build'` or `'serve'`), and `mode`.
*/ */
export function defineConfig(config: UserConfig): UserConfig main?: UserViteConfigExport
export function defineConfig(config: Promise<UserConfig>): Promise<UserConfig> /**
export function defineConfig(config: ElectronViteConfigFnObject): ElectronViteConfigFnObject * Vite config options for electron renderer process
export function defineConfig(config: ElectronViteConfigFnPromise): ElectronViteConfigFnPromise *
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport * https://cn.vitejs.dev/config/
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport { */
return config renderer?: UserViteConfigExport
/**
* Vite config options for electron preload files
*
* https://cn.vitejs.dev/config/
*/
preload?: UserViteConfigExport
} }
export type InlineConfig = Omit<ViteConfig, 'base'> & { export type InlineConfig = Omit<ViteConfig, 'base'> & {
@ -141,6 +66,16 @@ export type InlineConfig = Omit<ViteConfig, 'base'> & {
ignoreConfigWarning?: boolean ignoreConfigWarning?: boolean
} }
export type UserConfigExport = UserConfigSchema | Promise<UserConfigSchema>
/**
* Type helper to make it easier to use `electron.vite.config.ts`
* accepts a direct {@link UserConfig} object, or a function that returns it.
*/
export function defineConfig(config: UserConfigExport): UserConfigExport {
return config
}
export interface ResolvedConfig { export interface ResolvedConfig {
config?: UserConfig config?: UserConfig
configFile?: string configFile?: string
@ -155,10 +90,13 @@ export async function resolveConfig(
const config = inlineConfig const config = inlineConfig
const mode = inlineConfig.mode || defaultMode const mode = inlineConfig.mode || defaultMode
process.env.NODE_ENV = defaultMode config.mode = mode
const userConfig: UserConfig | undefined = {} if (mode === 'production') {
process.env.NODE_ENV = 'production'
}
let userConfig: UserConfig | undefined
let configFileDependencies: string[] = [] let configFileDependencies: string[] = []
let { configFile } = config let { configFile } = config
@ -167,7 +105,6 @@ export async function resolveConfig(
mode, mode,
command command
} }
const loadResult = await loadConfigFromFile( const loadResult = await loadConfigFromFile(
configEnv, configEnv,
configFile, configFile,
@ -175,30 +112,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 }))
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 }))
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 +172,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 +188,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') const CONFIG_FILE_NAME = 'electron.vite.config'
if (!hasBytecodePlugin) {
const bytecodeOptions = config.build?.bytecode
if (bytecodeOptions) {
isOptions<BytecodeOptions>(bytecodeOptions)
? configDrivenPlugins.push(bytecodePlugin(bytecodeOptions))
: configDrivenPlugins.push(bytecodePlugin())
}
}
return configDrivenPlugins
}
function isOptions<T extends object>(value: boolean | T): value is T {
return typeof value === 'object' && value !== null
}
export async function loadConfigFromFile( export async function loadConfigFromFile(
configEnv: ConfigEnv, configEnv: ConfigEnv,
@ -373,11 +206,14 @@ export async function loadConfigFromFile(
config: UserConfig config: UserConfig
dependencies: string[] dependencies: string[]
}> { }> {
if (configFile && /^vite.config.(js|ts|mjs|cjs|mts|cts)$/.test(configFile)) { let resolvedPath: string
let isESM = false
if (configFile && /^vite.config.(js)|(ts)|(mjs)|(cjs)$/.test(configFile)) {
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) resolvedPath = configFile ? path.resolve(configFile) : findConfigFile(configRoot, ['js', 'ts', 'mjs', 'cjs'])
if (!resolvedPath) { if (!resolvedPath) {
return { return {
@ -387,33 +223,151 @@ export async function loadConfigFromFile(
} }
} }
const isESM = isFilePathESM(resolvedPath) if (resolvedPath.endsWith('.mjs')) {
isESM = true
}
if (resolvedPath.endsWith('.js')) {
const pkg = path.join(configRoot, 'package.json')
if (fs.existsSync(pkg)) {
isESM = require(pkg).type === 'module'
}
}
const configFilePath = resolvedPath
try { try {
const { code, dependencies } = await bundleConfigFile(resolvedPath, isESM) const bundled = await bundleConfigFile(resolvedPath)
const configExport = await loadConfigFormBundledFile<ElectronViteConfigExport>(resolvedPath, code, isESM)
const config = await (typeof configExport === 'function' ? configExport(configEnv) : configExport) if (!isESM) {
resolvedPath = path.resolve(configRoot, `${CONFIG_FILE_NAME}.mjs`)
fs.writeFileSync(resolvedPath, bundled.code)
}
const fileUrl = require('url').pathToFileURL(resolvedPath)
const userConfig = (await dynamicImport(fileUrl)).default
if (!isESM) {
fs.unlinkSync(resolvedPath)
}
const config = await (typeof userConfig === 'function' ? userConfig() : userConfig)
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(configFilePath),
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 ${configFilePath}`), { 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): Promise<{ code: string; dependencies: string[] }> {
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: [fileName],
write: false,
platform: 'node',
bundle: true,
format: 'esm',
sourcemap: false,
metafile: true,
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: /\.[jt]s$/ }, async args => {
const contents = await fs.promises.readFile(args.path, 'utf8')
return {
loader: args.path.endsWith('.ts') ? 'ts' : 'js',
contents: contents
.replace(/\bimport\.meta\.url\b/g, JSON.stringify(`file://${args.path}`))
.replace(/\b__dirname\b/g, JSON.stringify(path.dirname(args.path)))
.replace(/\b__filename\b/g, JSON.stringify(args.path))
}
})
}
}
]
})
const { text } = result.outputFiles[0]
return {
code: text,
dependencies: result.metafile ? Object.keys(result.metafile.inputs) : []
}
}

View File

@ -1,200 +0,0 @@
import path from 'node:path'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import { type ChildProcess, spawn } from 'node:child_process'
import { loadPackageData } from './utils'
const _require = createRequire(import.meta.url)
const ensureElectronEntryFile = (root = process.cwd()): void => {
if (process.env.ELECTRON_ENTRY) return
const pkg = loadPackageData()
if (pkg) {
if (!pkg.main) {
throw new Error('No entry point found for electron app, please add a "main" field to package.json')
} else {
const entryPath = path.resolve(root, pkg.main)
if (!fs.existsSync(entryPath)) {
throw new Error(`No electron app entry file found: ${entryPath}`)
}
}
} else {
throw new Error('Not found: package.json')
}
}
const getElectronMajorVer = (): string => {
let majorVer = process.env.ELECTRON_MAJOR_VER || ''
if (!majorVer) {
const pkg = _require.resolve('electron/package.json')
if (fs.existsSync(pkg)) {
const version = _require(pkg).version
majorVer = version.split('.')[0]
process.env.ELECTRON_MAJOR_VER = majorVer
}
}
return majorVer
}
export function supportESM(): boolean {
const majorVer = getElectronMajorVer()
return parseInt(majorVer) >= 28
}
export function supportImportMetaPaths(): boolean {
const majorVer = getElectronMajorVer()
return parseInt(majorVer) >= 30
}
export function getElectronPath(): string {
let electronExecPath = process.env.ELECTRON_EXEC_PATH || ''
if (!electronExecPath) {
const electronModulePath = path.dirname(_require.resolve('electron'))
const pathFile = path.join(electronModulePath, 'path.txt')
let executablePath
if (fs.existsSync(pathFile)) {
executablePath = fs.readFileSync(pathFile, 'utf-8')
}
if (executablePath) {
electronExecPath = path.join(electronModulePath, 'dist', executablePath)
process.env.ELECTRON_EXEC_PATH = electronExecPath
} else {
throw new Error('Electron uninstall')
}
}
return electronExecPath
}
export function getElectronNodeTarget(): string {
const electronVer = getElectronMajorVer()
const nodeVer = {
'44': '24.18',
'43': '24.17',
'42': '24.15',
'41': '24.14',
'40': '24.14',
'39': '22.20',
'38': '22.19',
'37': '22.16',
'36': '22.14',
'35': '22.14',
'34': '20.18',
'33': '20.18',
'32': '20.16',
'31': '20.14',
'30': '20.11',
'29': '20.9',
'28': '18.18',
'27': '18.17',
'26': '18.16',
'25': '18.15',
'24': '18.14',
'23': '18.12',
'22': '16.17'
}
if (electronVer && parseInt(electronVer) > 10) {
let target = nodeVer[electronVer]
if (!target) target = Object.values(nodeVer).reverse()[0]
return 'node' + target
}
return ''
}
export function getElectronChromeTarget(): string {
const electronVer = getElectronMajorVer()
const chromeVer = {
'44': '152',
'43': '150',
'42': '148',
'41': '146',
'40': '144',
'39': '142',
'38': '140',
'37': '138',
'36': '136',
'35': '134',
'34': '132',
'33': '130',
'32': '128',
'31': '126',
'30': '124',
'29': '122',
'28': '120',
'27': '118',
'26': '116',
'25': '114',
'24': '112',
'23': '110',
'22': '108'
}
if (electronVer && parseInt(electronVer) > 10) {
let target = chromeVer[electronVer]
if (!target) target = Object.values(chromeVer).reverse()[0]
return 'chrome' + target
}
return ''
}
export function startElectron(root: string | undefined): ChildProcess {
ensureElectronEntryFile(root)
const electronPath = getElectronPath()
const isDev = process.env.NODE_ENV_ELECTRON_VITE === 'development'
const args: string[] = process.env.ELECTRON_CLI_ARGS ? JSON.parse(process.env.ELECTRON_CLI_ARGS) : []
if (!!process.env.REMOTE_DEBUGGING_PORT && isDev) {
args.push(`--remote-debugging-port=${process.env.REMOTE_DEBUGGING_PORT}`)
}
if (!!process.env.V8_INSPECTOR_PORT && isDev) {
args.push(`--inspect=${process.env.V8_INSPECTOR_PORT}`)
}
if (!!process.env.V8_INSPECTOR_BRK_PORT && isDev) {
args.push(`--inspect-brk=${process.env.V8_INSPECTOR_BRK_PORT}`)
}
if (process.env.NO_SANDBOX === '1') {
args.push('--no-sandbox')
}
const entry = process.env.ELECTRON_ENTRY || '.'
const ps = spawn(electronPath, [entry].concat(args), { stdio: 'inherit' })
// `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
}
let electronPs: ChildProcess | undefined
let signalHandlersRegistered = false
function setupElectronSignalHandlers(child: ChildProcess): void {
electronPs = child
if (signalHandlersRegistered) return
const signals: NodeJS.Signals[] =
process.platform === 'win32' ? ['SIGINT', 'SIGTERM'] : ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGUSR2']
for (const signal of signals) {
process.on(signal, () => {
const child = electronPs
if (!child?.pid || child.exitCode !== null || child.signalCode !== null) return
// A previously sent signal does not mean the child has exited.
child.kill(signal)
})
}
signalHandlersRegistered = true
}

View File

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

View File

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

353
src/plugin.ts Normal file
View File

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

View File

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

View File

@ -1,360 +0,0 @@
import path from 'node:path'
import { randomBytes } from 'node:crypto'
import { createRequire } from 'node:module'
import colors from 'picocolors'
import { type Plugin, type Rolldown, normalizePath } from 'vite'
import type * as Babel from '@babel/core'
import MagicString from 'magic-string'
import { toRelativePath, resolveBuildOutputs } from '../utils'
import type { BytecodeCompiler, BytecodeTarget } from '../bytecodeCompiler'
// Inspired by https://github.com/bytenode/bytenode
const require = createRequire(import.meta.url)
let babel: typeof Babel | undefined
function getBytecodeCompiler(plugins: readonly Plugin[]): BytecodeCompiler | undefined {
const plugin = plugins.find(plugin => plugin.name === 'vite:electron-bytecode-build')
return (plugin?.api as { compiler?: BytecodeCompiler } | undefined)?.compiler
}
// Versioned envelope: magic/version (8), UTF-16 source length (4), module ID (16), V8 cache.
// Keep our metadata separate from the private V8 cache header.
async function compileToBytecode(compiler: BytecodeCompiler, target: BytecodeTarget, code: string): Promise<Buffer> {
const id = randomBytes(16)
// Reserve enough source space for an untruncated, per-artifact placeholder identity.
const source = code + '\n/*' + id.toString('hex') + '*/'
const result = await compiler.compile(target, source)
const header = Buffer.alloc(28)
header.write('EVBC0001', 0, 'ascii')
header.writeUInt32LE(result.sourceLength, 8)
id.copy(header, 12)
return Buffer.concat([header, Buffer.from(result.cachedData, 'base64')])
}
const bytecodeModuleLoaderCode = [
'"use strict";',
'const fs = require("node:fs");',
'const path = require("node:path");',
'const vm = require("node:vm");',
'const v8 = require("node:v8");',
'const Module = require("node:module");',
'v8.setFlagsFromString("--no-lazy");',
'v8.setFlagsFromString("--no-flush-bytecode");',
'const params = ["exports", "require", "module", "__filename", "__dirname"];',
'Module._extensions[".jsc"] = Module._extensions[".cjsc"] = function (module, filename) {',
' const data = fs.readFileSync(filename);',
' if (data.length <= 28 || data.toString("ascii", 0, 8) !== "EVBC0001") {',
' throw new Error("Invalid electron-vite bytecode format; rebuild the application: " + filename);',
' }',
' const length = data.readUInt32LE(8);',
' const tag = "/*" + data.subarray(12, 28).toString("hex") + "*/";',
' if (length < tag.length || length > 0x1fffffff) {',
' throw new Error("Invalid bytecode source length: " + filename);',
' }',
' const placeholder = tag + " ".repeat(length - tag.length);',
' const compiledWrapper = vm.compileFunction(placeholder, params, {',
' filename,',
' cachedData: data.subarray(28)',
' });',
' if (compiledWrapper.cachedDataRejected) {',
' throw new Error("Invalid or incompatible cached data (cachedDataRejected): " + filename);',
' }',
' 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;',
' return compiledWrapper.call(module.exports, module.exports, require, module, filename, path.dirname(filename));',
'};'
]
const bytecodeChunkExtensionRE = /.(jsc|cjsc)$/
export interface BytecodeOptions {
chunkAlias?: string | string[] | RegExp
transformArrowFunctions?: boolean
removeBundleJS?: boolean
protectedStrings?: string[]
}
/**
* Compile source code to v8 bytecode.
*
* @deprecated use `build.bytecode` config option instead
*/
export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') {
return null
}
const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options
const _chunkAlias = chunkAlias instanceof RegExp ? chunkAlias : Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
const isBytecodeChunk = (chunkName: string): boolean => {
if (_chunkAlias instanceof RegExp) {
_chunkAlias.lastIndex = 0
return _chunkAlias.test(chunkName)
}
return _chunkAlias.length === 0 || _chunkAlias.some(alias => alias === chunkName)
}
const plugins: Babel.PluginItem[] = []
if (transformArrowFunctions) {
plugins.push('@babel/plugin-transform-arrow-functions')
}
if (protectedStrings.length > 0) {
plugins.push([protectStringsPlugin 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 bytecodeModuleLoader = 'bytecode-loader.cjs'
let supported = false
let isPreload = false
let bytecodeCompiler: BytecodeCompiler | undefined
return {
name: 'vite:bytecode',
apply: 'build',
enforce: 'post',
configResolved(config): void {
bytecodeCompiler = getBytecodeCompiler(config.plugins)
if (supported) {
return
}
isPreload = config.plugins.some(p => p.name === 'vite:electron-preload-config-preset')
const useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-config-preset')
if (useInRenderer) {
config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.'))
return
}
const build = config.build
const outputs = resolveBuildOutputs(build.rolldownOptions.output, build.lib)
const output = outputs[0]
if (output) {
if (output.format === 'es') {
config.logger.warn(
colors.yellow(
'bytecodePlugin does not support ES module, please remove "type": "module" ' +
'in package.json or set build.rollupOptions.output.format (or build.rolldownOptions.output.format) to "cjs".'
)
)
}
supported = output.format === 'cjs' && !useInRenderer
}
},
renderChunk(code, chunk, { sourcemap }): { code: string; map?: Rolldown.SourceMapInput } | null {
if (supported && isBytecodeChunk(chunk.name) && shouldTransformBytecodeChunk) {
return _transform(code, !!sourcemap)
}
return null
},
async generateBundle(_, output): Promise<void> {
if (!supported) {
return
}
const _chunks = Object.values(output)
const chunks = _chunks.filter(
chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name)
) as Rolldown.OutputChunk[]
if (chunks.length === 0) {
return
}
const bytecodeChunks = chunks.map(chunk => chunk.fileName)
const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName))
const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|')
const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null
const getBytecodeLoaderBlock = (chunkFileName: string): string => {
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
}
let bytecodeChunkCount = 0
const bundles = Object.keys(output)
if (!bytecodeCompiler) {
throw new Error('Bytecode compilation requires an electron-vite build')
}
for (const name of bundles) {
const chunk = output[name]
if (chunk.type === 'chunk') {
let _code = chunk.code
if (bytecodeRE) {
let match: RegExpExecArray | null
let s: MagicString | undefined
while ((match = bytecodeRE.exec(_code))) {
s ||= new MagicString(_code)
const [prefix, chunkName] = match
const len = prefix.length + chunkName.length
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
contentOnly: true
})
}
if (s) {
_code = s.toString()
}
}
if (bytecodeChunks.includes(name)) {
const bytecodeBuffer = await compileToBytecode(bytecodeCompiler, isPreload ? 'preload' : 'main', _code)
this.emitFile({
type: 'asset',
fileName: name + 'c',
source: bytecodeBuffer
})
if (!removeBundleJS) {
this.emitFile({
type: 'asset',
fileName: '_' + chunk.fileName,
source: chunk.code
})
}
if (chunk.isEntry) {
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
chunk.code = code
} else {
delete output[chunk.fileName]
}
bytecodeChunkCount += 1
} else {
if (chunk.isEntry) {
let hasBytecodeMoudle = false
const idsToHandle = new Set([...chunk.imports, ...chunk.dynamicImports])
for (const moduleId of idsToHandle) {
if (bytecodeChunks.includes(moduleId)) {
hasBytecodeMoudle = true
break
}
const moduleInfo = this.getModuleInfo(moduleId)
if (moduleInfo) {
const { importers, dynamicImporters } = moduleInfo
for (const importerId of importers) idsToHandle.add(importerId)
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
}
}
if (hasBytecodeMoudle) {
const loader = getBytecodeLoaderBlock(chunk.fileName)
const strictRE = /^(#![^\n]*\n)?(["'])use strict\2;/
_code = strictRE.test(_code)
? _code.replace(strictRE, `$&\n${loader}`)
: _code.replace(/^(#![^\n]*\n)?/, `$&${loader}\n`)
}
}
chunk.code = _code
}
}
}
if (bytecodeChunkCount && !_chunks.some(ass => ass.type === 'asset' && ass.fileName === bytecodeModuleLoader)) {
this.emitFile({
type: 'asset',
source: bytecodeModuleLoaderCode.join('\n') + '\n',
name: 'Bytecode Loader File',
fileName: bytecodeModuleLoader
})
}
},
writeBundle(_, output): void {
if (supported) {
const bytecodeChunkCount = Object.keys(output).filter(chunk => bytecodeChunkExtensionRE.test(chunk)).length
this.environment.logger.info(`${colors.green(`✓`)} ${bytecodeChunkCount} chunks compiled into bytecode.`)
}
}
}
}
interface ProtectStringsPluginState extends Babel.PluginPass {
opts: { protectedStrings: Set<string> }
}
function protectStringsPlugin(api: typeof Babel & Babel.ConfigAPI): Babel.PluginObj<ProtectStringsPluginState> {
const { types: t } = api
function createFromCharCodeFunction(value: string): Babel.types.CallExpression {
const charCodes = Array.from(value).map(s => s.charCodeAt(0))
const charCodeLiterals = charCodes.map(code => t.numericLiteral(code))
// String.fromCharCode
const memberExpression = t.memberExpression(t.identifier('String'), t.identifier('fromCharCode'))
// String.fromCharCode(...arr)
const callExpression = t.callExpression(memberExpression, [t.spreadElement(t.identifier('arr'))])
// return String.fromCharCode(...arr)
const returnStatement = t.returnStatement(callExpression)
// function (arr) { return ... }
const functionExpression = t.functionExpression(null, [t.identifier('arr')], t.blockStatement([returnStatement]))
// (function(...) { ... })([x, x, x])
return t.callExpression(functionExpression, [t.arrayExpression(charCodeLiterals)])
}
return {
name: 'protect-strings-plugin',
visitor: {
StringLiteral(path, state) {
// obj['property']
if (path.parentPath.isMemberExpression({ property: path.node, computed: true })) {
return
}
// { 'key': value }
if (path.parentPath.isObjectProperty({ key: path.node, computed: false })) {
return
}
// require('fs')
if (
path.parentPath.isCallExpression() &&
t.isIdentifier(path.parentPath.node.callee) &&
path.parentPath.node.callee.name === 'require' &&
path.parentPath.node.arguments[0] === path.node
) {
return
}
// Only CommonJS is supported, import declaration and export declaration checks are ignored
const { value } = path.node
if (state.opts.protectedStrings.has(value)) {
path.replaceWith(createFromCharCodeFunction(value))
}
},
TemplateLiteral(path, state) {
// Must be a pure static template literal
// expressions must be empty (no ${variables})
// quasis must have only one element (meaning the entire string is a single static part).
if (path.node.expressions.length > 0 || path.node.quasis.length !== 1) {
return
}
// Extract the raw value of the template literal
// path.node.quasis[0].value.raw is used to get the raw string, including escape sequences
// path.node.quasis[0].value.cooked is used to get the processed/cooked string (with escape sequences handled)
const value = path.node.quasis[0].value.cooked
if (value && state.opts.protectedStrings.has(value)) {
path.replaceWith(createFromCharCodeFunction(value))
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,64 +0,0 @@
import type { Plugin } from 'vite'
import MagicString from 'magic-string'
import { cleanUrl, toRelativePath } from '../utils'
const nodeWorkerAssetUrlRE = /__VITE_NODE_WORKER_ASSET__([\w$]+)__/g
const nodeWorkerRE = /\?nodeWorker(?:&|$)/
const nodeWorkerImporterRE = /(?:\?)nodeWorker&importer=([^&]+)(?:&|$)/
/**
* Resolve `?nodeWorker` import and automatically generate `Worker` wrapper.
*/
export default function workerPlugin(): Plugin {
return {
name: 'vite:node-worker',
apply: 'build',
enforce: 'pre',
resolveId(id, importer): string | void {
if (id.endsWith('?nodeWorker')) {
return id + `&importer=${importer}`
}
},
load(id): string | void {
if (nodeWorkerRE.test(id)) {
const match = nodeWorkerImporterRE.exec(id)
if (match) {
const hash = this.emitFile({
type: 'chunk',
id: cleanUrl(id),
importer: match[1]
})
const assetRefId = `__VITE_NODE_WORKER_ASSET__${hash}__`
return `
import { Worker } from 'node:worker_threads';
export default function (options) { return new Worker(new URL(${assetRefId}, import.meta.url), options); }`
}
}
},
renderChunk(code, chunk, { sourcemap }) {
let match: RegExpExecArray | null
let s: MagicString | undefined
nodeWorkerAssetUrlRE.lastIndex = 0
while ((match = nodeWorkerAssetUrlRE.exec(code))) {
s ||= new MagicString(code)
const [full, hash] = match
const filename = this.getFileName(hash)
const outputFilepath = toRelativePath(filename, chunk.fileName)
const replacement = JSON.stringify(outputFilepath)
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true
})
}
if (s) {
return {
code: s.toString(),
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
}
}
return null
}
}
}

View File

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

View File

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

View File

@ -1,192 +1,45 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */ import * as path from 'path'
import path from 'node:path' import * as fs from 'fs'
import fs from 'node:fs'
import { createHash } from 'node:crypto'
import { createRequire } from 'node:module'
import { type LibraryOptions, type Rolldown, loadEnv as viteLoadEnv } from 'vite'
export function isObject(value: unknown): value is Record<string, unknown> { export function isObject(value: unknown): value is Record<string, unknown> {
return Object.prototype.toString.call(value) === '[object Object]' return Object.prototype.toString.call(value) === '[object Object]'
} }
export const dynamicImport = new Function('file', 'return import(file)')
export function ensureElectronEntryFile(root = process.cwd()): void {
const pkg = path.join(root, 'package.json')
if (fs.existsSync(pkg)) {
const main = require(pkg).main
if (!main) {
throw new Error('not found an entry point to electorn app, please add main field for your package.json')
} else {
const entryPath = path.resolve(root, main)
if (!fs.existsSync(entryPath)) {
throw new Error(`not found the electorn app entry file: ${entryPath}`)
}
}
} else {
throw new Error('no package.json')
}
}
export function getElectronPath(): string {
const electronModulePath = path.resolve(process.cwd(), 'node_modules', 'electron')
const pathFile = path.join(electronModulePath, 'path.txt')
let executablePath
if (fs.existsSync(pathFile)) {
executablePath = fs.readFileSync(pathFile, 'utf-8')
}
if (executablePath) {
return path.join(electronModulePath, 'dist', executablePath)
} else {
throw new Error('Electron uninstall')
}
}
export const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000']) export 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 {
return typeof optionsHost === 'string' && !wildcardHosts.has(optionsHost) ? optionsHost : 'localhost' return typeof optionsHost === 'string' && !wildcardHosts.has(optionsHost) ? optionsHost : 'localhost'
} }
export const queryRE = /\?.*$/s
export const hashRE = /#.*$/s
export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '')
export function getHash(text: Buffer | string): string {
return createHash('sha256')
.update(text as unknown as Uint8Array)
.digest('hex')
.substring(0, 8)
}
export function toRelativePath(filename: string, importer: string): string {
const relPath = path.posix.relative(path.dirname(importer), filename)
return relPath.startsWith('.') ? relPath : `./${relPath}`
}
/**
* Resolve configured build outputs into an array.
*/
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.
*/
export function loadEnv(
mode: string,
envDir: string = process.cwd(),
prefixes: string | string[] = ['VITE_', 'MAIN_VITE_', 'PRELOAD_VITE_', 'RENDERER_VITE_']
): Record<string, string> {
return viteLoadEnv(mode, envDir, prefixes)
}
interface PackageData {
main?: string
type?: 'module' | 'commonjs'
dependencies?: Record<string, string>
}
let packageCached: PackageData | null = null
export function loadPackageData(root = process.cwd()): PackageData | null {
if (packageCached) return packageCached
const pkg = path.join(root, 'package.json')
if (fs.existsSync(pkg)) {
const _require = createRequire(import.meta.url)
const data = _require(pkg)
packageCached = {
main: data.main,
type: data.type,
dependencies: data.dependencies
}
return packageCached
}
return null
}
export function isFilePathESM(filePath: string): boolean {
if (/\.m[jt]s$/.test(filePath) || filePath.endsWith('.ts')) {
return true
} else if (/\.c[jt]s$/.test(filePath)) {
return false
} else {
const pkg = loadPackageData()
return pkg?.type === 'module'
}
}
type DeepWritable<T> =
T extends ReadonlyArray<unknown>
? { -readonly [P in keyof T]: DeepWritable<T[P]> }
: T extends RegExp
? RegExp
: T[keyof T] extends Function
? T
: { -readonly [P in keyof T]: DeepWritable<T[P]> }
export function deepClone<T>(value: T): DeepWritable<T> {
if (Array.isArray(value)) {
return value.map(v => deepClone(v)) as DeepWritable<T>
}
if (isObject(value)) {
const cloned: Record<string, any> = {}
for (const key in value) {
cloned[key] = deepClone(value[key])
}
return cloned as DeepWritable<T>
}
if (typeof value === 'function') {
return value as DeepWritable<T>
}
if (value instanceof RegExp) {
return new RegExp(value) as DeepWritable<T>
}
if (typeof value === 'object' && value != null) {
throw new Error('Cannot deep clone non-plain object')
}
return value as DeepWritable<T>
}
/**
* Isolate configuration data without cloning plugins or runtime instances.
* Plugins may capture their own object in hooks, so their identity must survive.
*/
export function cloneConfig<T>(config: T): T {
const seen = new WeakMap<object, unknown>()
const pluginArrays = new WeakMap<object, unknown>()
function clone(value: unknown, plugins = false): unknown {
if (value === null || typeof value !== 'object') return value
const cache = plugins ? pluginArrays : seen
if (cache.has(value)) return cache.get(value)
if (Array.isArray(value)) {
const result: unknown[] = new Array(value.length)
cache.set(value, result)
value.forEach((item, index) => {
result[index] = clone(item, plugins)
})
return result
}
// Preserve plugin objects, promises and other opaque plugin options.
if (plugins) return value
if (value instanceof RegExp) {
const result = new RegExp(value)
result.lastIndex = value.lastIndex
cache.set(value, result)
return result
}
const prototype = Object.getPrototypeOf(value)
if (prototype !== Object.prototype && prototype !== null) return value
const result = Object.create(prototype) as Record<PropertyKey, unknown>
cache.set(value, result)
for (const key of Reflect.ownKeys(value)) {
if (!Object.prototype.propertyIsEnumerable.call(value, key)) continue
Object.defineProperty(result, key, {
value: clone((value as Record<PropertyKey, unknown>)[key], key === 'plugins'),
enumerable: true,
configurable: true,
writable: true
})
}
return result
}
return clone(config) as T
}
type AsyncFlatten<T extends unknown[]> = T extends (infer U)[] ? Exclude<Awaited<U>, U[]>[] : never
export async function asyncFlatten<T extends unknown[]>(arr: T): Promise<AsyncFlatten<T>> {
do {
arr = (await Promise.all(arr)).flat(Infinity) as any
} while (arr.some((v: any) => v?.then))
return arr as unknown[] as AsyncFlatten<T>
}

View File

@ -1,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"]
} }