mirror of
https://github.com/alex8088/electron-vite.git
synced 2025-12-27 08:57:00 +08:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91368b6655 | ||
|
|
b1fd596afe | ||
|
|
438e9e7672 | ||
|
|
9eba4df577 | ||
|
|
465690ab0d | ||
|
|
6aae37833e | ||
|
|
9152dfc943 | ||
|
|
0276407b5b | ||
|
|
fe7e631f47 | ||
|
|
08be346407 | ||
|
|
3e9ded666c | ||
|
|
1bba6766e8 | ||
|
|
4edffe3b9a | ||
|
|
cfd9812a91 | ||
|
|
7c7f31b2a3 | ||
|
|
ae57b2489a | ||
|
|
397b02e384 | ||
|
|
a4f7693712 | ||
|
|
56fb519092 | ||
|
|
eb0a7e3ffe | ||
|
|
de70dfe1dc | ||
|
|
8892bf3679 | ||
|
|
2576484604 | ||
|
|
88f6db2239 | ||
|
|
0a79da03db | ||
|
|
c3939ade45 | ||
|
|
38228f9b3f | ||
|
|
8b193864fd | ||
|
|
f264d41d7f | ||
|
|
5debb6f83b | ||
|
|
d530597339 | ||
|
|
a9b5326544 | ||
|
|
7587d2c674 | ||
|
|
0fb8918090 | ||
|
|
c7955aa6fd | ||
|
|
70e027d38a | ||
|
|
28bb22b353 | ||
|
|
327adc23df | ||
|
|
4a6aea3704 | ||
|
|
3f7f65bf57 | ||
|
|
0badfc493f | ||
|
|
cbb039c3e9 | ||
|
|
ad50cba495 | ||
|
|
b55eb725c4 | ||
|
|
1eeb15ea77 | ||
|
|
52abc48abf | ||
|
|
cbee52c8bb | ||
|
|
48e6f4f570 | ||
|
|
4071778f07 | ||
|
|
3fd16d0c23 | ||
|
|
d2e8b1271b | ||
|
|
28d7df6e91 | ||
|
|
b7763a7f77 | ||
|
|
1c084cc090 | ||
|
|
9ec164d33e | ||
|
|
a9197f5cc9 | ||
|
|
3d5c9f68a1 | ||
|
|
1b411d3633 | ||
|
|
b56d3c2d21 | ||
|
|
f2eff25268 | ||
|
|
2d8e513e07 | ||
|
|
d8063320dc | ||
|
|
f33c5b2abe | ||
|
|
e91e70c105 | ||
|
|
ea144aef19 | ||
|
|
987c55ee8b | ||
|
|
8064bd81ff | ||
|
|
6e8572d9b7 | ||
|
|
4b47ef0bd4 | ||
|
|
5a5af050b2 | ||
|
|
96ae3c5cd9 | ||
|
|
79ac91dee2 | ||
|
|
1599d730f6 | ||
|
|
3c6e08b2f2 | ||
|
|
dfe6a3e3f8 | ||
|
|
6c01417909 | ||
|
|
5ffd49eddc | ||
|
|
bf1220875f |
@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
dist
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
commonjs: true,
|
|
||||||
es6: true,
|
|
||||||
node: true
|
|
||||||
},
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaVersion: 2022
|
|
||||||
},
|
|
||||||
plugins: ['@typescript-eslint'],
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
|
||||||
'plugin:prettier/recommended'
|
|
||||||
],
|
|
||||||
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',
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
||||||
'@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }],
|
|
||||||
'@typescript-eslint/no-explicit-any': 'error',
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
||||||
'@typescript-eslint/no-var-requires': 'off'
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['*.js'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
github: alex8088
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.eslintcache
|
||||||
*.log*
|
*.log*
|
||||||
|
|||||||
107
CHANGELOG.md
107
CHANGELOG.md
@ -1,3 +1,108 @@
|
|||||||
|
### 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_)
|
### v2.3.0 (_2024-06-23_)
|
||||||
|
|
||||||
- feat: resolve import.meta.\[dirname|filename\] to support CommonJS format
|
- feat: resolve import.meta.\[dirname|filename\] to support CommonJS format
|
||||||
@ -7,7 +112,7 @@
|
|||||||
- perf: build compatilibity target for Electron 31
|
- perf: build compatilibity target for Electron 31
|
||||||
- perf: improve cjs shim
|
- perf: improve cjs shim
|
||||||
- chore(deps): update all non-major dependencies
|
- chore(deps): update all non-major dependencies
|
||||||
- chore(deps): update @typescript-eslint/* to v7
|
- chore(deps): update @typescript-eslint/\* to v7
|
||||||
- chore(deps): update esbuild to v0.21
|
- chore(deps): update esbuild to v0.21
|
||||||
|
|
||||||
### v2.2.0 (_2024-04-21_)
|
### v2.2.0 (_2024-04-21_)
|
||||||
|
|||||||
16
README.md
16
README.md
@ -28,13 +28,13 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- ⚡️ [Vite](https://vitejs.dev) powered and use the same way.
|
- ⚡️ [Vite](https://vitejs.dev) powered and use the same way.
|
||||||
- 🛠 Pre-configured for Electron, don't worry about configuration.
|
- 🛠 Pre-configure with sensible defaults optimized for Electron.
|
||||||
- 💡 Optimize asset handling (Node.js addons, WebAssembly, Worker Thread, etc).
|
- 💡 Optimize asset handling for Electron main process.
|
||||||
- 🚀 Fast HMR for renderer processes.
|
- 🚀 Fast HMR & hot reloading.
|
||||||
- 🔥 Hot reloading for main process and preload scripts.
|
- 🔥 Isolated build for multi-entry application development.
|
||||||
- 🔌 Easy to debug in IDEs like VSCode or WebStorm.
|
- ✨ Simplify multi-threading development.
|
||||||
- 🔒 Compile to v8 bytecode to protect source code.
|
- 🔒 Compile code to v8 bytecode to protect source code.
|
||||||
- 🏷️ Support for TypeScript decorators.
|
- 🔌 Easy to debug in IDEs such as VSCode or WebStorm.
|
||||||
- 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more.
|
- 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -83,7 +83,7 @@ export default {
|
|||||||
Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project.
|
Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm create @quick-start/electron
|
npm create @quick-start/electron@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Currently supported template presets include:
|
Currently supported template presets include:
|
||||||
|
|||||||
@ -24,7 +24,7 @@ if (debugIndex > 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
import('../dist/cli.mjs')
|
import('../dist/cli.js')
|
||||||
}
|
}
|
||||||
|
|
||||||
run()
|
run()
|
||||||
|
|||||||
69
eslint.config.js
Normal file
69
eslint.config.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// ts-check
|
||||||
|
import { defineConfig } from 'eslint/config'
|
||||||
|
import eslint from '@eslint/js'
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||||
|
import globals from 'globals'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
|
{ ignores: ['**/node_modules', '**/dist', '**/bin'] },
|
||||||
|
eslint.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parser: tseslint.parser,
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2022
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
...globals.es2021,
|
||||||
|
...globals.node
|
||||||
|
}
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
node: {
|
||||||
|
version: '^20.19.0 || >=22.12.0'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': 'warn',
|
||||||
|
'no-empty': ['warn', { allowEmptyCatch: true }],
|
||||||
|
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],
|
||||||
|
'@typescript-eslint/explicit-function-return-type': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allowExpressions: true,
|
||||||
|
allowTypedFunctionExpressions: true,
|
||||||
|
allowHigherOrderFunctions: true,
|
||||||
|
allowIIFEs: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }],
|
||||||
|
'@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'always' }],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'error',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-require-imports': 'error',
|
||||||
|
'@typescript-eslint/no-unused-expressions': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allowShortCircuit: true,
|
||||||
|
allowTaggedTemplates: true,
|
||||||
|
allowTernary: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/consistent-type-imports': [
|
||||||
|
'error',
|
||||||
|
{ prefer: 'type-imports', disallowTypeAnnotations: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.js', '*.mjs'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
2
node.d.ts
vendored
2
node.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
// node worker
|
// node worker
|
||||||
declare module '*?nodeWorker' {
|
declare module '*?nodeWorker' {
|
||||||
import { Worker, WorkerOptions } from 'node:worker_threads'
|
import type { Worker, WorkerOptions } from 'node:worker_threads'
|
||||||
export default function (options: WorkerOptions): Worker
|
export default function (options: WorkerOptions): Worker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
74
package.json
74
package.json
@ -1,17 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "electron-vite",
|
"name": "electron-vite",
|
||||||
"version": "2.3.0",
|
"version": "5.0.0",
|
||||||
"description": "Electron build tooling based on Vite",
|
"description": "Electron build tooling based on Vite",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.cjs",
|
"main": "./dist/index.js",
|
||||||
"module": "dist/index.mjs",
|
"types": "./dist/index.d.ts",
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"import": "./dist/index.mjs",
|
|
||||||
"require": "./dist/index.cjs"
|
|
||||||
},
|
|
||||||
"./node": {
|
"./node": {
|
||||||
"types": "./node.d.ts"
|
"types": "./node.d.ts"
|
||||||
},
|
},
|
||||||
@ -26,9 +21,9 @@
|
|||||||
"node.d.ts"
|
"node.d.ts"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.0.0 || >=20.0.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.6.10",
|
"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": {
|
||||||
@ -47,7 +42,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "eslint --ext .ts src/**",
|
"lint": "eslint --cache .",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "pnpm run lint && rollup -c rollup.config.ts --configPlugin typescript"
|
"build": "pnpm run lint && rollup -c rollup.config.ts --configPlugin typescript"
|
||||||
},
|
},
|
||||||
@ -66,7 +61,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@swc/core": "^1.0.0",
|
"@swc/core": "^1.0.0",
|
||||||
"vite": "^4.0.0 || ^5.0.0"
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@swc/core": {
|
"@swc/core": {
|
||||||
@ -74,32 +69,41 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.37.0",
|
||||||
"@rollup/plugin-json": "^6.1.0",
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
"@rollup/plugin-typescript": "^12.1.4",
|
||||||
"@swc/core": "^1.6.5",
|
"@swc/core": "^1.13.5",
|
||||||
"@types/node": "^18.19.39",
|
"@types/babel__core": "^7.20.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
"@types/node": "^22.18.11",
|
||||||
"@typescript-eslint/parser": "^7.13.1",
|
"eslint": "^9.37.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"globals": "^16.4.0",
|
||||||
"lint-staged": "^15.2.7",
|
"lint-staged": "^16.2.4",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.6.2",
|
||||||
"rollup": "^4.18.0",
|
"rollup": "^4.52.4",
|
||||||
"rollup-plugin-dts": "^6.1.1",
|
"rollup-plugin-dts": "^6.2.3",
|
||||||
"rollup-plugin-rm": "^1.0.2",
|
"rollup-plugin-rm": "^1.0.2",
|
||||||
"simple-git-hooks": "^2.11.1",
|
"simple-git-hooks": "^2.13.1",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^5.3.1"
|
"typescript-eslint": "^8.46.1",
|
||||||
|
"vite": "^7.1.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.24.7",
|
"@babel/core": "^7.28.4",
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.24.7",
|
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"esbuild": "^0.21.5",
|
"esbuild": "^0.25.11",
|
||||||
"magic-string": "^0.30.10",
|
"magic-string": "^0.30.19",
|
||||||
"picocolors": "^1.0.1"
|
"picocolors": "^1.1.1"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@swc/core",
|
||||||
|
"esbuild",
|
||||||
|
"simple-git-hooks"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3949
pnpm-lock.yaml
generated
3949
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@ import rm from 'rollup-plugin-rm'
|
|||||||
const require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
const pkg = require('./package.json')
|
const pkg = require('./package.json')
|
||||||
|
|
||||||
const external = ['esbuild', ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]
|
const external = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
{
|
{
|
||||||
@ -17,14 +17,8 @@ export default defineConfig([
|
|||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
dir: 'dist',
|
dir: 'dist',
|
||||||
entryFileNames: '[name].cjs',
|
entryFileNames: '[name].js',
|
||||||
chunkFileNames: 'chunks/lib-[hash].cjs',
|
chunkFileNames: 'chunks/lib-[hash].js',
|
||||||
format: 'cjs'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dir: 'dist',
|
|
||||||
entryFileNames: '[name].mjs',
|
|
||||||
chunkFileNames: 'chunks/lib-[hash].mjs',
|
|
||||||
format: 'es'
|
format: 'es'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@ -7,7 +7,7 @@ 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()
|
||||||
|
|||||||
37
src/build.ts
37
src/build.ts
@ -1,5 +1,5 @@
|
|||||||
import { build as viteBuild } from 'vite'
|
import { build as viteBuild } from 'vite'
|
||||||
import { InlineConfig, resolveConfig } from './config'
|
import { type InlineConfig, resolveConfig } from './config'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bundles the electron app for production.
|
* Bundles the electron app for production.
|
||||||
@ -7,27 +7,22 @@ import { InlineConfig, resolveConfig } from './config'
|
|||||||
export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
|
export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
|
||||||
process.env.NODE_ENV_ELECTRON_VITE = 'production'
|
process.env.NODE_ENV_ELECTRON_VITE = 'production'
|
||||||
const config = await resolveConfig(inlineConfig, 'build', 'production')
|
const config = await resolveConfig(inlineConfig, 'build', 'production')
|
||||||
if (config.config) {
|
|
||||||
const mainViteConfig = config.config?.main
|
if (!config.config) {
|
||||||
if (mainViteConfig) {
|
return
|
||||||
if (mainViteConfig.build?.watch) {
|
}
|
||||||
mainViteConfig.build.watch = null
|
|
||||||
|
// Build targets in order: main -> preload -> renderer
|
||||||
|
const buildTargets = ['main', 'preload', 'renderer'] as const
|
||||||
|
|
||||||
|
for (const target of buildTargets) {
|
||||||
|
const viteConfig = config.config[target]
|
||||||
|
if (viteConfig) {
|
||||||
|
// Disable watch mode in production builds
|
||||||
|
if (viteConfig.build?.watch) {
|
||||||
|
viteConfig.build.watch = null
|
||||||
}
|
}
|
||||||
await viteBuild(mainViteConfig)
|
await viteBuild(viteConfig)
|
||||||
}
|
|
||||||
const preloadViteConfig = config.config?.preload
|
|
||||||
if (preloadViteConfig) {
|
|
||||||
if (preloadViteConfig.build?.watch) {
|
|
||||||
preloadViteConfig.build.watch = null
|
|
||||||
}
|
|
||||||
await viteBuild(preloadViteConfig)
|
|
||||||
}
|
|
||||||
const rendererViteConfig = config.config?.renderer
|
|
||||||
if (rendererViteConfig) {
|
|
||||||
if (rendererViteConfig.build?.watch) {
|
|
||||||
rendererViteConfig.build.watch = null
|
|
||||||
}
|
|
||||||
await viteBuild(rendererViteConfig)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { cac } from 'cac'
|
import { cac } from 'cac'
|
||||||
import colors from 'picocolors'
|
import colors from 'picocolors'
|
||||||
import { LogLevel, createLogger } from 'vite'
|
import { type LogLevel, createLogger } from 'vite'
|
||||||
import { InlineConfig } from './config'
|
import type { InlineConfig } from './config'
|
||||||
import { version } from '../package.json'
|
import { version } from '../package.json'
|
||||||
|
|
||||||
const cli = cac('electron-vite')
|
const cli = cac('electron-vite')
|
||||||
|
|||||||
335
src/config.ts
335
src/config.ts
@ -5,9 +5,10 @@ import { createRequire } from 'node:module'
|
|||||||
import colors from 'picocolors'
|
import colors from 'picocolors'
|
||||||
import {
|
import {
|
||||||
type UserConfig as ViteConfig,
|
type UserConfig as ViteConfig,
|
||||||
type UserConfigExport as ViteConfigExport,
|
|
||||||
type ConfigEnv,
|
type ConfigEnv,
|
||||||
|
type PluginOption,
|
||||||
type Plugin,
|
type Plugin,
|
||||||
|
type BuildEnvironmentOptions as ViteBuildOptions,
|
||||||
type LogLevel,
|
type LogLevel,
|
||||||
createLogger,
|
createLogger,
|
||||||
mergeConfig,
|
mergeConfig,
|
||||||
@ -15,89 +16,138 @@ import {
|
|||||||
} from 'vite'
|
} from 'vite'
|
||||||
import { build } from 'esbuild'
|
import { build } from 'esbuild'
|
||||||
|
|
||||||
import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugins/electron'
|
import {
|
||||||
|
electronMainConfigPresetPlugin,
|
||||||
|
electronMainConfigValidatorPlugin,
|
||||||
|
electronPreloadConfigPresetPlugin,
|
||||||
|
electronPreloadConfigValidatorPlugin,
|
||||||
|
electronRendererConfigPresetPlugin,
|
||||||
|
electronRendererConfigValidatorPlugin
|
||||||
|
} from './plugins/electron'
|
||||||
import assetPlugin from './plugins/asset'
|
import assetPlugin from './plugins/asset'
|
||||||
import workerPlugin from './plugins/worker'
|
import workerPlugin from './plugins/worker'
|
||||||
import importMetaPlugin from './plugins/importMeta'
|
import importMetaPlugin from './plugins/importMeta'
|
||||||
import esmShimPlugin from './plugins/esm'
|
import esmShimPlugin from './plugins/esmShim'
|
||||||
import modulePathPlugin from './plugins/modulePath'
|
import modulePathPlugin from './plugins/modulePath'
|
||||||
import { isObject, isFilePathESM } from './utils'
|
import isolateEntriesPlugin from './plugins/isolateEntries'
|
||||||
|
import { type ExternalOptions, externalizeDepsPlugin } from './plugins/externalizeDeps'
|
||||||
|
import { type BytecodeOptions, bytecodePlugin } from './plugins/bytecode'
|
||||||
|
import { isObject, isFilePathESM, deepClone, asyncFlatten } from './utils'
|
||||||
|
|
||||||
export { defineConfig as defineViteConfig } from 'vite'
|
export { defineConfig as defineViteConfig } from 'vite'
|
||||||
|
|
||||||
|
interface IsolatedEntriesMixin {
|
||||||
|
/**
|
||||||
|
* Build each entry point as an isolated bundle without code splitting.
|
||||||
|
*
|
||||||
|
* When enabled, each entry will include all its dependencies inline,
|
||||||
|
* preventing automatic code splitting across entries and ensuring each
|
||||||
|
* output file is fully standalone.
|
||||||
|
*
|
||||||
|
* **Important**: When using `isolatedEntries` in `preload` config, you
|
||||||
|
* should also disable `build.externalizeDeps` to ensure third-party dependencies
|
||||||
|
* from `node_modules` are bundled together, which is required for Electron
|
||||||
|
* sandbox support.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
isolatedEntries?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExternalizeDepsMixin {
|
||||||
|
/**
|
||||||
|
* Options pass on to `externalizeDeps` plugin in electron-vite.
|
||||||
|
*
|
||||||
|
* Automatically externalize dependencies.
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
externalizeDeps?: boolean | ExternalOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BytecodeMixin {
|
||||||
|
/**
|
||||||
|
* Options pass on to `bytecode` plugin in electron-vite.
|
||||||
|
* https://electron-vite.org/guide/source-code-protection#options
|
||||||
|
*
|
||||||
|
* Compile source code to v8 bytecode.
|
||||||
|
*/
|
||||||
|
bytecode?: boolean | BytecodeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MainBuildOptions extends ViteBuildOptions, ExternalizeDepsMixin, BytecodeMixin {}
|
||||||
|
|
||||||
|
interface PreloadBuildOptions extends ViteBuildOptions, ExternalizeDepsMixin, BytecodeMixin, IsolatedEntriesMixin {}
|
||||||
|
|
||||||
|
interface RendererBuildOptions extends ViteBuildOptions, IsolatedEntriesMixin {}
|
||||||
|
|
||||||
|
interface BaseViteConfig<T> extends Omit<ViteConfig, 'build'> {
|
||||||
|
/**
|
||||||
|
* Build specific options
|
||||||
|
*/
|
||||||
|
build?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainViteConfig extends BaseViteConfig<MainBuildOptions> {}
|
||||||
|
|
||||||
|
export interface PreloadViteConfig extends BaseViteConfig<PreloadBuildOptions> {}
|
||||||
|
|
||||||
|
export interface RendererViteConfig extends BaseViteConfig<RendererBuildOptions> {}
|
||||||
|
|
||||||
export interface UserConfig {
|
export interface UserConfig {
|
||||||
/**
|
/**
|
||||||
* Vite config options for electron main process
|
* Vite config options for electron main process
|
||||||
*
|
*
|
||||||
* https://vitejs.dev/config/
|
* @see https://vitejs.dev/config/
|
||||||
*/
|
*/
|
||||||
main?: ViteConfig & { configFile?: string | false }
|
main?: MainViteConfig
|
||||||
/**
|
/**
|
||||||
* Vite config options for electron renderer process
|
* Vite config options for electron renderer process
|
||||||
*
|
*
|
||||||
* https://vitejs.dev/config/
|
* @see https://vitejs.dev/config/
|
||||||
*/
|
*/
|
||||||
renderer?: ViteConfig & { configFile?: string | false }
|
renderer?: RendererViteConfig
|
||||||
/**
|
/**
|
||||||
* Vite config options for electron preload files
|
* Vite config options for electron preload scripts
|
||||||
*
|
*
|
||||||
* https://vitejs.dev/config/
|
* @see https://vitejs.dev/config/
|
||||||
*/
|
*/
|
||||||
preload?: ViteConfig & { configFile?: string | false }
|
preload?: PreloadViteConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ElectronViteConfig {
|
export type ElectronViteConfigFnObject = (env: ConfigEnv) => UserConfig
|
||||||
/**
|
export type ElectronViteConfigFnPromise = (env: ConfigEnv) => Promise<UserConfig>
|
||||||
* Vite config options for electron main process
|
export type ElectronViteConfigFn = (env: ConfigEnv) => UserConfig | Promise<UserConfig>
|
||||||
*
|
|
||||||
* https://vitejs.dev/config/
|
|
||||||
*/
|
|
||||||
main?: ViteConfigExport
|
|
||||||
/**
|
|
||||||
* Vite config options for electron renderer process
|
|
||||||
*
|
|
||||||
* https://vitejs.dev/config/
|
|
||||||
*/
|
|
||||||
renderer?: ViteConfigExport
|
|
||||||
/**
|
|
||||||
* Vite config options for electron preload files
|
|
||||||
*
|
|
||||||
* https://vitejs.dev/config/
|
|
||||||
*/
|
|
||||||
preload?: ViteConfigExport
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InlineConfig = Omit<ViteConfig, 'base'> & {
|
|
||||||
configFile?: string | false
|
|
||||||
envFile?: false
|
|
||||||
ignoreConfigWarning?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ElectronViteConfigFnObject = (env: ConfigEnv) => ElectronViteConfig
|
|
||||||
export type ElectronViteConfigFnPromise = (env: ConfigEnv) => Promise<ElectronViteConfig>
|
|
||||||
export type ElectronViteConfigFn = (env: ConfigEnv) => ElectronViteConfig | Promise<ElectronViteConfig>
|
|
||||||
|
|
||||||
export type ElectronViteConfigExport =
|
export type ElectronViteConfigExport =
|
||||||
| ElectronViteConfig
|
| UserConfig
|
||||||
| Promise<ElectronViteConfig>
|
| Promise<UserConfig>
|
||||||
| ElectronViteConfigFnObject
|
| ElectronViteConfigFnObject
|
||||||
| ElectronViteConfigFnPromise
|
| ElectronViteConfigFnPromise
|
||||||
| ElectronViteConfigFn
|
| ElectronViteConfigFn
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type helper to make it easier to use `electron.vite.config.*`
|
* Type helper to make it easier to use `electron.vite.config.*`
|
||||||
* accepts a direct {@link ElectronViteConfig} object, or a function that returns it.
|
* accepts a direct {@link UserConfig} object, or a function that returns it.
|
||||||
* The function receives a object that exposes two properties:
|
* The function receives a object that exposes two properties:
|
||||||
* `command` (either `'build'` or `'serve'`), and `mode`.
|
* `command` (either `'build'` or `'serve'`), and `mode`.
|
||||||
*/
|
*/
|
||||||
export function defineConfig(config: ElectronViteConfig): ElectronViteConfig
|
export function defineConfig(config: UserConfig): UserConfig
|
||||||
export function defineConfig(config: Promise<ElectronViteConfig>): Promise<ElectronViteConfig>
|
export function defineConfig(config: Promise<UserConfig>): Promise<UserConfig>
|
||||||
export function defineConfig(config: ElectronViteConfigFnObject): ElectronViteConfigFnObject
|
export function defineConfig(config: ElectronViteConfigFnObject): ElectronViteConfigFnObject
|
||||||
|
export function defineConfig(config: ElectronViteConfigFnPromise): ElectronViteConfigFnPromise
|
||||||
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport
|
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport
|
||||||
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport {
|
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InlineConfig = Omit<ViteConfig, 'base'> & {
|
||||||
|
configFile?: string | false
|
||||||
|
envFile?: false
|
||||||
|
ignoreConfigWarning?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface ResolvedConfig {
|
export interface ResolvedConfig {
|
||||||
config?: UserConfig
|
config?: UserConfig
|
||||||
configFile?: string
|
configFile?: string
|
||||||
@ -123,6 +173,7 @@ export async function resolveConfig(
|
|||||||
mode,
|
mode,
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadResult = await loadConfigFromFile(
|
const loadResult = await loadConfigFromFile(
|
||||||
configEnv,
|
configEnv,
|
||||||
configFile,
|
configFile,
|
||||||
@ -130,15 +181,18 @@ export async function resolveConfig(
|
|||||||
config.logLevel,
|
config.logLevel,
|
||||||
config.ignoreConfigWarning
|
config.ignoreConfigWarning
|
||||||
)
|
)
|
||||||
|
|
||||||
if (loadResult) {
|
if (loadResult) {
|
||||||
const root = config.root
|
const root = config.root
|
||||||
delete config.root
|
delete config.root
|
||||||
delete config.configFile
|
delete config.configFile
|
||||||
|
|
||||||
|
config.configFile = false
|
||||||
|
|
||||||
const outDir = config.build?.outDir
|
const outDir = config.build?.outDir
|
||||||
|
|
||||||
if (loadResult.config.main) {
|
if (loadResult.config.main) {
|
||||||
const mainViteConfig: ViteConfig = mergeConfig(loadResult.config.main, deepClone(config))
|
const mainViteConfig: MainViteConfig = mergeConfig(loadResult.config.main, deepClone(config))
|
||||||
|
|
||||||
mainViteConfig.mode = inlineConfig.mode || mainViteConfig.mode || defaultMode
|
mainViteConfig.mode = inlineConfig.mode || mainViteConfig.mode || defaultMode
|
||||||
|
|
||||||
@ -146,40 +200,83 @@ export async function resolveConfig(
|
|||||||
resetOutDir(mainViteConfig, outDir, 'main')
|
resetOutDir(mainViteConfig, outDir, 'main')
|
||||||
}
|
}
|
||||||
|
|
||||||
mergePlugins(mainViteConfig, [
|
const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(mainViteConfig)
|
||||||
...electronMainVitePlugin({ root }),
|
|
||||||
|
const builtInMainPlugins: PluginOption[] = [
|
||||||
|
electronMainConfigPresetPlugin({ root }),
|
||||||
|
electronMainConfigValidatorPlugin(),
|
||||||
assetPlugin(),
|
assetPlugin(),
|
||||||
workerPlugin(),
|
workerPlugin(),
|
||||||
modulePathPlugin(),
|
modulePathPlugin(
|
||||||
|
mergeConfig(
|
||||||
|
{
|
||||||
|
plugins: [
|
||||||
|
electronMainConfigPresetPlugin({ root }),
|
||||||
|
assetPlugin(),
|
||||||
|
importMetaPlugin(),
|
||||||
|
esmShimPlugin(),
|
||||||
|
...configDrivenPlugins
|
||||||
|
]
|
||||||
|
},
|
||||||
|
mainViteConfig
|
||||||
|
)
|
||||||
|
),
|
||||||
importMetaPlugin(),
|
importMetaPlugin(),
|
||||||
esmShimPlugin()
|
esmShimPlugin(),
|
||||||
])
|
...configDrivenPlugins
|
||||||
|
]
|
||||||
|
|
||||||
|
mainViteConfig.plugins = builtInMainPlugins.concat(mainViteConfig.plugins || [])
|
||||||
|
|
||||||
loadResult.config.main = mainViteConfig
|
loadResult.config.main = mainViteConfig
|
||||||
loadResult.config.main.configFile = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadResult.config.preload) {
|
if (loadResult.config.preload) {
|
||||||
const preloadViteConfig: ViteConfig = mergeConfig(loadResult.config.preload, deepClone(config))
|
const preloadViteConfig: PreloadViteConfig = mergeConfig(loadResult.config.preload, deepClone(config))
|
||||||
|
|
||||||
preloadViteConfig.mode = inlineConfig.mode || preloadViteConfig.mode || defaultMode
|
preloadViteConfig.mode = inlineConfig.mode || preloadViteConfig.mode || defaultMode
|
||||||
|
|
||||||
if (outDir) {
|
if (outDir) {
|
||||||
resetOutDir(preloadViteConfig, outDir, 'preload')
|
resetOutDir(preloadViteConfig, outDir, 'preload')
|
||||||
}
|
}
|
||||||
mergePlugins(preloadViteConfig, [
|
|
||||||
...electronPreloadVitePlugin({ root }),
|
const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(preloadViteConfig)
|
||||||
|
|
||||||
|
const builtInPreloadPlugins: PluginOption[] = [
|
||||||
|
electronPreloadConfigPresetPlugin({ root }),
|
||||||
|
electronPreloadConfigValidatorPlugin(),
|
||||||
assetPlugin(),
|
assetPlugin(),
|
||||||
importMetaPlugin(),
|
importMetaPlugin(),
|
||||||
esmShimPlugin()
|
esmShimPlugin(),
|
||||||
])
|
...configDrivenPlugins
|
||||||
|
]
|
||||||
|
|
||||||
|
if (preloadViteConfig.build?.isolatedEntries) {
|
||||||
|
builtInPreloadPlugins.push(
|
||||||
|
isolateEntriesPlugin(
|
||||||
|
mergeConfig(
|
||||||
|
{
|
||||||
|
plugins: [
|
||||||
|
electronPreloadConfigPresetPlugin({ root }),
|
||||||
|
assetPlugin(),
|
||||||
|
importMetaPlugin(),
|
||||||
|
esmShimPlugin(),
|
||||||
|
...configDrivenPlugins
|
||||||
|
]
|
||||||
|
},
|
||||||
|
preloadViteConfig
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
preloadViteConfig.plugins = builtInPreloadPlugins.concat(preloadViteConfig.plugins)
|
||||||
|
|
||||||
loadResult.config.preload = preloadViteConfig
|
loadResult.config.preload = preloadViteConfig
|
||||||
loadResult.config.preload.configFile = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadResult.config.renderer) {
|
if (loadResult.config.renderer) {
|
||||||
const rendererViteConfig: ViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config))
|
const rendererViteConfig: RendererViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config))
|
||||||
|
|
||||||
rendererViteConfig.mode = inlineConfig.mode || rendererViteConfig.mode || defaultMode
|
rendererViteConfig.mode = inlineConfig.mode || rendererViteConfig.mode || defaultMode
|
||||||
|
|
||||||
@ -187,10 +284,27 @@ export async function resolveConfig(
|
|||||||
resetOutDir(rendererViteConfig, outDir, 'renderer')
|
resetOutDir(rendererViteConfig, outDir, 'renderer')
|
||||||
}
|
}
|
||||||
|
|
||||||
mergePlugins(rendererViteConfig, electronRendererVitePlugin({ root }))
|
const builtInRendererPlugins: PluginOption[] = [
|
||||||
|
electronRendererConfigPresetPlugin({ root }),
|
||||||
|
electronRendererConfigValidatorPlugin()
|
||||||
|
]
|
||||||
|
|
||||||
|
if (rendererViteConfig.build?.isolatedEntries) {
|
||||||
|
builtInRendererPlugins.push(
|
||||||
|
isolateEntriesPlugin(
|
||||||
|
mergeConfig(
|
||||||
|
{
|
||||||
|
plugins: [electronRendererConfigPresetPlugin({ root })]
|
||||||
|
},
|
||||||
|
rendererViteConfig
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
rendererViteConfig.plugins = builtInRendererPlugins.concat(rendererViteConfig.plugins || [])
|
||||||
|
|
||||||
loadResult.config.renderer = rendererViteConfig
|
loadResult.config.renderer = rendererViteConfig
|
||||||
loadResult.config.renderer.configFile = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userConfig = loadResult.config
|
userConfig = loadResult.config
|
||||||
@ -208,10 +322,6 @@ export async function resolveConfig(
|
|||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
function deepClone<T>(data: T): T {
|
|
||||||
return JSON.parse(JSON.stringify(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void {
|
function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void {
|
||||||
let userOutDir = config.build?.outDir
|
let userOutDir = config.build?.outDir
|
||||||
if (outDir === userOutDir) {
|
if (outDir === userOutDir) {
|
||||||
@ -224,9 +334,36 @@ function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): voi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergePlugins(config: ViteConfig, plugins: Plugin[]): void {
|
async function resolveConfigDrivenPlugins(config: MainViteConfig | PreloadViteConfig): Promise<PluginOption[]> {
|
||||||
const userPlugins = config.plugins || []
|
const userPlugins = (await asyncFlatten(config.plugins || [])).filter(Boolean) as Plugin[]
|
||||||
config.plugins = userPlugins.concat(plugins)
|
|
||||||
|
const configDrivenPlugins: PluginOption[] = []
|
||||||
|
|
||||||
|
const hasExternalizeDepsPlugin = userPlugins.some(p => p.name === 'vite:externalize-deps')
|
||||||
|
if (!hasExternalizeDepsPlugin) {
|
||||||
|
const externalOptions = config.build?.externalizeDeps ?? true
|
||||||
|
if (externalOptions) {
|
||||||
|
isOptions<ExternalOptions>(externalOptions)
|
||||||
|
? configDrivenPlugins.push(externalizeDepsPlugin(externalOptions))
|
||||||
|
: configDrivenPlugins.push(externalizeDepsPlugin())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasBytecodePlugin = userPlugins.some(p => p.name === 'vite:bytecode')
|
||||||
|
if (!hasBytecodePlugin) {
|
||||||
|
const bytecodeOptions = config.build?.bytecode
|
||||||
|
if (bytecodeOptions) {
|
||||||
|
isOptions<BytecodeOptions>(bytecodeOptions)
|
||||||
|
? configDrivenPlugins.push(bytecodePlugin(bytecodeOptions))
|
||||||
|
: configDrivenPlugins.push(bytecodePlugin())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configDrivenPlugins
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOptions<T extends object>(value: boolean | T): value is T {
|
||||||
|
return typeof value === 'object' && value !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_FILE_NAME = 'electron.vite.config'
|
const CONFIG_FILE_NAME = 'electron.vite.config'
|
||||||
@ -261,63 +398,27 @@ export async function loadConfigFromFile(
|
|||||||
const isESM = isFilePathESM(resolvedPath)
|
const isESM = isFilePathESM(resolvedPath)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bundled = await bundleConfigFile(resolvedPath, isESM)
|
const { code, dependencies } = await bundleConfigFile(resolvedPath, isESM)
|
||||||
const userConfig = await loadConfigFormBundledFile(configRoot, resolvedPath, bundled.code, isESM)
|
const configExport = await loadConfigFormBundledFile(configRoot, resolvedPath, code, isESM)
|
||||||
|
|
||||||
const config = await (typeof userConfig === 'function' ? userConfig(configEnv) : userConfig)
|
const config = await (typeof configExport === 'function' ? configExport(configEnv) : configExport)
|
||||||
if (!isObject(config)) {
|
if (!isObject(config)) {
|
||||||
throw new Error(`config must export or return an object`)
|
throw new Error(`config must export or return an object`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const configRequired: string[] = []
|
if (!ignoreConfigWarning) {
|
||||||
|
const missingFields = ['main', 'renderer', 'preload'].filter(field => !config[field])
|
||||||
let mainConfig
|
if (missingFields.length > 0) {
|
||||||
if (config.main) {
|
createLogger(logLevel).warn(
|
||||||
const mainViteConfig = config.main
|
`${colors.yellow(colors.bold('(!)'))} ${colors.yellow(`${missingFields.join(' and ')} config is missing`)}\n`
|
||||||
mainConfig = await (typeof mainViteConfig === 'function' ? mainViteConfig(configEnv) : mainViteConfig)
|
)
|
||||||
if (!isObject(mainConfig)) {
|
|
||||||
throw new Error(`main config must export or return an object`)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
configRequired.push('main')
|
|
||||||
}
|
|
||||||
|
|
||||||
let rendererConfig
|
|
||||||
if (config.renderer) {
|
|
||||||
const rendererViteConfig = config.renderer
|
|
||||||
rendererConfig = await (typeof rendererViteConfig === 'function'
|
|
||||||
? rendererViteConfig(configEnv)
|
|
||||||
: rendererViteConfig)
|
|
||||||
if (!isObject(rendererConfig)) {
|
|
||||||
throw new Error(`renderer config must export or return an object`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
configRequired.push('renderer')
|
|
||||||
}
|
|
||||||
|
|
||||||
let preloadConfig
|
|
||||||
if (config.preload) {
|
|
||||||
const preloadViteConfig = config.preload
|
|
||||||
preloadConfig = await (typeof preloadViteConfig === 'function' ? preloadViteConfig(configEnv) : preloadViteConfig)
|
|
||||||
if (!isObject(preloadConfig)) {
|
|
||||||
throw new Error(`preload config must export or return an object`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
configRequired.push('preload')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ignoreConfigWarning && configRequired.length > 0) {
|
|
||||||
createLogger(logLevel).warn(colors.yellow(`${configRequired.join(' and ')} config is missing`))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: normalizePath(resolvedPath),
|
path: normalizePath(resolvedPath),
|
||||||
config: {
|
config,
|
||||||
main: mainConfig,
|
dependencies
|
||||||
renderer: rendererConfig,
|
|
||||||
preload: preloadConfig
|
|
||||||
},
|
|
||||||
dependencies: bundled.dependencies
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
createLogger(logLevel).error(colors.red(`failed to load config from ${resolvedPath}`), { error: e as Error })
|
createLogger(logLevel).error(colors.red(`failed to load config from ${resolvedPath}`), { error: e as Error })
|
||||||
@ -343,7 +444,7 @@ async function bundleConfigFile(fileName: string, isESM: boolean): Promise<{ cod
|
|||||||
absWorkingDir: process.cwd(),
|
absWorkingDir: process.cwd(),
|
||||||
entryPoints: [fileName],
|
entryPoints: [fileName],
|
||||||
write: false,
|
write: false,
|
||||||
target: ['node18'],
|
target: ['node20'],
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: isESM ? 'esm' : 'cjs',
|
format: isESM ? 'esm' : 'cjs',
|
||||||
|
|||||||
@ -41,9 +41,9 @@ export function supportESM(): boolean {
|
|||||||
return parseInt(majorVer) >= 28
|
return parseInt(majorVer) >= 28
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getElectronMajorVersion(): number {
|
export function supportImportMetaPaths(): boolean {
|
||||||
const majorVer = getElectronMajorVer()
|
const majorVer = getElectronMajorVer()
|
||||||
return parseInt(majorVer)
|
return parseInt(majorVer) >= 30
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getElectronPath(): string {
|
export function getElectronPath(): string {
|
||||||
@ -69,6 +69,14 @@ export function getElectronNodeTarget(): string {
|
|||||||
const electronVer = getElectronMajorVer()
|
const electronVer = getElectronMajorVer()
|
||||||
|
|
||||||
const nodeVer = {
|
const nodeVer = {
|
||||||
|
'39': '22.20',
|
||||||
|
'38': '22.19',
|
||||||
|
'37': '22.16',
|
||||||
|
'36': '22.14',
|
||||||
|
'35': '22.14',
|
||||||
|
'34': '20.18',
|
||||||
|
'33': '20.18',
|
||||||
|
'32': '20.16',
|
||||||
'31': '20.14',
|
'31': '20.14',
|
||||||
'30': '20.11',
|
'30': '20.11',
|
||||||
'29': '20.9',
|
'29': '20.9',
|
||||||
@ -78,16 +86,7 @@ export function getElectronNodeTarget(): string {
|
|||||||
'25': '18.15',
|
'25': '18.15',
|
||||||
'24': '18.14',
|
'24': '18.14',
|
||||||
'23': '18.12',
|
'23': '18.12',
|
||||||
'22': '16.17',
|
'22': '16.17'
|
||||||
'21': '16.16',
|
|
||||||
'20': '16.15',
|
|
||||||
'19': '16.14',
|
|
||||||
'18': '16.13',
|
|
||||||
'17': '16.13',
|
|
||||||
'16': '16.9',
|
|
||||||
'15': '16.5',
|
|
||||||
'14': '14.17',
|
|
||||||
'13': '14.17'
|
|
||||||
}
|
}
|
||||||
if (electronVer && parseInt(electronVer) > 10) {
|
if (electronVer && parseInt(electronVer) > 10) {
|
||||||
let target = nodeVer[electronVer]
|
let target = nodeVer[electronVer]
|
||||||
@ -101,6 +100,14 @@ export function getElectronChromeTarget(): string {
|
|||||||
const electronVer = getElectronMajorVer()
|
const electronVer = getElectronMajorVer()
|
||||||
|
|
||||||
const chromeVer = {
|
const chromeVer = {
|
||||||
|
'39': '142',
|
||||||
|
'38': '140',
|
||||||
|
'37': '138',
|
||||||
|
'36': '136',
|
||||||
|
'35': '134',
|
||||||
|
'34': '132',
|
||||||
|
'33': '130',
|
||||||
|
'32': '128',
|
||||||
'31': '126',
|
'31': '126',
|
||||||
'30': '124',
|
'30': '124',
|
||||||
'29': '122',
|
'29': '122',
|
||||||
@ -110,16 +117,7 @@ export function getElectronChromeTarget(): string {
|
|||||||
'25': '114',
|
'25': '114',
|
||||||
'24': '112',
|
'24': '112',
|
||||||
'23': '110',
|
'23': '110',
|
||||||
'22': '108',
|
'22': '108'
|
||||||
'21': '106',
|
|
||||||
'20': '104',
|
|
||||||
'19': '102',
|
|
||||||
'18': '100',
|
|
||||||
'17': '98',
|
|
||||||
'16': '96',
|
|
||||||
'15': '94',
|
|
||||||
'14': '93',
|
|
||||||
'13': '91'
|
|
||||||
}
|
}
|
||||||
if (electronVer && parseInt(electronVer) > 10) {
|
if (electronVer && parseInt(electronVer) > 10) {
|
||||||
let target = chromeVer[electronVer]
|
let target = chromeVer[electronVer]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export { type LogLevel, createLogger, mergeConfig, splitVendorChunkPlugin, splitVendorChunk } from 'vite'
|
export { type LogLevel, createLogger, mergeConfig } from 'vite'
|
||||||
export * from './config'
|
export * from './config'
|
||||||
export { createServer } from './server'
|
export { createServer } from './server'
|
||||||
export { build } from './build'
|
export { build } from './build'
|
||||||
|
|||||||
@ -3,48 +3,16 @@ import fs from 'node:fs/promises'
|
|||||||
import type { SourceMapInput } from 'rollup'
|
import type { SourceMapInput } from 'rollup'
|
||||||
import { type Plugin, normalizePath } from 'vite'
|
import { type Plugin, normalizePath } from 'vite'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { cleanUrl, parseRequest, getHash, toRelativePath } from '../utils'
|
import { cleanUrl, getHash, toRelativePath } from '../utils'
|
||||||
|
import { supportImportMetaPaths } from '../electron'
|
||||||
interface AssetResolved {
|
|
||||||
type: 'asset' | 'native' | 'wasm'
|
|
||||||
file: string
|
|
||||||
query: Record<string, string> | null
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveAsset(id: string): AssetResolved | null {
|
|
||||||
const file = cleanUrl(id)
|
|
||||||
const query = parseRequest(id)
|
|
||||||
|
|
||||||
if (query && typeof query.asset === 'string') {
|
|
||||||
return {
|
|
||||||
type: 'asset',
|
|
||||||
file,
|
|
||||||
query
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.endsWith('.node')) {
|
|
||||||
return {
|
|
||||||
type: 'native',
|
|
||||||
file,
|
|
||||||
query
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.endsWith('.wasm?loader')) {
|
|
||||||
return {
|
|
||||||
type: 'wasm',
|
|
||||||
file,
|
|
||||||
query
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeAssetRE = /__VITE_NODE_ASSET__([\w$]+)__/g
|
const nodeAssetRE = /__VITE_NODE_ASSET__([\w$]+)__/g
|
||||||
const nodePublicAssetRE = /__VITE_NODE_PUBLIC_ASSET__([a-z\d]{8})__/g
|
const nodePublicAssetRE = /__VITE_NODE_PUBLIC_ASSET__([a-z\d]{8})__/g
|
||||||
|
|
||||||
|
const assetImportRE = /(?:[?|&]asset(?:&|$)|\.wasm\?loader$|\.node$)/
|
||||||
|
const assetRE = /[?|&]asset(?:&|$)/
|
||||||
|
const assetUnpackRE = /[?|&]asset&asarUnpack$/
|
||||||
|
|
||||||
const wasmHelperId = '\0__electron-vite-wasm-helper'
|
const wasmHelperId = '\0__electron-vite-wasm-helper'
|
||||||
|
|
||||||
const wasmHelperCode = `
|
const wasmHelperCode = `
|
||||||
@ -59,11 +27,10 @@ export default async function loadWasm(file, importObject = {}) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
export default function assetPlugin(): Plugin {
|
export default function assetPlugin(): Plugin {
|
||||||
let sourcemap: boolean | 'inline' | 'hidden' = false
|
|
||||||
let publicDir = ''
|
let publicDir = ''
|
||||||
let outDir = ''
|
|
||||||
const publicAssetPathCache = new Map<string, string>()
|
const publicAssetPathCache = new Map<string, string>()
|
||||||
const assetCache = new Map<string, string>()
|
const assetCache = new Map<string, string>()
|
||||||
|
const isImportMetaPathSupported = supportImportMetaPaths()
|
||||||
return {
|
return {
|
||||||
name: 'vite:node-asset',
|
name: 'vite:node-asset',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
@ -73,9 +40,7 @@ export default function assetPlugin(): Plugin {
|
|||||||
assetCache.clear()
|
assetCache.clear()
|
||||||
},
|
},
|
||||||
configResolved(config): void {
|
configResolved(config): void {
|
||||||
sourcemap = config.build.sourcemap
|
publicDir = config.publicDir
|
||||||
publicDir = normalizePath(config.publicDir)
|
|
||||||
outDir = normalizePath(path.resolve(config.root, config.build.outDir))
|
|
||||||
},
|
},
|
||||||
resolveId(id): string | void {
|
resolveId(id): string | void {
|
||||||
if (id === wasmHelperId) {
|
if (id === wasmHelperId) {
|
||||||
@ -87,19 +52,12 @@ export default function assetPlugin(): Plugin {
|
|||||||
return wasmHelperCode
|
return wasmHelperCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.startsWith('\0')) {
|
if (id.startsWith('\0') || !assetImportRE.test(id)) {
|
||||||
// Rollup convention, this id should be handled by the
|
|
||||||
// plugin that marked it with \0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetResolved = resolveAsset(id)
|
|
||||||
if (!assetResolved) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let referenceId: string
|
let referenceId: string
|
||||||
const file = assetResolved.file
|
const file = cleanUrl(id)
|
||||||
if (publicDir && file.startsWith(publicDir)) {
|
if (publicDir && file.startsWith(publicDir)) {
|
||||||
const hash = getHash(file)
|
const hash = getHash(file)
|
||||||
if (!publicAssetPathCache.get(hash)) {
|
if (!publicAssetPathCache.get(hash)) {
|
||||||
@ -115,65 +73,62 @@ export default function assetPlugin(): Plugin {
|
|||||||
const hash = this.emitFile({
|
const hash = this.emitFile({
|
||||||
type: 'asset',
|
type: 'asset',
|
||||||
name: path.basename(file),
|
name: path.basename(file),
|
||||||
source
|
source: source as unknown as Uint8Array
|
||||||
})
|
})
|
||||||
referenceId = `__VITE_NODE_ASSET__${hash}__`
|
referenceId = `__VITE_NODE_ASSET__${hash}__`
|
||||||
assetCache.set(file, referenceId)
|
assetCache.set(file, referenceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetResolved.type === 'asset') {
|
if (assetRE.test(id)) {
|
||||||
if (assetResolved.query && typeof assetResolved.query.asarUnpack === 'string') {
|
const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname'
|
||||||
|
if (assetUnpackRE.test(id)) {
|
||||||
return `
|
return `
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
export default join(__dirname, ${referenceId}).replace('app.asar', 'app.asar.unpacked')`
|
export default join(${dirnameExpr}, ${referenceId}).replace('app.asar', 'app.asar.unpacked')`
|
||||||
} else {
|
} else {
|
||||||
return `
|
return `
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
export default join(__dirname, ${referenceId})`
|
export default join(${dirnameExpr}, ${referenceId})`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetResolved.type === 'native') {
|
if (id.endsWith('.node')) {
|
||||||
return `export default require(${referenceId})`
|
return `export default require(${referenceId})`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetResolved.type === 'wasm') {
|
if (id.endsWith('.wasm?loader')) {
|
||||||
return `
|
return `
|
||||||
import loadWasm from ${JSON.stringify(wasmHelperId)}
|
import loadWasm from ${JSON.stringify(wasmHelperId)}
|
||||||
export default importObject => loadWasm(${referenceId}, importObject)`
|
export default importObject => loadWasm(${referenceId}, importObject)`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null {
|
renderChunk(code, chunk, { sourcemap, dir }): { code: string; map: SourceMapInput } | null {
|
||||||
let match: RegExpExecArray | null
|
let match: RegExpExecArray | null
|
||||||
let s: MagicString | undefined
|
let s: MagicString | undefined
|
||||||
|
|
||||||
nodeAssetRE.lastIndex = 0
|
nodeAssetRE.lastIndex = 0
|
||||||
if (code.match(nodeAssetRE)) {
|
while ((match = nodeAssetRE.exec(code))) {
|
||||||
while ((match = nodeAssetRE.exec(code))) {
|
s ||= new MagicString(code)
|
||||||
s ||= new MagicString(code)
|
const [full, hash] = match
|
||||||
const [full, hash] = match
|
const filename = this.getFileName(hash)
|
||||||
const filename = this.getFileName(hash)
|
const outputFilepath = toRelativePath(filename, chunk.fileName)
|
||||||
const outputFilepath = toRelativePath(filename, chunk.fileName)
|
const replacement = JSON.stringify(outputFilepath)
|
||||||
const replacement = JSON.stringify(outputFilepath)
|
s.overwrite(match.index, match.index + full.length, replacement, {
|
||||||
s.overwrite(match.index, match.index + full.length, replacement, {
|
contentOnly: true
|
||||||
contentOnly: true
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nodePublicAssetRE.lastIndex = 0
|
nodePublicAssetRE.lastIndex = 0
|
||||||
if (code.match(nodePublicAssetRE)) {
|
while ((match = nodePublicAssetRE.exec(code))) {
|
||||||
while ((match = nodePublicAssetRE.exec(code))) {
|
s ||= new MagicString(code)
|
||||||
s ||= new MagicString(code)
|
const [full, hash] = match
|
||||||
const [full, hash] = match
|
const filename = publicAssetPathCache.get(hash)!
|
||||||
const filename = publicAssetPathCache.get(hash)!
|
const outputFilepath = toRelativePath(filename, normalizePath(path.join(dir!, chunk.fileName)))
|
||||||
const outputFilepath = toRelativePath(filename, normalizePath(path.join(outDir, chunk.fileName)))
|
const replacement = JSON.stringify(outputFilepath)
|
||||||
const replacement = JSON.stringify(outputFilepath)
|
s.overwrite(match.index, match.index + full.length, replacement, {
|
||||||
s.overwrite(match.index, match.index + full.length, replacement, {
|
contentOnly: true
|
||||||
contentOnly: true
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s) {
|
if (s) {
|
||||||
@ -181,9 +136,9 @@ export default function assetPlugin(): Plugin {
|
|||||||
code: s.toString(),
|
code: s.toString(),
|
||||||
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/plugins/buildReporter.ts
Normal file
30
src/plugins/buildReporter.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { type Plugin } from 'vite'
|
||||||
|
|
||||||
|
type BuildReporterApi = {
|
||||||
|
getWatchFiles: () => string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function buildReporterPlugin(): Plugin<BuildReporterApi> {
|
||||||
|
const moduleIds: string[] = []
|
||||||
|
return {
|
||||||
|
name: 'vite:build-reporter',
|
||||||
|
|
||||||
|
buildEnd() {
|
||||||
|
const allModuleIds = Array.from(this.getModuleIds())
|
||||||
|
const sourceFiles = allModuleIds.filter(id => {
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const info = this.getModuleInfo(id)
|
||||||
|
return info && !info.isExternal
|
||||||
|
})
|
||||||
|
moduleIds.push(...sourceFiles)
|
||||||
|
},
|
||||||
|
|
||||||
|
api: {
|
||||||
|
getWatchFiles() {
|
||||||
|
return moduleIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,11 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import fs from 'node:fs'
|
|
||||||
import { spawn } from 'node:child_process'
|
import { spawn } from 'node:child_process'
|
||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
import colors from 'picocolors'
|
import colors from 'picocolors'
|
||||||
import { type Plugin, type ResolvedConfig, normalizePath, createFilter } from 'vite'
|
import { type Plugin, type Logger, type LibraryOptions, normalizePath } from 'vite'
|
||||||
import * as babel from '@babel/core'
|
import * as babel from '@babel/core'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import type { SourceMapInput, OutputChunk } from 'rollup'
|
import type { SourceMapInput, OutputChunk, OutputOptions } from 'rollup'
|
||||||
import { getElectronPath } from '../electron'
|
import { getElectronPath } from '../electron'
|
||||||
import { toRelativePath } from '../utils'
|
import { toRelativePath } from '../utils'
|
||||||
|
|
||||||
@ -141,6 +140,8 @@ const bytecodeModuleLoaderCode = [
|
|||||||
`};`
|
`};`
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const bytecodeChunkExtensionRE = /.(jsc|cjsc)$/
|
||||||
|
|
||||||
export interface BytecodeOptions {
|
export interface BytecodeOptions {
|
||||||
chunkAlias?: string | string[]
|
chunkAlias?: string | string[]
|
||||||
transformArrowFunctions?: boolean
|
transformArrowFunctions?: boolean
|
||||||
@ -149,7 +150,9 @@ export interface BytecodeOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile to v8 bytecode to protect source code.
|
* Compile source code to v8 bytecode.
|
||||||
|
*
|
||||||
|
* @deprecated use `build.bytecode` config option instead
|
||||||
*/
|
*/
|
||||||
export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
||||||
if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') {
|
if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') {
|
||||||
@ -159,174 +162,138 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
|||||||
const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options
|
const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options
|
||||||
const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
|
const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
|
||||||
|
|
||||||
const filter = createFilter(/\.(m?[jt]s|[jt]sx)$/)
|
|
||||||
|
|
||||||
const escapeRegExpString = (str: string): string => {
|
|
||||||
return str
|
|
||||||
.replace(/\\/g, '\\\\\\\\')
|
|
||||||
.replace(/[|{}()[\]^$+*?.]/g, '\\$&')
|
|
||||||
.replace(/-/g, '\\u002d')
|
|
||||||
}
|
|
||||||
|
|
||||||
const transformAllChunks = _chunkAlias.length === 0
|
const transformAllChunks = _chunkAlias.length === 0
|
||||||
const isBytecodeChunk = (chunkName: string): boolean => {
|
const isBytecodeChunk = (chunkName: string): boolean => {
|
||||||
return transformAllChunks || _chunkAlias.some(alias => alias === chunkName)
|
return transformAllChunks || _chunkAlias.some(alias => alias === chunkName)
|
||||||
}
|
}
|
||||||
|
|
||||||
const _transform = (code: string): string => {
|
const plugins: babel.PluginItem[] = []
|
||||||
const re = babel.transform(code, {
|
|
||||||
plugins: ['@babel/plugin-transform-arrow-functions']
|
if (transformArrowFunctions) {
|
||||||
})
|
plugins.push('@babel/plugin-transform-arrow-functions')
|
||||||
return re.code || ''
|
}
|
||||||
|
|
||||||
|
if (protectedStrings.length > 0) {
|
||||||
|
plugins.push([protectStringsPlugin, { protectedStrings: new Set(protectedStrings) }])
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldTransformBytecodeChunk = plugins.length !== 0
|
||||||
|
|
||||||
|
const _transform = (code: string, sourceMaps: boolean = false): { code: string; map?: SourceMapInput } | null => {
|
||||||
|
const re = babel.transform(code, { plugins, sourceMaps })
|
||||||
|
return re ? { code: re.code || '', map: re.map } : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStrict = '"use strict";'
|
const useStrict = '"use strict";'
|
||||||
const bytecodeModuleLoader = 'bytecode-loader.cjs'
|
const bytecodeModuleLoader = 'bytecode-loader.cjs'
|
||||||
|
|
||||||
let config: ResolvedConfig
|
let logger: Logger
|
||||||
let useInRenderer = false
|
let supported = false
|
||||||
let bytecodeRequired = false
|
|
||||||
let bytecodeFiles: { name: string; size: number }[] = []
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'vite:bytecode',
|
name: 'vite:bytecode',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
configResolved(resolvedConfig): void {
|
configResolved(config): void {
|
||||||
config = resolvedConfig
|
if (supported) {
|
||||||
useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config')
|
return
|
||||||
|
}
|
||||||
|
logger = config.logger
|
||||||
|
const useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config')
|
||||||
if (useInRenderer) {
|
if (useInRenderer) {
|
||||||
config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.'))
|
config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.'))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (resolvedConfig.build.minify && protectedStrings.length > 0) {
|
const build = config.build
|
||||||
config.logger.warn(colors.yellow('Strings cannot be protected when minification is enabled.'))
|
const resolvedOutputs = resolveBuildOutputs(build.rollupOptions.output, build.lib)
|
||||||
}
|
if (resolvedOutputs) {
|
||||||
},
|
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
|
||||||
transform(code, id): void | { code: string; map: SourceMapInput } {
|
const output = outputs[0]
|
||||||
if (config.build.minify || protectedStrings.length === 0 || !filter(id)) return
|
if (output.format === 'es') {
|
||||||
|
config.logger.warn(
|
||||||
let match: RegExpExecArray | null
|
colors.yellow(
|
||||||
let s: MagicString | undefined
|
'bytecodePlugin does not support ES module, please remove "type": "module" ' +
|
||||||
|
'in package.json or set the "build.rollupOptions.output.format" option to "cjs".'
|
||||||
protectedStrings.forEach(str => {
|
)
|
||||||
const escapedStr = escapeRegExpString(str)
|
|
||||||
const re = new RegExp(`\\u0027${escapedStr}\\u0027|\\u0022${escapedStr}\\u0022`, 'g')
|
|
||||||
const charCodes = Array.from(str).map(s => s.charCodeAt(0))
|
|
||||||
const replacement = `String.fromCharCode(${charCodes.toString()})`
|
|
||||||
while ((match = re.exec(code))) {
|
|
||||||
s ||= new MagicString(code)
|
|
||||||
const [full] = match
|
|
||||||
s.overwrite(match.index, match.index + full.length, replacement, {
|
|
||||||
contentOnly: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (s) {
|
|
||||||
return {
|
|
||||||
code: s.toString(),
|
|
||||||
map: config.build.sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderChunk(code, chunk, options): { code: string } | null {
|
|
||||||
if (options.format === 'es') {
|
|
||||||
config.logger.warn(
|
|
||||||
colors.yellow(
|
|
||||||
'bytecodePlugin does not support ES module, please remove "type": "module" ' +
|
|
||||||
'in package.json or set the "build.rollupOptions.output.format" option to "cjs".'
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (useInRenderer) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (chunk.type === 'chunk' && isBytecodeChunk(chunk.name)) {
|
|
||||||
bytecodeRequired = true
|
|
||||||
if (transformArrowFunctions) {
|
|
||||||
return {
|
|
||||||
code: _transform(code)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
supported = output.format === 'cjs' && !useInRenderer
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderChunk(code, chunk, { sourcemap }): { code: string; map?: SourceMapInput } | null {
|
||||||
|
if (supported && isBytecodeChunk(chunk.name) && shouldTransformBytecodeChunk) {
|
||||||
|
return _transform(code, !!sourcemap)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
generateBundle(options): void {
|
async generateBundle(_, output): Promise<void> {
|
||||||
if (options.format !== 'es' && !useInRenderer && bytecodeRequired) {
|
if (!supported) {
|
||||||
this.emitFile({
|
return
|
||||||
type: 'asset',
|
|
||||||
source: bytecodeModuleLoaderCode.join('\n') + '\n',
|
|
||||||
name: 'Bytecode Loader File',
|
|
||||||
fileName: bytecodeModuleLoader
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
const _chunks = Object.values(output)
|
||||||
async writeBundle(options, output): Promise<void> {
|
const chunks = _chunks.filter(chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name)) as OutputChunk[]
|
||||||
if (options.format === 'es' || useInRenderer || !bytecodeRequired) {
|
|
||||||
|
if (chunks.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const outDir = options.dir!
|
|
||||||
|
|
||||||
bytecodeFiles = []
|
|
||||||
|
|
||||||
const bundles = Object.keys(output)
|
|
||||||
const chunks = Object.values(output).filter(
|
|
||||||
chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name) && chunk.fileName !== bytecodeModuleLoader
|
|
||||||
) as OutputChunk[]
|
|
||||||
const bytecodeChunks = chunks.map(chunk => chunk.fileName)
|
const bytecodeChunks = chunks.map(chunk => chunk.fileName)
|
||||||
const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName))
|
const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName))
|
||||||
|
|
||||||
const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|')
|
const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|')
|
||||||
const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null
|
const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null
|
||||||
|
|
||||||
const keepBundle = (chunkFileName: string): void => {
|
|
||||||
const newFileName = path.resolve(path.dirname(chunkFileName), `_${path.basename(chunkFileName)}`)
|
|
||||||
fs.renameSync(chunkFileName, newFileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBytecodeLoaderBlock = (chunkFileName: string): string => {
|
const getBytecodeLoaderBlock = (chunkFileName: string): string => {
|
||||||
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
|
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let bytecodeChunkCount = 0
|
||||||
|
|
||||||
|
const bundles = Object.keys(output)
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
bundles.map(async name => {
|
bundles.map(async name => {
|
||||||
const chunk = output[name]
|
const chunk = output[name]
|
||||||
if (chunk.type === 'chunk') {
|
if (chunk.type === 'chunk') {
|
||||||
let _code = chunk.code
|
let _code = chunk.code
|
||||||
if (bytecodeRE && _code.match(bytecodeRE)) {
|
if (bytecodeRE) {
|
||||||
let match: RegExpExecArray | null
|
let match: RegExpExecArray | null
|
||||||
const s = new MagicString(_code)
|
let s: MagicString | undefined
|
||||||
while ((match = bytecodeRE.exec(_code))) {
|
while ((match = bytecodeRE.exec(_code))) {
|
||||||
|
s ||= new MagicString(_code)
|
||||||
const [prefix, chunkName] = match
|
const [prefix, chunkName] = match
|
||||||
const len = prefix.length + chunkName.length
|
const len = prefix.length + chunkName.length
|
||||||
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
|
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
|
||||||
contentOnly: true
|
contentOnly: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_code = s.toString()
|
if (s) {
|
||||||
|
_code = s.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const chunkFileName = path.resolve(outDir, name)
|
|
||||||
if (bytecodeChunks.includes(name)) {
|
if (bytecodeChunks.includes(name)) {
|
||||||
const bytecodeBuffer = await compileToBytecode(_code)
|
const bytecodeBuffer = await compileToBytecode(_code)
|
||||||
fs.writeFileSync(path.resolve(outDir, name + 'c'), bytecodeBuffer)
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: name + 'c',
|
||||||
|
source: bytecodeBuffer
|
||||||
|
})
|
||||||
|
if (!removeBundleJS) {
|
||||||
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: '_' + chunk.fileName,
|
||||||
|
source: chunk.code
|
||||||
|
})
|
||||||
|
}
|
||||||
if (chunk.isEntry) {
|
if (chunk.isEntry) {
|
||||||
if (!removeBundleJS) {
|
|
||||||
keepBundle(chunkFileName)
|
|
||||||
}
|
|
||||||
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
|
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
|
||||||
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
|
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
|
||||||
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
|
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
|
||||||
fs.writeFileSync(chunkFileName, code)
|
chunk.code = code
|
||||||
} else {
|
} else {
|
||||||
if (removeBundleJS) {
|
delete output[chunk.fileName]
|
||||||
fs.unlinkSync(chunkFileName)
|
|
||||||
} else {
|
|
||||||
keepBundle(chunkFileName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
bytecodeFiles.push({ name: name + 'c', size: bytecodeBuffer.length })
|
bytecodeChunkCount += 1
|
||||||
} else {
|
} else {
|
||||||
if (chunk.isEntry) {
|
if (chunk.isEntry) {
|
||||||
let hasBytecodeMoudle = false
|
let hasBytecodeMoudle = false
|
||||||
@ -343,34 +310,118 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
|||||||
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
|
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
|
_code = hasBytecodeMoudle
|
||||||
_code = hasBytecodeMoudle ? _code.replace(useStrict, `${useStrict}\n${bytecodeLoaderBlock}`) : _code
|
? _code.replace(
|
||||||
|
/("use strict";)|('use strict';)/,
|
||||||
|
`${useStrict}\n${getBytecodeLoaderBlock(chunk.fileName)}`
|
||||||
|
)
|
||||||
|
: _code
|
||||||
}
|
}
|
||||||
fs.writeFileSync(chunkFileName, _code)
|
chunk.code = _code
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (bytecodeChunkCount && !_chunks.some(ass => ass.type === 'asset' && ass.fileName === bytecodeModuleLoader)) {
|
||||||
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
source: bytecodeModuleLoaderCode.join('\n') + '\n',
|
||||||
|
name: 'Bytecode Loader File',
|
||||||
|
fileName: bytecodeModuleLoader
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
closeBundle(): void {
|
writeBundle(_, output): void {
|
||||||
if (!useInRenderer) {
|
if (supported) {
|
||||||
const chunkLimit = config.build.chunkSizeWarningLimit
|
const bytecodeChunkCount = Object.keys(output).filter(chunk => bytecodeChunkExtensionRE.test(chunk)).length
|
||||||
const outDir = normalizePath(path.relative(config.root, path.resolve(config.root, config.build.outDir))) + '/'
|
logger.info(`${colors.green(`✓`)} ${bytecodeChunkCount} chunks compiled into bytecode.`)
|
||||||
config.logger.info(`${colors.green(`✓`)} ${bytecodeFiles.length} bundles compiled into bytecode.`)
|
}
|
||||||
let longest = 0
|
}
|
||||||
bytecodeFiles.forEach(file => {
|
}
|
||||||
const len = file.name.length
|
}
|
||||||
if (len > longest) longest = len
|
|
||||||
})
|
function resolveBuildOutputs(
|
||||||
bytecodeFiles.forEach(file => {
|
outputs: OutputOptions | OutputOptions[] | undefined,
|
||||||
const kbs = file.size / 1000
|
libOptions: LibraryOptions | false
|
||||||
config.logger.info(
|
): OutputOptions | OutputOptions[] | undefined {
|
||||||
`${colors.gray(colors.white(colors.dim(outDir)))}${colors.green(file.name.padEnd(longest + 2))} ${
|
if (libOptions && !Array.isArray(outputs)) {
|
||||||
kbs > chunkLimit ? colors.yellow(`${kbs.toFixed(2)} kB`) : colors.dim(`${kbs.toFixed(2)} kB`)
|
const libFormats = libOptions.formats || []
|
||||||
}`
|
return libFormats.map(format => ({ ...outputs, format }))
|
||||||
)
|
}
|
||||||
})
|
return outputs
|
||||||
bytecodeFiles = []
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,361 +50,363 @@ function resolveBuildOutputs(
|
|||||||
return outputs
|
return outputs
|
||||||
}
|
}
|
||||||
|
|
||||||
export function electronMainVitePlugin(options?: ElectronPluginOptions): Plugin[] {
|
export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
|
||||||
return [
|
return {
|
||||||
{
|
name: 'vite:electron-main-config-preset',
|
||||||
name: 'vite:electron-main-preset-config',
|
apply: 'build',
|
||||||
apply: 'build',
|
enforce: 'pre',
|
||||||
enforce: 'pre',
|
config(config): void {
|
||||||
config(config): void {
|
const root = options?.root || process.cwd()
|
||||||
const root = options?.root || process.cwd()
|
|
||||||
|
|
||||||
const nodeTarget = getElectronNodeTarget()
|
const nodeTarget = getElectronNodeTarget()
|
||||||
|
|
||||||
const pkg = loadPackageData() || { type: 'commonjs' }
|
const pkg = loadPackageData() || { type: 'commonjs' }
|
||||||
|
|
||||||
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
|
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
resolve: {
|
resolve: {
|
||||||
browserField: false,
|
browserField: false,
|
||||||
mainFields: ['module', 'jsnext:main', 'jsnext'],
|
mainFields: ['module', 'jsnext:main', 'jsnext'],
|
||||||
conditions: ['node']
|
conditions: ['node']
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(root, 'out', 'main'),
|
||||||
|
target: nodeTarget,
|
||||||
|
assetsDir: 'chunks',
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
||||||
|
output: {}
|
||||||
},
|
},
|
||||||
build: {
|
reportCompressedSize: false,
|
||||||
outDir: path.resolve(root, 'out', 'main'),
|
minify: false
|
||||||
target: nodeTarget,
|
|
||||||
assetsDir: 'chunks',
|
|
||||||
rollupOptions: {
|
|
||||||
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
|
||||||
output: {}
|
|
||||||
},
|
|
||||||
reportCompressedSize: false,
|
|
||||||
minify: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const build = config.build || {}
|
|
||||||
const rollupOptions = build.rollupOptions || {}
|
|
||||||
if (!rollupOptions.input) {
|
|
||||||
const libOptions = build.lib
|
|
||||||
const outputOptions = rollupOptions.output
|
|
||||||
defaultConfig.build['lib'] = {
|
|
||||||
entry: findLibEntry(root, 'main'),
|
|
||||||
formats:
|
|
||||||
libOptions && libOptions.formats && libOptions.formats.length > 0
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
outputOptions && !Array.isArray(outputOptions) && outputOptions.format
|
|
||||||
? outputOptions.format
|
|
||||||
: format
|
|
||||||
]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
defaultConfig.build.rollupOptions.output['format'] = format
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
|
|
||||||
build.assetsDir || defaultConfig.build.assetsDir,
|
|
||||||
'[name]-[hash].[ext]'
|
|
||||||
)
|
|
||||||
|
|
||||||
const buildConfig = mergeConfig(defaultConfig.build, build)
|
|
||||||
config.build = buildConfig
|
|
||||||
|
|
||||||
config.resolve = mergeConfig(defaultConfig.resolve, config.resolve || {})
|
|
||||||
|
|
||||||
config.define = config.define || {}
|
|
||||||
config.define = { ...processEnvDefine(), ...config.define }
|
|
||||||
|
|
||||||
config.envPrefix = config.envPrefix || ['MAIN_VITE_', 'VITE_']
|
|
||||||
|
|
||||||
config.publicDir = config.publicDir || 'resources'
|
|
||||||
// do not copy public dir
|
|
||||||
config.build.copyPublicDir = false
|
|
||||||
// module preload polyfill does not apply to nodejs (main process)
|
|
||||||
config.build.modulePreload = false
|
|
||||||
// enable ssr build
|
|
||||||
config.build.ssr = true
|
|
||||||
config.build.ssrEmitAssets = true
|
|
||||||
config.ssr = { ...config.ssr, ...{ noExternal: true } }
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
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 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 build = config.build || {}
|
||||||
|
const rollupOptions = build.rollupOptions || {}
|
||||||
|
if (!rollupOptions.input) {
|
||||||
const libOptions = build.lib
|
const libOptions = build.lib
|
||||||
const rollupOptions = build.rollupOptions
|
const outputOptions = rollupOptions.output
|
||||||
|
defaultConfig.build['lib'] = {
|
||||||
if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
|
entry: findLibEntry(root, 'main'),
|
||||||
throw new Error(
|
formats:
|
||||||
'An entry point is required in the electron vite main config, ' +
|
libOptions && libOptions.formats && libOptions.formats.length > 0
|
||||||
'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
|
? []
|
||||||
)
|
: [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedOutputs = resolveBuildOutputs(rollupOptions.output, libOptions)
|
|
||||||
|
|
||||||
if (resolvedOutputs) {
|
|
||||||
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
|
|
||||||
if (outputs.length > 1) {
|
|
||||||
throw new Error('The electron vite main config does not support multiple outputs.')
|
|
||||||
} else {
|
|
||||||
const outpout = outputs[0]
|
|
||||||
if (['es', 'cjs'].includes(outpout.format || '')) {
|
|
||||||
if (outpout.format === 'es' && !supportESM()) {
|
|
||||||
throw new Error(
|
|
||||||
'The electron vite main config output format does not support "es", ' +
|
|
||||||
'you can upgrade electron to the latest version or switch to "cjs" format.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`The electron vite main config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
defaultConfig.build.rollupOptions.output['format'] = format
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
|
||||||
|
build.assetsDir || defaultConfig.build.assetsDir,
|
||||||
|
'[name]-[hash].[ext]'
|
||||||
|
)
|
||||||
|
|
||||||
|
const buildConfig = mergeConfig(defaultConfig.build, build)
|
||||||
|
config.build = buildConfig
|
||||||
|
|
||||||
|
config.resolve = mergeConfig(defaultConfig.resolve, config.resolve || {})
|
||||||
|
|
||||||
|
config.define = config.define || {}
|
||||||
|
config.define = { ...processEnvDefine(), ...config.define }
|
||||||
|
|
||||||
|
config.envPrefix = config.envPrefix || ['MAIN_VITE_', 'VITE_']
|
||||||
|
|
||||||
|
config.publicDir = config.publicDir || 'resources'
|
||||||
|
// do not copy public dir
|
||||||
|
config.build.copyPublicDir = false
|
||||||
|
// module preload polyfill does not apply to nodejs (main process)
|
||||||
|
config.build.modulePreload = false
|
||||||
|
// enable ssr build
|
||||||
|
config.build.ssr = true
|
||||||
|
config.build.ssrEmitAssets = true
|
||||||
|
config.ssr = { ...config.ssr, ...{ noExternal: true } }
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function electronPreloadVitePlugin(options?: ElectronPluginOptions): Plugin[] {
|
export function electronMainConfigValidatorPlugin(): Plugin {
|
||||||
return [
|
return {
|
||||||
{
|
name: 'vite:electron-main-config-validator',
|
||||||
name: 'vite:electron-preload-preset-config',
|
apply: 'build',
|
||||||
apply: 'build',
|
enforce: 'post',
|
||||||
enforce: 'pre',
|
configResolved(config): void {
|
||||||
config(config): void {
|
const build = config.build
|
||||||
const root = options?.root || process.cwd()
|
if (!build.target) {
|
||||||
|
throw new Error('build.target option is required in the electron vite main config.')
|
||||||
const nodeTarget = getElectronNodeTarget()
|
} else {
|
||||||
|
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
||||||
const pkg = loadPackageData() || { type: 'commonjs' }
|
if (targets.some(t => !t.startsWith('node'))) {
|
||||||
|
throw new Error('The electron vite main config build.target option must be "node?".')
|
||||||
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
|
|
||||||
|
|
||||||
const defaultConfig = {
|
|
||||||
build: {
|
|
||||||
outDir: path.resolve(root, 'out', 'preload'),
|
|
||||||
target: nodeTarget,
|
|
||||||
assetsDir: 'chunks',
|
|
||||||
rollupOptions: {
|
|
||||||
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
|
||||||
output: {}
|
|
||||||
},
|
|
||||||
reportCompressedSize: false,
|
|
||||||
minify: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const build = config.build || {}
|
|
||||||
const rollupOptions = build.rollupOptions || {}
|
|
||||||
if (!rollupOptions.input) {
|
|
||||||
const libOptions = build.lib
|
|
||||||
const outputOptions = rollupOptions.output
|
|
||||||
defaultConfig.build['lib'] = {
|
|
||||||
entry: findLibEntry(root, 'preload'),
|
|
||||||
formats:
|
|
||||||
libOptions && libOptions.formats && libOptions.formats.length > 0
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
outputOptions && !Array.isArray(outputOptions) && outputOptions.format
|
|
||||||
? outputOptions.format
|
|
||||||
: format
|
|
||||||
]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
defaultConfig.build.rollupOptions.output['format'] = format
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
|
|
||||||
build.assetsDir || defaultConfig.build.assetsDir,
|
|
||||||
'[name]-[hash].[ext]'
|
|
||||||
)
|
|
||||||
|
|
||||||
const buildConfig = mergeConfig(defaultConfig.build, build)
|
|
||||||
config.build = buildConfig
|
|
||||||
|
|
||||||
const resolvedOutputs = resolveBuildOutputs(config.build.rollupOptions!.output, config.build.lib || false)
|
|
||||||
|
|
||||||
if (resolvedOutputs) {
|
|
||||||
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
|
|
||||||
|
|
||||||
if (outputs.find(({ format }) => format === 'es')) {
|
|
||||||
if (Array.isArray(config.build.rollupOptions!.output)) {
|
|
||||||
config.build.rollupOptions!.output.forEach(output => {
|
|
||||||
if (output.format === 'es') {
|
|
||||||
output['entryFileNames'] = '[name].mjs'
|
|
||||||
output['chunkFileNames'] = '[name]-[hash].mjs'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
config.build.rollupOptions!.output!['entryFileNames'] = '[name].mjs'
|
|
||||||
config.build.rollupOptions!.output!['chunkFileNames'] = '[name]-[hash].mjs'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.define = config.define || {}
|
|
||||||
config.define = { ...processEnvDefine(), ...config.define }
|
|
||||||
|
|
||||||
config.envPrefix = config.envPrefix || ['PRELOAD_VITE_', 'VITE_']
|
|
||||||
|
|
||||||
config.publicDir = config.publicDir || 'resources'
|
|
||||||
// do not copy public dir
|
|
||||||
config.build.copyPublicDir = false
|
|
||||||
// module preload polyfill does not apply to nodejs (preload scripts)
|
|
||||||
config.build.modulePreload = false
|
|
||||||
// enable ssr build
|
|
||||||
config.build.ssr = true
|
|
||||||
config.build.ssrEmitAssets = true
|
|
||||||
config.ssr = { ...config.ssr, ...{ noExternal: true } }
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
const libOptions = build.lib
|
||||||
name: 'vite:electron-preload-resolved-config',
|
const rollupOptions = build.rollupOptions
|
||||||
apply: 'build',
|
|
||||||
enforce: 'post',
|
if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
|
||||||
configResolved(config): void {
|
throw new Error(
|
||||||
const build = config.build
|
'An entry point is required in the electron vite main config, ' +
|
||||||
if (!build.target) {
|
'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
|
||||||
throw new Error('build.target option is required in the electron vite preload config.')
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedOutputs = resolveBuildOutputs(rollupOptions.output, libOptions)
|
||||||
|
|
||||||
|
if (resolvedOutputs) {
|
||||||
|
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
|
||||||
|
if (outputs.length > 1) {
|
||||||
|
throw new Error('The electron vite main config does not support multiple outputs.')
|
||||||
} else {
|
} else {
|
||||||
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
const outpout = outputs[0]
|
||||||
if (targets.some(t => !t.startsWith('node'))) {
|
if (['es', 'cjs'].includes(outpout.format || '')) {
|
||||||
throw new Error('The electron vite preload config build.target must be "node?".')
|
if (outpout.format === 'es' && !supportESM()) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const libOptions = build.lib
|
|
||||||
const rollupOptions = build.rollupOptions
|
|
||||||
|
|
||||||
if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
|
|
||||||
throw new Error(
|
|
||||||
'An entry point is required in the electron vite preload config, ' +
|
|
||||||
'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedOutputs = resolveBuildOutputs(rollupOptions.output, libOptions)
|
|
||||||
|
|
||||||
if (resolvedOutputs) {
|
|
||||||
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
|
|
||||||
if (outputs.length > 1) {
|
|
||||||
throw new Error('The electron vite preload config does not support multiple outputs.')
|
|
||||||
} else {
|
|
||||||
const outpout = outputs[0]
|
|
||||||
if (['es', 'cjs'].includes(outpout.format || '')) {
|
|
||||||
if (outpout.format === 'es' && !supportESM()) {
|
|
||||||
throw new Error(
|
|
||||||
'The electron vite preload config output format does not support "es", ' +
|
|
||||||
'you can upgrade electron to the latest version or switch to "cjs" format.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The electron vite preload config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
|
'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 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' || process.env.NODE_ENV_ELECTRON_VITE === 'production' ? './' : config.base
|
|
||||||
config.root = config.root || './src/renderer'
|
|
||||||
|
|
||||||
const chromeTarget = getElectronChromeTarget()
|
|
||||||
|
|
||||||
const emptyOutDir = (): boolean => {
|
|
||||||
let outDir = config.build?.outDir
|
|
||||||
if (outDir) {
|
|
||||||
if (!path.isAbsolute(outDir)) {
|
|
||||||
outDir = path.resolve(root, outDir)
|
|
||||||
}
|
|
||||||
const resolvedRoot = normalizePath(path.resolve(root))
|
|
||||||
return normalizePath(outDir).startsWith(resolvedRoot + '/')
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultConfig = {
|
|
||||||
build: {
|
|
||||||
outDir: path.resolve(root, 'out', 'renderer'),
|
|
||||||
target: chromeTarget,
|
|
||||||
modulePreload: { polyfill: false },
|
|
||||||
rollupOptions: {
|
|
||||||
input: findInput(root)
|
|
||||||
},
|
|
||||||
reportCompressedSize: false,
|
|
||||||
minify: false,
|
|
||||||
emptyOutDir: emptyOutDir()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.build?.outDir) {
|
|
||||||
config.build.outDir = path.resolve(root, config.build.outDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
|
|
||||||
config.build = buildConfig
|
|
||||||
|
|
||||||
config.envDir = config.envDir || path.resolve(root)
|
|
||||||
|
|
||||||
config.envPrefix = config.envPrefix || ['RENDERER_VITE_', 'VITE_']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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" option for the electron vite renderer config.'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const build = config.build
|
|
||||||
if (!build.target) {
|
|
||||||
throw new Error('build.target option is required in the electron vite renderer config.')
|
|
||||||
} else {
|
|
||||||
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
|
||||||
if (targets.some(t => !t.startsWith('chrome') && !/^es((202\d{1})|next)$/.test(t))) {
|
|
||||||
config.logger.warn(
|
|
||||||
'The electron vite renderer config build.target is not "chrome?" or "es?". This could be a mistake.'
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const rollupOptions = build.rollupOptions
|
export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
|
||||||
if (!rollupOptions.input) {
|
return {
|
||||||
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory.`))
|
name: 'vite:electron-preload-config-preset',
|
||||||
throw new Error('build.rollupOptions.input option is required in the electron vite renderer config.')
|
apply: 'build',
|
||||||
|
enforce: 'pre',
|
||||||
|
config(config): void {
|
||||||
|
const root = options?.root || process.cwd()
|
||||||
|
|
||||||
|
const nodeTarget = getElectronNodeTarget()
|
||||||
|
|
||||||
|
const pkg = loadPackageData() || { type: 'commonjs' }
|
||||||
|
|
||||||
|
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
ssr: {
|
||||||
|
resolve: {
|
||||||
|
conditions: ['module', 'browser', 'development|production'],
|
||||||
|
mainFields: ['browser', 'module', 'jsnext:main', 'jsnext']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(root, 'out', 'preload'),
|
||||||
|
target: nodeTarget,
|
||||||
|
assetsDir: 'chunks',
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
|
||||||
|
output: {}
|
||||||
|
},
|
||||||
|
reportCompressedSize: false,
|
||||||
|
minify: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const build = config.build || {}
|
||||||
|
const rollupOptions = build.rollupOptions || {}
|
||||||
|
if (!rollupOptions.input) {
|
||||||
|
const libOptions = build.lib
|
||||||
|
const outputOptions = rollupOptions.output
|
||||||
|
defaultConfig.build['lib'] = {
|
||||||
|
entry: findLibEntry(root, 'preload'),
|
||||||
|
formats:
|
||||||
|
libOptions && libOptions.formats && libOptions.formats.length > 0
|
||||||
|
? []
|
||||||
|
: [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defaultConfig.build.rollupOptions.output['format'] = format
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
|
||||||
|
build.assetsDir || defaultConfig.build.assetsDir,
|
||||||
|
'[name]-[hash].[ext]'
|
||||||
|
)
|
||||||
|
|
||||||
|
const buildConfig = mergeConfig(defaultConfig.build, build)
|
||||||
|
config.build = buildConfig
|
||||||
|
|
||||||
|
const resolvedOutputs = resolveBuildOutputs(config.build.rollupOptions!.output, config.build.lib || false)
|
||||||
|
|
||||||
|
if (resolvedOutputs) {
|
||||||
|
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
|
||||||
|
|
||||||
|
if (outputs.find(({ format }) => format === 'es')) {
|
||||||
|
if (Array.isArray(config.build.rollupOptions!.output)) {
|
||||||
|
config.build.rollupOptions!.output.forEach(output => {
|
||||||
|
if (output.format === 'es') {
|
||||||
|
output['entryFileNames'] = '[name].mjs'
|
||||||
|
output['chunkFileNames'] = '[name]-[hash].mjs'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
config.build.rollupOptions!.output!['entryFileNames'] = '[name].mjs'
|
||||||
|
config.build.rollupOptions!.output!['chunkFileNames'] = '[name]-[hash].mjs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.define = config.define || {}
|
||||||
|
config.define = { ...processEnvDefine(), ...config.define }
|
||||||
|
|
||||||
|
config.envPrefix = config.envPrefix || ['PRELOAD_VITE_', 'VITE_']
|
||||||
|
|
||||||
|
config.publicDir = config.publicDir || 'resources'
|
||||||
|
// do not copy public dir
|
||||||
|
config.build.copyPublicDir = false
|
||||||
|
// module preload polyfill does not apply to nodejs (preload scripts)
|
||||||
|
config.build.modulePreload = false
|
||||||
|
// enable ssr build
|
||||||
|
config.build.ssr = true
|
||||||
|
config.build.ssrEmitAssets = true
|
||||||
|
config.ssr = mergeConfig(defaultConfig.ssr, config.ssr || {})
|
||||||
|
config.ssr.noExternal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function electronPreloadConfigValidatorPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite:electron-preload-config-validator',
|
||||||
|
apply: 'build',
|
||||||
|
enforce: 'post',
|
||||||
|
configResolved(config): void {
|
||||||
|
const build = config.build
|
||||||
|
if (!build.target) {
|
||||||
|
throw new Error('build.target option is required in the electron vite preload config.')
|
||||||
|
} else {
|
||||||
|
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
||||||
|
if (targets.some(t => !t.startsWith('node'))) {
|
||||||
|
throw new Error('The electron vite preload config build.target must be "node?".')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const libOptions = build.lib
|
||||||
|
const rollupOptions = build.rollupOptions
|
||||||
|
|
||||||
|
if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
|
||||||
|
throw new Error(
|
||||||
|
'An entry point is required in the electron vite preload config, ' +
|
||||||
|
'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedOutputs = resolveBuildOutputs(rollupOptions.output, libOptions)
|
||||||
|
|
||||||
|
if (resolvedOutputs) {
|
||||||
|
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
|
||||||
|
if (outputs.length > 1) {
|
||||||
|
throw new Error('The electron vite preload config does not support multiple outputs.')
|
||||||
|
} else {
|
||||||
|
const outpout = outputs[0]
|
||||||
|
if (['es', 'cjs'].includes(outpout.format || '')) {
|
||||||
|
if (outpout.format === 'es' && !supportESM()) {
|
||||||
|
throw new Error(
|
||||||
|
'The electron vite preload config output format does not support "es", ' +
|
||||||
|
'you can upgrade electron to the latest version or switch to "cjs" format.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`The electron vite preload config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function electronRendererConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite:electron-renderer-config-preset',
|
||||||
|
enforce: 'pre',
|
||||||
|
config(config): void {
|
||||||
|
const root = options?.root || process.cwd()
|
||||||
|
|
||||||
|
config.base =
|
||||||
|
config.mode === 'production' || process.env.NODE_ENV_ELECTRON_VITE === 'production' ? './' : config.base
|
||||||
|
config.root = config.root || './src/renderer'
|
||||||
|
|
||||||
|
const chromeTarget = getElectronChromeTarget()
|
||||||
|
|
||||||
|
const emptyOutDir = (): boolean => {
|
||||||
|
let outDir = config.build?.outDir
|
||||||
|
if (outDir) {
|
||||||
|
if (!path.isAbsolute(outDir)) {
|
||||||
|
outDir = path.resolve(root, outDir)
|
||||||
|
}
|
||||||
|
const resolvedRoot = normalizePath(path.resolve(root))
|
||||||
|
return normalizePath(outDir).startsWith(resolvedRoot + '/')
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(root, 'out', 'renderer'),
|
||||||
|
target: chromeTarget,
|
||||||
|
modulePreload: { polyfill: false },
|
||||||
|
rollupOptions: {
|
||||||
|
input: findInput(root)
|
||||||
|
},
|
||||||
|
reportCompressedSize: false,
|
||||||
|
minify: false,
|
||||||
|
emptyOutDir: emptyOutDir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.build?.outDir) {
|
||||||
|
config.build.outDir = path.resolve(root, config.build.outDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
|
||||||
|
config.build = buildConfig
|
||||||
|
|
||||||
|
config.envDir = config.envDir || path.resolve(root)
|
||||||
|
|
||||||
|
config.envPrefix = config.envPrefix || ['RENDERER_VITE_', 'VITE_']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function electronRendererConfigValidatorPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite:electron-renderer-config-validator',
|
||||||
|
enforce: 'post',
|
||||||
|
configResolved(config): void {
|
||||||
|
if (config.base !== './' && config.base !== '/') {
|
||||||
|
config.logger.warn(colors.yellow('(!) Should not set "base" option for the electron vite renderer config.'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const build = config.build
|
||||||
|
if (!build.target) {
|
||||||
|
throw new Error('build.target option is required in the electron vite renderer config.')
|
||||||
|
} else {
|
||||||
|
const targets = Array.isArray(build.target) ? build.target : [build.target]
|
||||||
|
if (targets.some(t => !t.startsWith('chrome') && !/^es((202\d{1})|next)$/.test(t))) {
|
||||||
|
config.logger.warn(
|
||||||
|
'The electron vite renderer config build.target is not "chrome?" or "es?". This could be a mistake.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rollupOptions = build.rollupOptions
|
||||||
|
if (!rollupOptions.input) {
|
||||||
|
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory.`))
|
||||||
|
throw new Error('build.rollupOptions.input option is required in the electron vite renderer config.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import MagicString from 'magic-string'
|
|||||||
import type { SourceMapInput } from 'rollup'
|
import type { SourceMapInput } from 'rollup'
|
||||||
import type { Plugin } from 'vite'
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
import { getElectronMajorVersion } from '../electron'
|
import { supportImportMetaPaths } from '../electron'
|
||||||
|
|
||||||
const CJSyntaxRe = /__filename|__dirname|require\(|require\.resolve\(/
|
const CJSyntaxRe = /__filename|__dirname|require\(|require\.resolve\(/
|
||||||
|
|
||||||
@ -46,19 +46,14 @@ function findStaticImports(code: string): StaticImport[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function esmShimPlugin(): Plugin {
|
export default function esmShimPlugin(): Plugin {
|
||||||
let sourcemap: boolean | 'inline' | 'hidden' = false
|
const CJSShim = supportImportMetaPaths() ? CJSShim_node_20_11 : CJSShim_normal
|
||||||
|
|
||||||
const CJSShim = getElectronMajorVersion() >= 30 ? CJSShim_node_20_11 : CJSShim_normal
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'vite:esm-shim',
|
name: 'vite:esm-shim',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
configResolved(config): void {
|
renderChunk(code, _chunk, { format, sourcemap }): { code: string; map?: SourceMapInput } | null {
|
||||||
sourcemap = config.build.sourcemap
|
if (format === 'es') {
|
||||||
},
|
|
||||||
renderChunk(code, _chunk, options): { code: string; map?: SourceMapInput } | null {
|
|
||||||
if (options.format === 'es') {
|
|
||||||
if (code.includes(CJSShim) || !CJSyntaxRe.test(code)) {
|
if (code.includes(CJSShim) || !CJSyntaxRe.test(code)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -7,7 +7,9 @@ export interface ExternalOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically externalize dependencies
|
* Automatically externalize dependencies.
|
||||||
|
*
|
||||||
|
* @deprecated use `build.externalizeDeps` config option instead
|
||||||
*/
|
*/
|
||||||
export function externalizeDepsPlugin(options: ExternalOptions = {}): Plugin | null {
|
export function externalizeDepsPlugin(options: ExternalOptions = {}): Plugin | null {
|
||||||
const { exclude = [], include = [] } = options
|
const { exclude = [], include = [] } = options
|
||||||
|
|||||||
198
src/plugins/isolateEntries.ts
Normal file
198
src/plugins/isolateEntries.ts
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */
|
||||||
|
import path from 'node:path'
|
||||||
|
import { type InlineConfig, type Plugin, type Logger, type LogLevel, build as viteBuild, mergeConfig } from 'vite'
|
||||||
|
import type { InputOptions, RollupOutput } from 'rollup'
|
||||||
|
import colors from 'picocolors'
|
||||||
|
import buildReporterPlugin from './buildReporter'
|
||||||
|
|
||||||
|
const VIRTUAL_ENTRY_ID = '\0virtual:isolate-entries'
|
||||||
|
|
||||||
|
const LogLevels: Record<LogLevel, number> = {
|
||||||
|
silent: 0,
|
||||||
|
error: 1,
|
||||||
|
warn: 2,
|
||||||
|
info: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function isolateEntriesPlugin(userConfig: InlineConfig): Plugin {
|
||||||
|
let logger: Logger
|
||||||
|
|
||||||
|
let entries: string[] | { [x: string]: string }[]
|
||||||
|
|
||||||
|
let transformedCount = 0
|
||||||
|
|
||||||
|
const assetCache = new Set<string>()
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'vite:isolate-entries',
|
||||||
|
apply: 'build',
|
||||||
|
|
||||||
|
configResolved(config): void {
|
||||||
|
logger = config.logger
|
||||||
|
},
|
||||||
|
|
||||||
|
options(opts): InputOptions | void {
|
||||||
|
const { input } = opts
|
||||||
|
if (input && typeof input === 'object') {
|
||||||
|
if ((Array.isArray(input) && input.length > 0) || Object.keys(input).length > 1) {
|
||||||
|
opts.input = VIRTUAL_ENTRY_ID
|
||||||
|
entries = Array.isArray(input) ? input : Object.entries(input).map(([key, value]) => ({ [key]: value }))
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
buildStart(): void {
|
||||||
|
transformedCount = 0
|
||||||
|
assetCache.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveId(id): string | null {
|
||||||
|
if (id === VIRTUAL_ENTRY_ID) {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
async load(id): Promise<string | void> {
|
||||||
|
if (id === VIRTUAL_ENTRY_ID) {
|
||||||
|
const shouldLog = LogLevels[userConfig.logLevel || 'info'] >= LogLevels.info
|
||||||
|
const shouldWatch = this.meta.watchMode
|
||||||
|
|
||||||
|
const watchFiles = new Set<string>()
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const re = await bundleEntryFile(entry, userConfig, shouldWatch, shouldLog, transformedCount)
|
||||||
|
|
||||||
|
const outputChunks = re.bundles.output
|
||||||
|
for (const chunk of outputChunks) {
|
||||||
|
if (assetCache.has(chunk.fileName)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: chunk.fileName,
|
||||||
|
source: chunk.type === 'chunk' ? chunk.code : chunk.source
|
||||||
|
})
|
||||||
|
assetCache.add(chunk.fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of re.watchFiles) {
|
||||||
|
watchFiles.add(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedCount += re.transformedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of watchFiles) {
|
||||||
|
this.addWatchFile(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
// This is the virtual entry file
|
||||||
|
console.log(1)`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStart(): void {
|
||||||
|
clearLine(-1)
|
||||||
|
logger.info(`${colors.green(`✓`)} ${transformedCount} modules transformed.`)
|
||||||
|
},
|
||||||
|
|
||||||
|
generateBundle(_, bundle): void {
|
||||||
|
for (const chunkName in bundle) {
|
||||||
|
if (chunkName.includes('virtual_isolate-entries')) {
|
||||||
|
delete bundle[chunkName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bundleEntryFile(
|
||||||
|
input: string | Record<string, string>,
|
||||||
|
config: InlineConfig,
|
||||||
|
watch: boolean,
|
||||||
|
shouldLog: boolean,
|
||||||
|
preTransformedCount: number
|
||||||
|
): Promise<{ bundles: RollupOutput; watchFiles: string[]; transformedCount: number }> {
|
||||||
|
const transformReporter = transformReporterPlugin(preTransformedCount, shouldLog)
|
||||||
|
const buildReporter = watch ? buildReporterPlugin() : undefined
|
||||||
|
|
||||||
|
const viteConfig = mergeConfig(config, {
|
||||||
|
build: {
|
||||||
|
write: false,
|
||||||
|
watch: false
|
||||||
|
},
|
||||||
|
plugins: [transformReporter, buildReporter],
|
||||||
|
logLevel: 'warn',
|
||||||
|
configFile: false
|
||||||
|
}) as InlineConfig
|
||||||
|
|
||||||
|
// rewrite the input instead of merging
|
||||||
|
viteConfig.build!.rollupOptions!.input = input
|
||||||
|
|
||||||
|
const bundles = await viteBuild(viteConfig)
|
||||||
|
|
||||||
|
return {
|
||||||
|
bundles: bundles as RollupOutput,
|
||||||
|
watchFiles: buildReporter?.api?.getWatchFiles() || [],
|
||||||
|
transformedCount: transformReporter?.api?.getTransformedCount() || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformReporterPlugin(
|
||||||
|
preTransformedCount = 0,
|
||||||
|
shouldLog = true
|
||||||
|
): Plugin<{ getTransformedCount: () => number }> {
|
||||||
|
let transformedCount = 0
|
||||||
|
let root
|
||||||
|
const log = throttle(id => {
|
||||||
|
writeLine(`transforming (${preTransformedCount + transformedCount}) ${colors.dim(path.relative(root, id))}`)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
name: 'vite:transform-reporter',
|
||||||
|
configResolved(config) {
|
||||||
|
root = config.root
|
||||||
|
},
|
||||||
|
transform(_, id) {
|
||||||
|
transformedCount++
|
||||||
|
if (!shouldLog) return
|
||||||
|
if (id.includes('?')) return
|
||||||
|
log(id)
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
getTransformedCount() {
|
||||||
|
return transformedCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLine(output: string): void {
|
||||||
|
clearLine()
|
||||||
|
if (output.length < process.stdout.columns) {
|
||||||
|
process.stdout.write(output)
|
||||||
|
} else {
|
||||||
|
process.stdout.write(output.substring(0, process.stdout.columns - 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLine(move: number = 0): void {
|
||||||
|
if (move < 0) {
|
||||||
|
process.stdout.moveCursor(0, move)
|
||||||
|
}
|
||||||
|
process.stdout.clearLine(0)
|
||||||
|
process.stdout.cursorTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function throttle(fn: Function) {
|
||||||
|
let timerHandle: NodeJS.Timeout | null = null
|
||||||
|
return (...args: any[]) => {
|
||||||
|
if (timerHandle) return
|
||||||
|
fn(...args)
|
||||||
|
timerHandle = setTimeout(() => {
|
||||||
|
timerHandle = null
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,58 +1,74 @@
|
|||||||
import type { Plugin } from 'vite'
|
import path from 'node:path'
|
||||||
import type { SourceMapInput } from 'rollup'
|
import { type Plugin, type InlineConfig, build as viteBuild, mergeConfig } from 'vite'
|
||||||
|
import type { SourceMapInput, RollupOutput, OutputOptions } from 'rollup'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { cleanUrl, parseRequest, toRelativePath } from '../utils'
|
import buildReporterPlugin from './buildReporter'
|
||||||
|
import { cleanUrl, toRelativePath } from '../utils'
|
||||||
|
import { supportImportMetaPaths } from '../electron'
|
||||||
|
|
||||||
const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g
|
const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve `?modulePath` import and return the module bundle path.
|
* Resolve `?modulePath` import and return the module bundle path.
|
||||||
*/
|
*/
|
||||||
export default function modulePathPlugin(): Plugin {
|
export default function modulePathPlugin(config: InlineConfig): Plugin {
|
||||||
let sourcemap: boolean | 'inline' | 'hidden' = false
|
const isImportMetaPathSupported = supportImportMetaPaths()
|
||||||
|
const assetCache = new Set<string>()
|
||||||
return {
|
return {
|
||||||
name: 'vite:module-path',
|
name: 'vite:module-path',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
configResolved(config): void {
|
buildStart(): void {
|
||||||
sourcemap = config.build.sourcemap
|
assetCache.clear()
|
||||||
},
|
},
|
||||||
resolveId(id, importer): string | void {
|
async load(id): Promise<string | void> {
|
||||||
const query = parseRequest(id)
|
if (id.endsWith('?modulePath')) {
|
||||||
if (query && typeof query.modulePath === 'string') {
|
// id resolved by Vite resolve plugin
|
||||||
return id + `&importer=${importer}`
|
const re = await bundleEntryFile(cleanUrl(id), config, this.meta.watchMode)
|
||||||
}
|
const [outputChunk, ...outputChunks] = re.bundles.output
|
||||||
},
|
|
||||||
load(id): string | void {
|
|
||||||
const query = parseRequest(id)
|
|
||||||
if (query && typeof query.modulePath === 'string' && typeof query.importer === 'string') {
|
|
||||||
const cleanPath = cleanUrl(id)
|
|
||||||
const hash = this.emitFile({
|
const hash = this.emitFile({
|
||||||
type: 'chunk',
|
type: 'asset',
|
||||||
id: cleanPath,
|
fileName: outputChunk.fileName,
|
||||||
importer: query.importer
|
source: outputChunk.code
|
||||||
})
|
})
|
||||||
|
for (const chunk of outputChunks) {
|
||||||
|
if (assetCache.has(chunk.fileName)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: chunk.fileName,
|
||||||
|
source: chunk.type === 'chunk' ? chunk.code : chunk.source
|
||||||
|
})
|
||||||
|
assetCache.add(chunk.fileName)
|
||||||
|
}
|
||||||
|
for (const id of re.watchFiles) {
|
||||||
|
this.addWatchFile(id)
|
||||||
|
}
|
||||||
const refId = `__VITE_MODULE_PATH__${hash}__`
|
const refId = `__VITE_MODULE_PATH__${hash}__`
|
||||||
|
const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname'
|
||||||
return `
|
return `
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
export default join(__dirname, ${refId})`
|
export default join(${dirnameExpr}, ${refId})`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null {
|
renderChunk(code, chunk, { sourcemap }): { code: string; map: SourceMapInput } | null {
|
||||||
if (code.match(modulePathRE)) {
|
let match: RegExpExecArray | null
|
||||||
let match: RegExpExecArray | null
|
let s: MagicString | undefined
|
||||||
const s = new MagicString(code)
|
|
||||||
|
|
||||||
while ((match = modulePathRE.exec(code))) {
|
modulePathRE.lastIndex = 0
|
||||||
const [full, hash] = match
|
while ((match = modulePathRE.exec(code))) {
|
||||||
const filename = this.getFileName(hash)
|
s ||= new MagicString(code)
|
||||||
const outputFilepath = toRelativePath(filename, chunk.fileName)
|
const [full, hash] = match
|
||||||
const replacement = JSON.stringify(outputFilepath)
|
const filename = this.getFileName(hash)
|
||||||
s.overwrite(match.index, match.index + full.length, replacement, {
|
const outputFilepath = toRelativePath(filename, chunk.fileName)
|
||||||
contentOnly: true
|
const replacement = JSON.stringify(outputFilepath)
|
||||||
})
|
s.overwrite(match.index, match.index + full.length, replacement, {
|
||||||
}
|
contentOnly: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s) {
|
||||||
return {
|
return {
|
||||||
code: s.toString(),
|
code: s.toString(),
|
||||||
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
||||||
@ -63,3 +79,45 @@ export default function modulePathPlugin(): Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bundleEntryFile(
|
||||||
|
input: string,
|
||||||
|
config: InlineConfig,
|
||||||
|
watch: boolean
|
||||||
|
): Promise<{ bundles: RollupOutput; watchFiles: string[] }> {
|
||||||
|
const reporter = watch ? buildReporterPlugin() : undefined
|
||||||
|
const viteConfig = mergeConfig(config, {
|
||||||
|
build: {
|
||||||
|
write: false,
|
||||||
|
watch: false
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'vite:entry-file-name',
|
||||||
|
outputOptions(output): OutputOptions {
|
||||||
|
if (typeof output.entryFileNames !== 'function' && output.entryFileNames) {
|
||||||
|
output.entryFileNames = '[name]-[hash]' + path.extname(output.entryFileNames)
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reporter
|
||||||
|
],
|
||||||
|
logLevel: 'warn',
|
||||||
|
configFile: false
|
||||||
|
}) as InlineConfig
|
||||||
|
|
||||||
|
// rewrite the input instead of merging
|
||||||
|
const buildOptions = viteConfig.build!
|
||||||
|
buildOptions.rollupOptions = {
|
||||||
|
...buildOptions.rollupOptions,
|
||||||
|
input
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundles = await viteBuild(viteConfig)
|
||||||
|
|
||||||
|
return {
|
||||||
|
bundles: bundles as RollupOutput,
|
||||||
|
watchFiles: reporter?.api?.getWatchFiles() || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,58 +1,58 @@
|
|||||||
import type { Plugin } from 'vite'
|
import type { Plugin } from 'vite'
|
||||||
import type { SourceMapInput } from 'rollup'
|
import type { SourceMapInput } from 'rollup'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { cleanUrl, parseRequest, toRelativePath } from '../utils'
|
import { cleanUrl, toRelativePath } from '../utils'
|
||||||
|
|
||||||
const nodeWorkerAssetUrlRE = /__VITE_NODE_WORKER_ASSET__([\w$]+)__/g
|
const nodeWorkerAssetUrlRE = /__VITE_NODE_WORKER_ASSET__([\w$]+)__/g
|
||||||
|
const nodeWorkerRE = /\?nodeWorker(?:&|$)/
|
||||||
|
const nodeWorkerImporterRE = /(?:\?)nodeWorker&importer=([^&]+)(?:&|$)/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve `?nodeWorker` import and automatically generate `Worker` wrapper.
|
* Resolve `?nodeWorker` import and automatically generate `Worker` wrapper.
|
||||||
*/
|
*/
|
||||||
export default function workerPlugin(): Plugin {
|
export default function workerPlugin(): Plugin {
|
||||||
let sourcemap: boolean | 'inline' | 'hidden' = false
|
|
||||||
return {
|
return {
|
||||||
name: 'vite:node-worker',
|
name: 'vite:node-worker',
|
||||||
apply: 'build',
|
apply: 'build',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
configResolved(config): void {
|
|
||||||
sourcemap = config.build.sourcemap
|
|
||||||
},
|
|
||||||
resolveId(id, importer): string | void {
|
resolveId(id, importer): string | void {
|
||||||
const query = parseRequest(id)
|
if (id.endsWith('?nodeWorker')) {
|
||||||
if (query && typeof query.nodeWorker === 'string') {
|
|
||||||
return id + `&importer=${importer}`
|
return id + `&importer=${importer}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
load(id): string | void {
|
load(id): string | void {
|
||||||
const query = parseRequest(id)
|
if (nodeWorkerRE.test(id)) {
|
||||||
if (query && typeof query.nodeWorker === 'string' && typeof query.importer === 'string') {
|
const match = nodeWorkerImporterRE.exec(id)
|
||||||
const cleanPath = cleanUrl(id)
|
if (match) {
|
||||||
const hash = this.emitFile({
|
const hash = this.emitFile({
|
||||||
type: 'chunk',
|
type: 'chunk',
|
||||||
id: cleanPath,
|
id: cleanUrl(id),
|
||||||
importer: query.importer
|
importer: match[1]
|
||||||
})
|
})
|
||||||
const assetRefId = `__VITE_NODE_WORKER_ASSET__${hash}__`
|
const assetRefId = `__VITE_NODE_WORKER_ASSET__${hash}__`
|
||||||
return `
|
return `
|
||||||
import { Worker } from 'node:worker_threads';
|
import { Worker } from 'node:worker_threads';
|
||||||
export default function (options) { return new Worker(new URL(${assetRefId}, import.meta.url), options); }`
|
export default function (options) { return new Worker(new URL(${assetRefId}, import.meta.url), options); }`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null {
|
renderChunk(code, chunk, { sourcemap }): { code: string; map: SourceMapInput } | null {
|
||||||
if (code.match(nodeWorkerAssetUrlRE)) {
|
let match: RegExpExecArray | null
|
||||||
let match: RegExpExecArray | null
|
let s: MagicString | undefined
|
||||||
const s = new MagicString(code)
|
|
||||||
|
|
||||||
while ((match = nodeWorkerAssetUrlRE.exec(code))) {
|
nodeWorkerAssetUrlRE.lastIndex = 0
|
||||||
const [full, hash] = match
|
while ((match = nodeWorkerAssetUrlRE.exec(code))) {
|
||||||
const filename = this.getFileName(hash)
|
s ||= new MagicString(code)
|
||||||
const outputFilepath = toRelativePath(filename, chunk.fileName)
|
const [full, hash] = match
|
||||||
const replacement = JSON.stringify(outputFilepath)
|
const filename = this.getFileName(hash)
|
||||||
s.overwrite(match.index, match.index + full.length, replacement, {
|
const outputFilepath = toRelativePath(filename, chunk.fileName)
|
||||||
contentOnly: true
|
const replacement = JSON.stringify(outputFilepath)
|
||||||
})
|
s.overwrite(match.index, match.index + full.length, replacement, {
|
||||||
}
|
contentOnly: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s) {
|
||||||
return {
|
return {
|
||||||
code: s.toString(),
|
code: s.toString(),
|
||||||
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
|
||||||
|
|||||||
@ -13,5 +13,5 @@ export async function preview(inlineConfig: InlineConfig = {}, options: { skipBu
|
|||||||
|
|
||||||
startElectron(inlineConfig.root)
|
startElectron(inlineConfig.root)
|
||||||
|
|
||||||
logger.info(colors.green(`\nstart electron app...\n`))
|
logger.info(colors.green(`\nstarting electron app...\n`))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,22 +31,20 @@ export async function createServer(
|
|||||||
const mainViteConfig = config.config?.main
|
const mainViteConfig = config.config?.main
|
||||||
if (mainViteConfig && !options.rendererOnly) {
|
if (mainViteConfig && !options.rendererOnly) {
|
||||||
const watchHook = (): void => {
|
const watchHook = (): void => {
|
||||||
logger.info(colors.green(`\nrebuild the electron main process successfully`))
|
logger.info(colors.green(`\nelectron main process rebuilt successfully`))
|
||||||
|
|
||||||
if (ps) {
|
if (ps) {
|
||||||
logger.info(colors.cyan(`\n waiting for electron to exit...`))
|
|
||||||
|
|
||||||
ps.removeAllListeners()
|
ps.removeAllListeners()
|
||||||
ps.kill()
|
ps.kill()
|
||||||
ps = startElectron(inlineConfig.root)
|
ps = startElectron(inlineConfig.root)
|
||||||
|
|
||||||
logger.info(colors.green(`\nrestart electron app...`))
|
logger.info(colors.green(`\nrestarting electron app...\n`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await doBuild(mainViteConfig, watchHook, errorHook)
|
await doBuild(mainViteConfig, watchHook, errorHook)
|
||||||
|
|
||||||
logger.info(colors.green(`\nbuild the electron main process successfully`))
|
logger.info(colors.green(`\nelectron main process built successfully`))
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadViteConfig = config.config?.preload
|
const preloadViteConfig = config.config?.preload
|
||||||
@ -54,10 +52,10 @@ export async function createServer(
|
|||||||
logger.info(colors.gray(`\n-----\n`))
|
logger.info(colors.gray(`\n-----\n`))
|
||||||
|
|
||||||
const watchHook = (): void => {
|
const watchHook = (): void => {
|
||||||
logger.info(colors.green(`\nrebuild the electron preload files successfully`))
|
logger.info(colors.green(`\nelectron preload scripts rebuilt successfully`))
|
||||||
|
|
||||||
if (server) {
|
if (server) {
|
||||||
logger.info(colors.cyan(`\n trigger renderer reload`))
|
logger.info(colors.cyan(`\nreloading electron renderer...\n`))
|
||||||
|
|
||||||
server.ws.send({ type: 'full-reload' })
|
server.ws.send({ type: 'full-reload' })
|
||||||
}
|
}
|
||||||
@ -65,14 +63,12 @@ export async function createServer(
|
|||||||
|
|
||||||
await doBuild(preloadViteConfig, watchHook, errorHook)
|
await doBuild(preloadViteConfig, watchHook, errorHook)
|
||||||
|
|
||||||
logger.info(colors.green(`\nbuild the electron preload files successfully`))
|
logger.info(colors.green(`\nelectron preload scripts built successfully`))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.rendererOnly) {
|
if (options.rendererOnly) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`\n${colors.yellow(colors.bold('warn'))}:${colors.yellow(
|
`\n${colors.yellow(colors.bold('(!)'))} ${colors.yellow('skipped building main process and preload scripts (using previous build)')}`
|
||||||
' you have skipped the main process and preload scripts building'
|
|
||||||
)}`
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +102,7 @@ export async function createServer(
|
|||||||
|
|
||||||
ps = startElectron(inlineConfig.root)
|
ps = startElectron(inlineConfig.root)
|
||||||
|
|
||||||
logger.info(colors.green(`\nstart electron app...\n`))
|
logger.info(colors.green(`\nstarting electron app...\n`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
src/utils.ts
56
src/utils.ts
@ -1,4 +1,4 @@
|
|||||||
import { URL, URLSearchParams } from 'node:url'
|
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
@ -20,16 +20,11 @@ export const hashRE = /#.*$/s
|
|||||||
|
|
||||||
export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '')
|
export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '')
|
||||||
|
|
||||||
export function parseRequest(id: string): Record<string, string> | null {
|
|
||||||
const { search } = new URL(id, 'file:')
|
|
||||||
if (!search) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return Object.fromEntries(new URLSearchParams(search))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHash(text: Buffer | string): string {
|
export function getHash(text: Buffer | string): string {
|
||||||
return createHash('sha256').update(text).digest('hex').substring(0, 8)
|
return createHash('sha256')
|
||||||
|
.update(text as unknown as Uint8Array)
|
||||||
|
.digest('hex')
|
||||||
|
.substring(0, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toRelativePath(filename: string, importer: string): string {
|
export function toRelativePath(filename: string, importer: string): string {
|
||||||
@ -84,3 +79,44 @@ export function isFilePathESM(filePath: string): boolean {
|
|||||||
return pkg?.type === 'module'
|
return pkg?.type === 'module'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeepWritable<T> =
|
||||||
|
T extends ReadonlyArray<unknown>
|
||||||
|
? { -readonly [P in keyof T]: DeepWritable<T[P]> }
|
||||||
|
: T extends RegExp
|
||||||
|
? RegExp
|
||||||
|
: T[keyof T] extends Function
|
||||||
|
? T
|
||||||
|
: { -readonly [P in keyof T]: DeepWritable<T[P]> }
|
||||||
|
|
||||||
|
export function deepClone<T>(value: T): DeepWritable<T> {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(v => deepClone(v)) as DeepWritable<T>
|
||||||
|
}
|
||||||
|
if (isObject(value)) {
|
||||||
|
const cloned: Record<string, any> = {}
|
||||||
|
for (const key in value) {
|
||||||
|
cloned[key] = deepClone(value[key])
|
||||||
|
}
|
||||||
|
return cloned as DeepWritable<T>
|
||||||
|
}
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
return value as DeepWritable<T>
|
||||||
|
}
|
||||||
|
if (value instanceof RegExp) {
|
||||||
|
return new RegExp(value) as DeepWritable<T>
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && value != null) {
|
||||||
|
throw new Error('Cannot deep clone non-plain object')
|
||||||
|
}
|
||||||
|
return value as DeepWritable<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
type AsyncFlatten<T extends unknown[]> = T extends (infer U)[] ? Exclude<Awaited<U>, U[]>[] : never
|
||||||
|
|
||||||
|
export async function asyncFlatten<T extends unknown[]>(arr: T): Promise<AsyncFlatten<T>> {
|
||||||
|
do {
|
||||||
|
arr = (await Promise.all(arr)).flat(Infinity) as any
|
||||||
|
} while (arr.some((v: any) => v?.then))
|
||||||
|
return arr as unknown[] as AsyncFlatten<T>
|
||||||
|
}
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2023",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user