Compare commits

..

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

38 changed files with 1997 additions and 4108 deletions

2
.eslintignore Normal file
View File

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

38
.eslintrc.js Normal file
View File

@ -0,0 +1,38 @@
module.exports = {
root: true,
env: {
commonjs: true,
es6: true,
node: true
},
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaVersion: 2021
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:prettier/recommended'
],
rules: {
'no-empty': ['warn', { allowEmptyCatch: true }],
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 'off'
},
overrides: [
{
files: ['*.js'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off'
}
}
]
}

View File

@ -1,5 +1,6 @@
name: "\U0001F41E Bug Report"
description: Report an issue with electron-vite
labels: ['bug', 'triage']
body:
- type: markdown
attributes:
@ -44,7 +45,7 @@ body:
required: true
- label: Read the [Contributing Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md).
required: true
- label: Read the [docs](https://electron-vite.org).
- label: Read the [docs](https://github.com/alex8088/electron-vite#readme).
required: true
- label: Check that there isn't [already an issue](https://github.com/alex8088/electron-vite/issues) that reports the same bug to avoid creating a duplicate.
required: true

View File

@ -1,5 +1 @@
blank_issues_enabled: false
contact_links:
- name: Questions & Discussions
url: https://github.com/alex8088/electron-vite/discussions
about: Use GitHub discussions for message-board style questions and discussions.

View File

@ -40,7 +40,7 @@ body:
required: true
- label: Read the [Contributing Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md).
required: true
- label: Read the [docs](https://electron-vite.org).
- label: Read the [docs](https://github.com/alex8088/electron-vite#readme).
required: true
- label: Check that there isn't [already an issue](https://github.com/alex8088/electron-vite/issues) that requests the same feature to avoid creating a duplicate.
- label: Check that there isn't [already an issue](https://github.com/alex8088/electron-vite/issues) that reports the same bug to avoid creating a duplicate.
required: true

3
.gitignore vendored
View File

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

11
.vscode/settings.json vendored
View File

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

View File

@ -1,279 +1,3 @@
### v3.1.0 (_2025-03-25_)
- fix(bytecodePlugin): optimize 'use strict' directive replacement ([#681](https://github.com/alex8088/electron-vite/issues/681))
- perf: build compatilibity target for Electron 35 ([#729](https://github.com/alex8088/electron-vite/pull/729))
- chore(deps): update all non-major dependencies
- chore(deps): update globals to v16
- chore(deps): update esbuild to v0.25
### v3.1.0-beta.0 (_2025-03-12_)
See [v3.1.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v3.1.0-beta.0/CHANGELOG.md)
### v3.0.0 (_2025-02-16_)
- feat: resolve conditions for preload
- perf: build compatilibity target for Electron 32
- perf: build compatilibity target for Electron 33 ([#651](https://github.com/alex8088/electron-vite/pull/651))
- perf: build compatilibity target for Electron 34
- chore: move to eslint flat config
- chore(deps): update all non-major dependencies
- chore(deps): update @rollup/plugin-node-resolve to v16
- chore(deps): update @rollup/plugin-typescript to v12
- chore(deps): update esbuild to v0.24
- chore(deps): update vite to v6
- chore(deps): update @type/node to v22
### v3.0.0-beta.0 (_2025-01-22_)
See [v3.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v3.0.0-beta.0/CHANGELOG.md)
### v2.3.0 (_2024-06-23_)
- feat: resolve import.meta.\[dirname|filename\] to support CommonJS format
- fix: don't handle module ID that begin with \0 ([#530](https://github.com/alex8088/electron-vite/pull/530))
- fix: not using the mode from the config file ([#539](https://github.com/alex8088/electron-vite/pull/539))
- fix: default mode should not overrite user config mode
- perf: build compatilibity target for Electron 31
- perf: improve cjs shim
- chore(deps): update all non-major dependencies
- chore(deps): update @typescript-eslint/\* to v7
- chore(deps): update esbuild to v0.21
### v2.2.0 (_2024-04-21_)
- feat: export mergeConfig from vite ([#471](https://github.com/alex8088/electron-vite/issues/471))
- fix(types): narrow down the return type of defineConfig
- perf: build compatilibity target for Electron 30
- refactor(config): defineConfig types
- chore: fix camelcase typo
- chore: use rollup-plugin-rm to clean dist
### v2.1.0 (_2024-03-03_)
- feat: easy way to fork processes and use workers
- fix: config via build.lib fails when default entry point not found ([#393](https://github.com/alex8088/electron-vite/issues/393))
- perf: build compatilibity target for Electron 29
- perf: allow integrating more complex render solutions ([#412](https://github.com/alex8088/electron-vite/pull/412))
- perf(bytecodePlugin): warn that strings cannot be protected when minification is enabled ([#417](https://github.com/alex8088/electron-vite/issues/417))
### v2.0.0 (_2024-01-09_)
- feat: bump minimum node version to 18
- feat: migrate to ESM
- feat: support vite 5
- feat: add package.json to export map
- feat: support ESM in Electron
- feat: env variables prefixed with VITE\_ will be shared in main process and renderer
- feat: support for passing arguments to electron in dev and preview commands ([#339](https://github.com/alex8088/electron-vite/pull/339))
- feat: config file supports "type": "module" in package.json
- fix: emit assets when ssr is enabled
- fix: externalizeDepPlugin not work
- fix: electron's export subpaths also need to be externalized ([#372](https://github.com/alex8088/electron-vite/issues/372))
- perf: improve package.json resolve
- perf: use magic-string hires boundary for sourcemaps
- perf: build compatilibity target for Electron 28
- pref: resolve import meta url in CommonJS format
- perf(worker): ESM syntax
- perf: package version
- perf: dev error message
- perf(externalizeDepsPlugin): use cached package data to improve performance
- perf: loadEnv api also needs to load shared env variables prefixed with VITE\_
- refactor: build
- refactor: file hashes use url-safe base64 encoded hashes in vite 5 (rollup 4)
- refactor: remove Electron 11, 12 build compatilibity target
- refactor: use dynamic import directly
- build: use rollup-plugin-dts
- chore(deps): update all non-major dependencies
- chore(deps): update lint-staged to v15
- chore(deps): update eslint-config-prettier to v9
- chore(deps): update @rollup/plugin-typescript to v11
- chore(deps): update rollup to v4
- chore(deps): update vite to v5
- chore(deps): update esbuild to v0.19
- chore(deps): update typescript to 5.3.3
- chore: improve prettier config
- chore: update homepage
### v2.0.0-beta.4 (_2024-01-06_)
See [v2.0.0-beta.4 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.4/CHANGELOG.md)
### v2.0.0-beta.3 (_2024-01-04_)
See [v2.0.0-beta.3 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.3/CHANGELOG.md)
### v2.0.0-beta.2 (_2023-12-19_)
See [v2.0.0-beta.2 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.2/CHANGELOG.md)
### v2.0.0-beta.1 (_2023-12-14_)
See [v2.0.0-beta.1 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.1/CHANGELOG.md)
### v2.0.0-beta.0 (_2023-12-13_)
See [v2.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v2.0.0-beta.0/CHANGELOG.md)
### v1.0.29 (_2023-11-17_)
- feat(cli): support --noSandbox option for dev and preview command
- perf: build compatilibity target for Electron 27
### v1.0.28 (_2023-09-18_)
- feat(cli): supports specifying electron entry file ([#270](https://github.com/alex8088/electron-vite/issues/270))
- fix(externalizeDepsPlugin): supports subpath
- perf: build compatilibity target for Electron 26
- chore(types): add .json?commonjs-external&asset typing
### v1.0.27 (_2023-08-01_)
- chore: remove preinstall script
### v1.0.26 (_2023-07-30_)
- feat(cli): add CLI `--inspect[-brk]` to support debugging without IDEs ([#231](https://github.com/alex8088/electron-vite/issues/231))
- feat(types): add process.env.ELECTRON_RENDERER_URL type
- feat(types): add Vite importMeta types
- perf: spawn Electron process using parent's stdios ([#236](https://github.com/alex8088/electron-vite/issues/236))
- chore: update user config interface jsdoc
- chore(deps): update pnpm to v8
- chore(deps): update prettier to v3
- chore(deps): update @typescript-eslint/\* to v6
### v1.0.25 (_2023-07-11_)
- fix: remove node resolve condition for preload [#204](https://github.com/alex8088/electron-vite/issues/204)
- fix(asset): asset handling error when hot reloading
- chore(deps): update all non-major dependencies
- chore(deps): update fs-extra to v11
- chore(deps): update @types/node to v18
- chore(deps): update typescript to 5.0.4
- chore(deps): update vite to 4.4.2
- chore(deps): update esbuild to v0.18
- chore(deps): update rollup to 3.26.2
### v1.0.24 (_2023-06-25_)
- fix(bytecodePlugin): bytecode loader relative path is incorrect
- perf: ignore `browser` field and additional `node` condition for main config
### v1.0.23 (_2023-06-04_)
- feat: supports ES build target for renderer [#174](https://github.com/alex8088/electron-vite/issues/174)
- revert: chore: remove process env define [#159](https://github.com/alex8088/electron-vite/issues/174)
- perf: build compatilibity target for Electron 25
- chore(deps): update all non-major dependencies
### v1.0.22 (_2023-04-23_)
- feat(cli): add --rendererOnly flag to dev command
- perf: build compatilibity target for Electron 24
- chore: remove process env define
- chore: typo error messages
### v1.0.21 (_2023-03-27_)
- fix(bytecodePlugin): bytecode loader is not referenced correctly in the chunks
- fix(bytecodePlugin): sub-chunks are not compliled in vite 4
- perf: always disable build.modulePreload in main and preload config
- chore(deps): update esbuild to 0.17
- chore(deps): update vite to 4.2.1
### v1.0.20 (_2023-03-12_)
- feat: support for renderer debugging [#130](https://github.com/alex8088/electron-vite/issues/130)
- fix(asset): asset path is not resolved correctly when outDir is specified [#117](https://github.com/alex8088/electron-vite/issues/117)
- fix: specified renderer outDir is not parsed correctly
- fix(bytecodePlugin): not work in monorepo [#128](https://github.com/alex8088/electron-vite/issues/128)
- perf: build compatilibity target for Electron 23
- perf: print log
- chore(deps): update all non-major dependencies
- chore(deps): update vite to 4.1.4
- chore(deps): update rollup to 3.18
- chore(deps): update magic-string to 0.30.0
### v1.0.19 (_2023-02-06_)
- feat(bytecodePlugin): protect strings [#91](https://github.com/alex8088/electron-vite/issues/91)
- fix(bytecodePlugin): escape protected strings (thanks to [@jeremyben](https://github.com/jeremyben))
### v1.0.18 (_2023-01-16_)
- feat(asset): support for WebAssembly in the main process
- fix(asset): wasm must be suffixed with `?loader`
### v1.0.17 (_2023-01-08_)
- feat: static asset handling
- fix: output duplicate log in vscode debugging [#75](https://github.com/alex8088/electron-vite/issues/75)
- chore(bytecodePlugin): KiB to kB
- chore(worker): use toRelativePath helper
- chore(deps): update all non-major dependencies
- chore(deps): update vite to 4.0.4
### v1.0.16 (_2022-12-12_)
- feat: vite 4.x support [#69](https://github.com/alex8088/electron-vite/issues/69)
- fix: `NODE_ENV` is incorrect in vite 4.x [#70](https://github.com/alex8088/electron-vite/issues/70)
- fix: invalid output format check
- fix: output format check
- chore(deps): update all non-major dependencies
- chore(deps): update esbuild and magic-string
- chore(deps): update vite to 4.0.0
### v1.0.15 (_2022-12-05_)
- feat: support mode and command conditional config
- feat: specify env prefixes for vite's loadEnv and export it
- perf: build compatilibity target for Electron 22
- perf: do not externalize node builtin modules for the renderer [#61](https://github.com/alex8088/electron-vite/issues/61)
### v1.0.14 (_2022-11-13_)
- fix(bytecodePlugin): replace bytecode module regex
### v1.0.13 (_2022-11-11_)
- feat: support for node worker
- refactor: plugins
- fix(swcPlugin): unreachable code
- fix(bytecodePlugin): bytecode loader injection and chunk module parsing errors [#49](https://github.com/alex8088/electron-vite/issues/49)
- fix: incorrect replace `__dirname`/`__filename` in config file
- fix: output format error under multiple entries
### v1.0.12 (_2022-11-02_)
- feat: support monorepo (by @ianstormtaylor)
- feat: add `--skipBuild` flag to preview command
- feat: make a SWC plugin to support TypeScript decorators (`emitDecoratorMetadata`)
- fix: use `modulePreload.polyfill` instead `polyfillModulePreload`
- chore: update deps
### v1.0.11 (_2022-10-16_)
- feat: externalize deps plugin
### v1.0.10 (_2022-10-07_)
- feat: export splitVendorChunk from vite
- fix: compatible with the latest version of Electron
- refactor: load config file
- perf: the bytecodePlugin transform arrow function by default
- perf: disable gzip-compressed size reporting, increase build performance
- perf: bytecode compilation log print format
- perf: build compatilibity target for Electron 21
### v1.0.9 (_2022-09-19_)
- feat: source code protection
- fix: specify a config file error
- fix: in the specified non-production mode, the `base` path is wrong
- perf: cache electron executable path
- chore: add electron-vite mode node env
- chore: use node prefix
### v1.0.8 (_2022-09-11_)
- feat: the main process and preload scripts support hot reloading [#7](https://github.com/alex8088/electron-vite/issues/7)

View File

@ -9,17 +9,17 @@
<p align="center">
<img src="https://img.shields.io/npm/v/electron-vite?color=6988e6&label=version">
<img src="https://img.shields.io/github/license/alex8088/electron-vite?color=blue" alt="license" />
<img src="https://img.shields.io/github/license/alex8088/wx-vue-next?color=blue" alt="license" />
</p>
<p align="center">
<a href="https://electron-vite.org">Documentation</a> |
<a href="https://electron-vite.org/guide">Getting Started</a> |
<a href="https://evite.netlify.app/">Documentation</a> |
<a href="https://evite.netlify.app/guide/">Getting Started</a> |
<a href="https://github.com/alex8088/quick-start/tree/master/packages/create-electron">create-electron</a>
</p>
<p align="center">
<a href="https://cn.electron-vite.org">中文文档</a>
<a href="https://cn-evite.netlify.app/">中文文档</a>
</p>
<br />
@ -27,15 +27,13 @@
## Features
- ⚡️ [Vite](https://vitejs.dev) powered and use the same way.
- 🛠 Pre-configured for Electron, don't worry about configuration.
- 💡 Optimize asset handling (Node.js addons, WebAssembly, Worker Thread, etc).
- 🚀 Fast HMR for renderer processes.
- 🔥 Hot reloading for main process and preload scripts.
- 🔌 Easy to debug in IDEs like VSCode or WebStorm.
- 🔒 Compile to v8 bytecode to protect source code.
- 🏷️ Support for TypeScript decorators.
- 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more.
- ⚡️ Inherit all the benefits of Vite and use the same way as [Vite](https://vitejs.dev).
- 📦The main process, renderers and preload scripts are all built with Vite.
- 🛠The main process, renderers and preload scripts Vite configuration combined into one file.
- 💡Pre-configured for Electron, don't worry about configuration.
- 🚀HMR for renderer processes.
- 🔥The main process and preload scripts support hot reloading.
- 🔌Easy to debug
## Usage
@ -59,7 +57,7 @@ In a project where `electron-vite` is installed, you can use `electron-vite` bin
}
```
### Configuration
### Configuring
When running `electron-vite` from the command line, electron-vite will automatically try to resolve a config file named `electron.vite.config.js` inside project root. The most basic config file looks like this:
@ -78,24 +76,73 @@ export default {
}
```
### Use HMR in Renderer
In order to use the renderer process HMR, you need to use the `environment variables` to determine whether the window browser loads a local html file or a local URL.
```js
function createWindow() {
// Create the browser window
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js')
}
})
// Load the remote URL for development or the local html file for production
if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}
}
```
### Hot Reloading
Hot reloading refers to quickly rebuilding and restarting the Electron app when the main process or preload scripts module changes. In fact, it's not really hot reloading, but similar. It also brings a good development experience to developers.
There are two ways to enable it:
1. Use CLI option `-w` or `--watch`, e.g. `electron-vite dev --watch`. This is the preferred way, it's more flexible.
2. Use configuration option `build.watch` and set to `{}`. In addition, more watcher options can be configured, see [WatcherOptions](https://rollupjs.org/guide/en/#watch-options).
### Debugging in VSCode
Add a file `.vscode/launch.json` with the following configuration:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"]
}
]
}
```
Then, set some breakpoints in `main.ts` (source code), and start debugging in the `VSCode Debug View`.
### Getting Started
Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project.
```bash
npm create @quick-start/electron
npm init @quick-start/electron
```
Currently supported template presets include:
| JavaScript | TypeScript |
| :--------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------: |
| [vanilla](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vanilla) | [vanilla-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vanilla-ts) |
| [vue](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vue) | [vue-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/vue-ts) |
| [react](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/react) | [react-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/react-ts) |
| [svelte](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/svelte) | [svelte-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/svelte-ts) |
| [solid](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/solid) | [solid-ts](https://github.com/alex8088/quick-start/tree/master/packages/create-electron/playground/solid-ts) |
## Contribution
See [Contributing Guide](CONTRIBUTING.md).

52
api-extractor.json Normal file
View File

@ -0,0 +1,52 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "./dist/types/index.d.ts",
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "",
"publicTrimmedFilePath": "./dist/index.d.ts"
},
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": false
},
"tsdocMetadata": {
"enabled": false
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "warning",
"addToApiReportFile": true
},
"ae-missing-release-tag": {
"logLevel": "none"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
},
"tsdoc-undefined-tag": {
"logLevel": "none"
}
}
}
}

View File

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

View File

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

View File

@ -1,42 +0,0 @@
// ts-check
import eslint from '@eslint/js'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import globals from 'globals'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ 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
}
},
rules: {
'prettier/prettier': 'warn',
'@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'
}
},
{
files: ['*.js', '*.mjs'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off'
}
}
)

112
node.d.ts vendored
View File

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

View File

@ -1,34 +1,19 @@
{
"name": "electron-vite",
"version": "3.1.0",
"version": "1.0.8",
"description": "Electron build tooling based on Vite",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./node": {
"types": "./node.d.ts"
},
"./package.json": "./package.json"
},
"bin": {
"electron-vite": "bin/electron-vite.js"
},
"files": [
"bin",
"dist",
"node.d.ts"
"dist"
],
"engines": {
"node": "^18.0.0 || >=20.0.0"
"node": ">=12.2.0"
},
"packageManager": "pnpm@8.6.10",
"author": "Alex Wei<https://github.com/alex8088>",
"license": "MIT",
"repository": {
@ -38,7 +23,7 @@
"bugs": {
"url": "https://github.com/alex8088/electron-vite/issues"
},
"homepage": "https://electron-vite.org",
"homepage": "https://github.com/alex8088/electron-vite#readme",
"keywords": [
"electron",
"vite",
@ -47,9 +32,9 @@
],
"scripts": {
"format": "prettier --write .",
"lint": "eslint --cache .",
"lint": "eslint --ext .ts src/**",
"typecheck": "tsc --noEmit",
"build": "pnpm run lint && rollup -c rollup.config.ts --configPlugin typescript"
"build": "npm run lint && node scripts/build.js"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged",
@ -65,42 +50,30 @@
]
},
"peerDependencies": {
"@swc/core": "^1.0.0",
"vite": "^4.0.0 || ^5.0.0 || ^6.0.0"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
}
"vite": "^3.0.0"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
"@swc/core": "^1.11.9",
"@types/node": "^22.13.10",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "^5.2.3",
"globals": "^16.0.0",
"lint-staged": "^15.4.3",
"prettier": "^3.5.3",
"rollup": "^4.35.0",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-rm": "^1.0.2",
"simple-git-hooks": "^2.11.1",
"tslib": "^2.8.1",
"typescript": "^5.7.3",
"typescript-eslint": "^8.26.1",
"vite": "^6.2.1"
"@microsoft/api-extractor": "^7.29.5",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-typescript": "^8.4.0",
"@types/node": "16.11.22",
"@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.35.1",
"eslint": "^8.22.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"fs-extra": "^10.1.0",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"rollup": "^2.78.1",
"simple-git-hooks": "^2.8.0",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"vite": "^3.0.9"
},
"dependencies": {
"@babel/core": "^7.26.10",
"@babel/plugin-transform-arrow-functions": "^7.25.9",
"cac": "^6.7.14",
"esbuild": "^0.25.1",
"magic-string": "^0.30.17",
"picocolors": "^1.1.1"
"cac": "^6.7.12",
"esbuild": "^0.14.54",
"picocolors": "^1.0.0"
}
}

2859
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +0,0 @@
import { createRequire } from 'node:module'
import { defineConfig } from 'rollup'
import ts from '@rollup/plugin-typescript'
import resolve from '@rollup/plugin-node-resolve'
import json from '@rollup/plugin-json'
import dts from 'rollup-plugin-dts'
import rm from 'rollup-plugin-rm'
const require = createRequire(import.meta.url)
const pkg = require('./package.json')
const external = ['esbuild', ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]
export default defineConfig([
{
input: ['src/index.ts', 'src/cli.ts'],
output: [
{
dir: 'dist',
entryFileNames: '[name].cjs',
chunkFileNames: 'chunks/lib-[hash].cjs',
format: 'cjs'
},
{
dir: 'dist',
entryFileNames: '[name].mjs',
chunkFileNames: 'chunks/lib-[hash].mjs',
format: 'es'
}
],
external,
plugins: [
rm('dist', 'buildStart'),
json(),
ts({ compilerOptions: { rootDir: 'src', declaration: true, declarationDir: 'dist/types' } }),
resolve()
],
treeshake: {
moduleSideEffects: false
}
},
{
input: 'dist/types/index.d.ts',
output: [{ file: pkg.types, format: 'es' }],
plugins: [dts(), rm('dist/types', 'buildEnd')]
}
])

67
scripts/build.js Normal file
View File

@ -0,0 +1,67 @@
const path = require('path')
const colors = require('picocolors')
const fs = require('fs-extra')
const rollup = require('rollup')
const typescript = require('@rollup/plugin-typescript')
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const { Extractor, ExtractorConfig } = require('@microsoft/api-extractor')
;(async () => {
const dist = path.resolve(__dirname, '../dist')
await fs.remove(dist)
console.log()
console.log(colors.bold(colors.yellow(`Rolling up ts code...`)))
const pkg = require('../package.json')
const external = ['esbuild', ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]
const bundle = await rollup.rollup({
input: {
index: path.resolve(__dirname, '../src/index.ts'),
cli: path.resolve(__dirname, '../src/cli.ts')
},
external,
plugins: [
typescript({
tsconfig: path.resolve(__dirname, '../tsconfig.json')
}),
nodeResolve()
],
treeshake: {
moduleSideEffects: false
}
})
await bundle.write({
dir: dist,
entryFileNames: '[name].js',
chunkFileNames: 'chunks/lib-[hash].js',
format: 'cjs'
})
console.log(colors.bold(colors.yellow(`Rolling up type definitions...`)))
if (pkg.types) {
const extractorConfig = ExtractorConfig.loadFileAndPrepare(path.resolve(__dirname, '../api-extractor.json'))
const extractorResult = Extractor.invoke(extractorConfig, {
localBuild: true,
showVerboseMessages: true
})
if (extractorResult.succeeded) {
console.log(colors.green('API Extractor completed successfully'))
} else {
console.error(
`API Extractor completed with ${extractorResult.errorCount} errors` +
` and ${extractorResult.warningCount} warnings`
)
process.exitCode = 1
}
}
await fs.remove(path.resolve(dist, 'types'))
console.log(colors.green(`Build ${pkg.name}@${pkg.version} successfully`))
})()

View File

@ -1,7 +1,7 @@
// Invoked on the commit-msg git hook by simple-git-hooks.
import colors from 'picocolors'
import fs from 'node:fs'
const colors = require('picocolors')
const fs = require('fs')
const msgPath = process.argv[2]
const msg = fs.readFileSync(msgPath, 'utf-8').trim()

View File

@ -5,7 +5,6 @@ import { InlineConfig, resolveConfig } from './config'
* Bundles the electron app for production.
*/
export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
process.env.NODE_ENV_ELECTRON_VITE = 'production'
const config = await resolveConfig(inlineConfig, 'build', 'production')
if (config.config) {
const mainViteConfig = config.config?.main

View File

@ -2,7 +2,6 @@ import { cac } from 'cac'
import colors from 'picocolors'
import { LogLevel, createLogger } from 'vite'
import { InlineConfig } from './config'
import { version } from '../package.json'
const cli = cac('electron-vite')
@ -25,20 +24,6 @@ interface GlobalCLIOptions {
w?: boolean
watch?: boolean
outDir?: string
entry?: string
}
interface DevCLIOptions {
inspect?: boolean | string
inspectBrk?: boolean | string
remoteDebuggingPort?: string
noSandbox?: boolean
rendererOnly?: boolean
}
interface PreviewCLIOptions {
noSandbox?: boolean
skipBuild?: boolean
}
function createInlineConfig(root: string, options: GlobalCLIOptions): InlineConfig {
@ -67,7 +52,6 @@ cli
.option('--ignoreConfigWarning', `[boolean] ignore config warning`)
.option('--sourcemap', `[boolean] output source maps for debug (default: false)`)
.option('--outDir <dir>', `[string] output directory (default: out)`)
.option('--entry <file>', `[string] specify electron entry file`)
// dev
cli
@ -75,41 +59,12 @@ cli
.alias('serve')
.alias('dev')
.option('-w, --watch', `[boolean] rebuilds when main process or preload script modules have changed on disk`)
.option('--inspect [port]', `[boolean | number] enable V8 inspector on the specified port`)
.option('--inspectBrk [port]', `[boolean | number] enable V8 inspector on the specified port`)
.option('--remoteDebuggingPort <port>', `[string] port for remote debugging`)
.option('--noSandbox', `[boolean] forces renderer process to run un-sandboxed`)
.option('--rendererOnly', `[boolean] only dev server for the renderer`)
.action(async (root: string, options: DevCLIOptions & GlobalCLIOptions) => {
if (options.remoteDebuggingPort) {
process.env.REMOTE_DEBUGGING_PORT = options.remoteDebuggingPort
}
if (options.inspect) {
process.env.V8_INSPECTOR_PORT = typeof options.inspect === 'number' ? `${options.inspect}` : '5858'
}
if (options.inspectBrk) {
process.env.V8_INSPECTOR_BRK_PORT = typeof options.inspectBrk === 'number' ? `${options.inspectBrk}` : '5858'
}
if (options.noSandbox) {
process.env.NO_SANDBOX = '1'
}
if (options['--']) {
process.env.ELECTRON_CLI_ARGS = JSON.stringify(options['--'])
}
if (options.entry) {
process.env.ELECTRON_ENTRY = options.entry
}
.action(async (root: string, options: GlobalCLIOptions) => {
const { createServer } = await import('./server')
const inlineConfig = createInlineConfig(root, options)
try {
await createServer(inlineConfig, { rendererOnly: options.rendererOnly })
await createServer(inlineConfig)
} catch (e) {
const error = e as Error
createLogger(options.logLevel).error(
@ -125,10 +80,6 @@ cli.command('build [root]', 'build for production').action(async (root: string,
const { build } = await import('./build')
const inlineConfig = createInlineConfig(root, options)
if (options.entry) {
process.env.ELECTRON_ENTRY = options.entry
}
try {
await build(inlineConfig)
} catch (e) {
@ -141,26 +92,12 @@ cli.command('build [root]', 'build for production').action(async (root: string,
// preview
cli
.command('preview [root]', 'start electron app to preview production build')
.option('--noSandbox', `[boolean] forces renderer process to run un-sandboxed`)
.option('--skipBuild', `[boolean] skip build`)
.action(async (root: string, options: PreviewCLIOptions & GlobalCLIOptions) => {
.action(async (root: string, options: GlobalCLIOptions) => {
const { preview } = await import('./preview')
const inlineConfig = createInlineConfig(root, options)
if (options.noSandbox) {
process.env.NO_SANDBOX = '1'
}
if (options.entry) {
process.env.ELECTRON_ENTRY = options.entry
}
if (options['--']) {
process.env.ELECTRON_CLI_ARGS = JSON.stringify(options['--'])
}
try {
await preview(inlineConfig, { skipBuild: options.skipBuild })
await preview(inlineConfig)
} catch (e) {
const error = e as Error
createLogger(options.logLevel).error(colors.red(`error during preview electron app:\n${error.stack}`), { error })
@ -169,6 +106,6 @@ cli
})
cli.help()
cli.version(version)
cli.version(require('../package.json').version)
cli.parse()

View File

@ -1,27 +1,20 @@
import path from 'node:path'
import fs from 'node:fs'
import { pathToFileURL } from 'node:url'
import { createRequire } from 'node:module'
import * as path from 'path'
import * as fs from 'fs'
import colors from 'picocolors'
import {
type UserConfig as ViteConfig,
type UserConfigExport as ViteConfigExport,
type ConfigEnv,
type Plugin,
type LogLevel,
UserConfig as ViteConfig,
UserConfigExport as UserViteConfigExport,
ConfigEnv,
Plugin,
LogLevel,
createLogger,
mergeConfig,
normalizePath
} from 'vite'
import { build } from 'esbuild'
import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugins/electron'
import assetPlugin from './plugins/asset'
import workerPlugin from './plugins/worker'
import importMetaPlugin from './plugins/importMeta'
import esmShimPlugin from './plugins/esm'
import modulePathPlugin from './plugins/modulePath'
import { isObject, isFilePathESM } from './utils'
import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugin'
import { isObject, dynamicImport } from './utils'
export { defineConfig as defineViteConfig } from 'vite'
@ -29,42 +22,42 @@ export interface UserConfig {
/**
* Vite config options for electron main process
*
* https://vitejs.dev/config/
* https://cn.vitejs.dev/config/
*/
main?: ViteConfig & { configFile?: string | false }
/**
* Vite config options for electron renderer process
*
* https://vitejs.dev/config/
* https://cn.vitejs.dev/config/
*/
renderer?: ViteConfig & { configFile?: string | false }
/**
* Vite config options for electron preload files
*
* https://vitejs.dev/config/
* https://cn.vitejs.dev/config/
*/
preload?: ViteConfig & { configFile?: string | false }
}
export interface ElectronViteConfig {
export interface UserConfigSchema {
/**
* Vite config options for electron main process
*
* https://vitejs.dev/config/
* https://cn.vitejs.dev/config/
*/
main?: ViteConfigExport
main?: UserViteConfigExport
/**
* Vite config options for electron renderer process
*
* https://vitejs.dev/config/
* https://cn.vitejs.dev/config/
*/
renderer?: ViteConfigExport
renderer?: UserViteConfigExport
/**
* Vite config options for electron preload files
*
* https://vitejs.dev/config/
* https://cn.vitejs.dev/config/
*/
preload?: ViteConfigExport
preload?: UserViteConfigExport
}
export type InlineConfig = Omit<ViteConfig, 'base'> & {
@ -73,28 +66,13 @@ export type InlineConfig = Omit<ViteConfig, 'base'> & {
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 =
| ElectronViteConfig
| Promise<ElectronViteConfig>
| ElectronViteConfigFnObject
| ElectronViteConfigFnPromise
| ElectronViteConfigFn
export type UserConfigExport = UserConfigSchema | Promise<UserConfigSchema>
/**
* Type helper to make it easier to use `electron.vite.config.*`
* accepts a direct {@link ElectronViteConfig} object, or a function that returns it.
* The function receives a object that exposes two properties:
* `command` (either `'build'` or `'serve'`), and `mode`.
* Type helper to make it easier to use `electron.vite.config.ts`
* accepts a direct {@link UserConfig} object, or a function that returns it.
*/
export function defineConfig(config: ElectronViteConfig): ElectronViteConfig
export function defineConfig(config: Promise<ElectronViteConfig>): Promise<ElectronViteConfig>
export function defineConfig(config: ElectronViteConfigFnObject): ElectronViteConfigFnObject
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport
export function defineConfig(config: ElectronViteConfigExport): ElectronViteConfigExport {
export function defineConfig(config: UserConfigExport): UserConfigExport {
return config
}
@ -112,7 +90,11 @@ export async function resolveConfig(
const config = inlineConfig
const mode = inlineConfig.mode || defaultMode
process.env.NODE_ENV = defaultMode
config.mode = mode
if (mode === 'production') {
process.env.NODE_ENV = 'production'
}
let userConfig: UserConfig | undefined
let configFileDependencies: string[] = []
@ -140,20 +122,11 @@ export async function resolveConfig(
if (loadResult.config.main) {
const mainViteConfig: ViteConfig = mergeConfig(loadResult.config.main, deepClone(config))
mainViteConfig.mode = inlineConfig.mode || mainViteConfig.mode || defaultMode
if (outDir) {
resetOutDir(mainViteConfig, outDir, 'main')
}
mergePlugins(mainViteConfig, [
...electronMainVitePlugin({ root }),
assetPlugin(),
workerPlugin(),
modulePathPlugin(),
importMetaPlugin(),
esmShimPlugin()
])
mergePlugins(mainViteConfig, electronMainVitePlugin({ root }))
loadResult.config.main = mainViteConfig
loadResult.config.main.configFile = false
@ -162,17 +135,10 @@ export async function resolveConfig(
if (loadResult.config.preload) {
const preloadViteConfig: ViteConfig = mergeConfig(loadResult.config.preload, deepClone(config))
preloadViteConfig.mode = inlineConfig.mode || preloadViteConfig.mode || defaultMode
if (outDir) {
resetOutDir(preloadViteConfig, outDir, 'preload')
}
mergePlugins(preloadViteConfig, [
...electronPreloadVitePlugin({ root }),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin()
])
mergePlugins(preloadViteConfig, electronPreloadVitePlugin({ root }))
loadResult.config.preload = preloadViteConfig
loadResult.config.preload.configFile = false
@ -181,8 +147,6 @@ export async function resolveConfig(
if (loadResult.config.renderer) {
const rendererViteConfig: ViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config))
rendererViteConfig.mode = inlineConfig.mode || rendererViteConfig.mode || defaultMode
if (outDir) {
resetOutDir(rendererViteConfig, outDir, 'renderer')
}
@ -242,13 +206,14 @@ export async function loadConfigFromFile(
config: UserConfig
dependencies: string[]
}> {
if (configFile && /^vite.config.(js|ts|mjs|cjs|mts|cts)$/.test(configFile)) {
let resolvedPath: string
let isESM = false
if (configFile && /^vite.config.(js)|(ts)|(mjs)|(cjs)$/.test(configFile)) {
throw new Error(`config file cannot be named ${configFile}.`)
}
const resolvedPath = configFile
? path.resolve(configFile)
: findConfigFile(configRoot, ['js', 'ts', 'mjs', 'cjs', 'mts', 'cts'])
resolvedPath = configFile ? path.resolve(configFile) : findConfigFile(configRoot, ['js', 'ts', 'mjs', 'cjs'])
if (!resolvedPath) {
return {
@ -258,13 +223,36 @@ export async function loadConfigFromFile(
}
}
const isESM = isFilePathESM(resolvedPath)
if (resolvedPath.endsWith('.mjs')) {
isESM = true
}
if (resolvedPath.endsWith('.js')) {
const pkg = path.join(configRoot, 'package.json')
if (fs.existsSync(pkg)) {
isESM = require(pkg).type === 'module'
}
}
const configFilePath = resolvedPath
try {
const bundled = await bundleConfigFile(resolvedPath, isESM)
const userConfig = await loadConfigFormBundledFile(configRoot, resolvedPath, bundled.code, isESM)
const bundled = await bundleConfigFile(resolvedPath)
const config = await (typeof userConfig === 'function' ? userConfig(configEnv) : userConfig)
if (!isESM) {
resolvedPath = path.resolve(configRoot, `${CONFIG_FILE_NAME}.mjs`)
fs.writeFileSync(resolvedPath, bundled.code)
}
const fileUrl = require('url').pathToFileURL(resolvedPath)
const userConfig = (await dynamicImport(fileUrl)).default
if (!isESM) {
fs.unlinkSync(resolvedPath)
}
const config = await (typeof userConfig === 'function' ? userConfig() : userConfig)
if (!isObject(config)) {
throw new Error(`config must export or return an object`)
}
@ -311,7 +299,7 @@ export async function loadConfigFromFile(
}
return {
path: normalizePath(resolvedPath),
path: normalizePath(configFilePath),
config: {
main: mainConfig,
renderer: rendererConfig,
@ -320,7 +308,7 @@ export async function loadConfigFromFile(
dependencies: bundled.dependencies
}
} catch (e) {
createLogger(logLevel).error(colors.red(`failed to load config from ${resolvedPath}`), { error: e as Error })
createLogger(logLevel).error(colors.red(`failed to load config from ${configFilePath}`), { error: e as Error })
throw e
}
}
@ -335,25 +323,16 @@ function findConfigFile(configRoot: string, extensions: string[]): string {
return ''
}
async function bundleConfigFile(fileName: string, isESM: boolean): Promise<{ code: string; dependencies: string[] }> {
const dirnameVarName = '__electron_vite_injected_dirname'
const filenameVarName = '__electron_vite_injected_filename'
const importMetaUrlVarName = '__electron_vite_injected_import_meta_url'
async function bundleConfigFile(fileName: string): Promise<{ code: string; dependencies: string[] }> {
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: [fileName],
write: false,
target: ['node18'],
platform: 'node',
bundle: true,
format: isESM ? 'esm' : 'cjs',
format: 'esm',
sourcemap: false,
metafile: true,
define: {
__dirname: dirnameVarName,
__filename: filenameVarName,
'import.meta.url': importMetaUrlVarName
},
plugins: [
{
name: 'externalize-deps',
@ -372,16 +351,14 @@ async function bundleConfigFile(fileName: string, isESM: boolean): Promise<{ cod
{
name: 'replace-import-meta',
setup(build): void {
build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async args => {
build.onLoad({ filter: /\.[jt]s$/ }, async args => {
const contents = await fs.promises.readFile(args.path, 'utf8')
const injectValues =
`const ${dirnameVarName} = ${JSON.stringify(path.dirname(args.path))};` +
`const ${filenameVarName} = ${JSON.stringify(args.path)};` +
`const ${importMetaUrlVarName} = ${JSON.stringify(pathToFileURL(args.path).href)};`
return {
loader: args.path.endsWith('ts') ? 'ts' : 'js',
contents: injectValues + contents
loader: args.path.endsWith('.ts') ? 'ts' : 'js',
contents: contents
.replace(/\bimport\.meta\.url\b/g, JSON.stringify(`file://${args.path}`))
.replace(/\b__dirname\b/g, JSON.stringify(path.dirname(args.path)))
.replace(/\b__filename\b/g, JSON.stringify(args.path))
}
})
}
@ -394,47 +371,3 @@ async function bundleConfigFile(fileName: string, isESM: boolean): Promise<{ cod
dependencies: result.metafile ? Object.keys(result.metafile.inputs) : []
}
}
interface NodeModuleWithCompile extends NodeModule {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
_compile(code: string, filename: string): any
}
const _require = createRequire(import.meta.url)
async function loadConfigFormBundledFile(
configRoot: string,
configFile: string,
bundledCode: string,
isESM: boolean
): Promise<ElectronViteConfigExport> {
if (isESM) {
const fileNameTmp = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${Date.now()}.mjs`)
fs.writeFileSync(fileNameTmp, bundledCode)
const fileUrl = pathToFileURL(fileNameTmp)
try {
return (await import(fileUrl.href)).default
} finally {
try {
fs.unlinkSync(fileNameTmp)
// eslint-disable-next-line no-empty
} catch {}
}
} else {
const extension = path.extname(configFile)
const realFileName = fs.realpathSync(configFile)
const loaderExt = extension in _require.extensions ? extension : '.js'
const defaultLoader = _require.extensions[loaderExt]!
_require.extensions[loaderExt] = (module: NodeModule, filename: string): void => {
if (filename === realFileName) {
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
} else {
defaultLoader(module, filename)
}
}
delete _require.cache[_require.resolve(configFile)]
const raw = _require(configFile)
_require.extensions[loaderExt] = defaultLoader
return raw.__esModule ? raw.default : raw
}
}

View File

@ -1,166 +1,51 @@
import path from 'node:path'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import { type ChildProcess, spawn } from 'node:child_process'
import { loadPackageData } from './utils'
const _require = createRequire(import.meta.url)
import { type ChildProcessWithoutNullStreams, spawn } from 'child_process'
import { type Logger } from 'vite'
const ensureElectronEntryFile = (root = process.cwd()): void => {
if (process.env.ELECTRON_ENTRY) return
const pkg = loadPackageData()
if (pkg) {
if (!pkg.main) {
throw new Error('No entry point found for electron app, please add a "main" field to package.json')
const pkg = path.join(root, 'package.json')
if (fs.existsSync(pkg)) {
const main = require(pkg).main
if (!main) {
throw new Error('not found an entry point to electorn app, please add main field for your package.json')
} else {
const entryPath = path.resolve(root, pkg.main)
const entryPath = path.resolve(root, main)
if (!fs.existsSync(entryPath)) {
throw new Error(`No electron app entry file found: ${entryPath}`)
throw new Error(`not found the electorn app entry file: ${entryPath}`)
}
}
} else {
throw new Error('Not found: package.json')
throw new Error('no package.json')
}
}
const getElectronMajorVer = (): string => {
let majorVer = process.env.ELECTRON_MAJOR_VER || ''
if (!majorVer) {
const pkg = _require.resolve('electron/package.json')
if (fs.existsSync(pkg)) {
const version = _require(pkg).version
majorVer = version.split('.')[0]
process.env.ELECTRON_MAJOR_VER = majorVer
}
const getElectronPath = (): string => {
const electronModulePath = path.resolve(process.cwd(), 'node_modules', 'electron')
const pathFile = path.join(electronModulePath, 'path.txt')
let executablePath
if (fs.existsSync(pathFile)) {
executablePath = fs.readFileSync(pathFile, 'utf-8')
}
if (executablePath) {
return path.join(electronModulePath, 'dist', executablePath)
} else {
throw new Error('Electron uninstall')
}
return majorVer
}
export function supportESM(): boolean {
const majorVer = getElectronMajorVer()
return parseInt(majorVer) >= 28
}
export function getElectronMajorVersion(): number {
const majorVer = getElectronMajorVer()
return parseInt(majorVer)
}
export function getElectronPath(): string {
let electronExecPath = process.env.ELECTRON_EXEC_PATH || ''
if (!electronExecPath) {
const electronModulePath = path.dirname(_require.resolve('electron'))
const pathFile = path.join(electronModulePath, 'path.txt')
let executablePath
if (fs.existsSync(pathFile)) {
executablePath = fs.readFileSync(pathFile, 'utf-8')
}
if (executablePath) {
electronExecPath = path.join(electronModulePath, 'dist', executablePath)
process.env.ELECTRON_EXEC_PATH = electronExecPath
} else {
throw new Error('Electron uninstall')
}
}
return electronExecPath
}
export function getElectronNodeTarget(): string {
const electronVer = getElectronMajorVer()
const nodeVer = {
'35': '22.14',
'34': '20.18',
'33': '20.18',
'32': '20.16',
'31': '20.14',
'30': '20.11',
'29': '20.9',
'28': '18.18',
'27': '18.17',
'26': '18.16',
'25': '18.15',
'24': '18.14',
'23': '18.12',
'22': '16.17',
'21': '16.16',
'20': '16.15',
'19': '16.14',
'18': '16.13',
'17': '16.13',
'16': '16.9',
'15': '16.5'
}
if (electronVer && parseInt(electronVer) > 10) {
let target = nodeVer[electronVer]
if (!target) target = Object.values(nodeVer).reverse()[0]
return 'node' + target
}
return ''
}
export function getElectronChromeTarget(): string {
const electronVer = getElectronMajorVer()
const chromeVer = {
'35': '134',
'34': '132',
'33': '130',
'32': '128',
'31': '126',
'30': '124',
'29': '122',
'28': '120',
'27': '118',
'26': '116',
'25': '114',
'24': '112',
'23': '110',
'22': '108',
'21': '106',
'20': '104',
'19': '102',
'18': '100',
'17': '98',
'16': '96',
'15': '94'
}
if (electronVer && parseInt(electronVer) > 10) {
let target = chromeVer[electronVer]
if (!target) target = Object.values(chromeVer).reverse()[0]
return 'chrome' + target
}
return ''
}
export function startElectron(root: string | undefined): ChildProcess {
export function startElectron(root: string | undefined, logger: Logger): ChildProcessWithoutNullStreams {
ensureElectronEntryFile(root)
const electronPath = getElectronPath()
const isDev = process.env.NODE_ENV_ELECTRON_VITE === 'development'
const args: string[] = process.env.ELECTRON_CLI_ARGS ? JSON.parse(process.env.ELECTRON_CLI_ARGS) : []
if (!!process.env.REMOTE_DEBUGGING_PORT && isDev) {
args.push(`--remote-debugging-port=${process.env.REMOTE_DEBUGGING_PORT}`)
}
if (!!process.env.V8_INSPECTOR_PORT && isDev) {
args.push(`--inspect=${process.env.V8_INSPECTOR_PORT}`)
}
if (!!process.env.V8_INSPECTOR_BRK_PORT && isDev) {
args.push(`--inspect-brk=${process.env.V8_INSPECTOR_BRK_PORT}`)
}
if (process.env.NO_SANDBOX === '1') {
args.push('--no-sandbox')
}
const entry = process.env.ELECTRON_ENTRY || '.'
const ps = spawn(electronPath, [entry].concat(args), { stdio: 'inherit' })
const ps = spawn(electronPath, ['.'])
ps.stdout.on('data', chunk => {
chunk.toString().trim() && logger.info(chunk.toString())
})
ps.stderr.on('data', chunk => {
chunk.toString().trim() && logger.error(chunk.toString())
})
ps.on('close', process.exit)
return ps

View File

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

363
src/plugin.ts Normal file
View File

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

View File

@ -1,189 +0,0 @@
import path from 'node:path'
import fs from 'node:fs/promises'
import type { SourceMapInput } from 'rollup'
import { type Plugin, normalizePath } from 'vite'
import MagicString from 'magic-string'
import { cleanUrl, parseRequest, getHash, toRelativePath } from '../utils'
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 nodePublicAssetRE = /__VITE_NODE_PUBLIC_ASSET__([a-z\d]{8})__/g
const wasmHelperId = '\0__electron-vite-wasm-helper'
const wasmHelperCode = `
import { join } from 'path'
import { readFile } from 'fs/promises'
export default async function loadWasm(file, importObject = {}) {
const wasmBuffer = await readFile(join(__dirname, file))
const result = await WebAssembly.instantiate(wasmBuffer, importObject)
return result.instance
}
`
export default function assetPlugin(): Plugin {
let sourcemap: boolean | 'inline' | 'hidden' = false
let publicDir = ''
let outDir = ''
const publicAssetPathCache = new Map<string, string>()
const assetCache = new Map<string, string>()
return {
name: 'vite:node-asset',
apply: 'build',
enforce: 'pre',
buildStart(): void {
publicAssetPathCache.clear()
assetCache.clear()
},
configResolved(config): void {
sourcemap = config.build.sourcemap
publicDir = normalizePath(config.publicDir)
outDir = normalizePath(path.resolve(config.root, config.build.outDir))
},
resolveId(id): string | void {
if (id === wasmHelperId) {
return id
}
},
async load(id): Promise<string | void> {
if (id === wasmHelperId) {
return wasmHelperCode
}
if (id.startsWith('\0')) {
// Rollup convention, this id should be handled by the
// plugin that marked it with \0
return
}
const assetResolved = resolveAsset(id)
if (!assetResolved) {
return
}
let referenceId: string
const file = assetResolved.file
if (publicDir && file.startsWith(publicDir)) {
const hash = getHash(file)
if (!publicAssetPathCache.get(hash)) {
publicAssetPathCache.set(hash, file)
}
referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__`
} else {
const cached = assetCache.get(file)
if (cached) {
referenceId = cached
} else {
const source = await fs.readFile(file)
const hash = this.emitFile({
type: 'asset',
name: path.basename(file),
source: source as unknown as Uint8Array
})
referenceId = `__VITE_NODE_ASSET__${hash}__`
assetCache.set(file, referenceId)
}
}
if (assetResolved.type === 'asset') {
if (assetResolved.query && typeof assetResolved.query.asarUnpack === 'string') {
return `
import { join } from 'path'
export default join(__dirname, ${referenceId}).replace('app.asar', 'app.asar.unpacked')`
} else {
return `
import { join } from 'path'
export default join(__dirname, ${referenceId})`
}
}
if (assetResolved.type === 'native') {
return `export default require(${referenceId})`
}
if (assetResolved.type === 'wasm') {
return `
import loadWasm from ${JSON.stringify(wasmHelperId)}
export default importObject => loadWasm(${referenceId}, importObject)`
}
},
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null {
let match: RegExpExecArray | null
let s: MagicString | undefined
nodeAssetRE.lastIndex = 0
if (code.match(nodeAssetRE)) {
while ((match = nodeAssetRE.exec(code))) {
s ||= new MagicString(code)
const [full, hash] = match
const filename = this.getFileName(hash)
const outputFilepath = toRelativePath(filename, chunk.fileName)
const replacement = JSON.stringify(outputFilepath)
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true
})
}
}
nodePublicAssetRE.lastIndex = 0
if (code.match(nodePublicAssetRE)) {
while ((match = nodePublicAssetRE.exec(code))) {
s ||= new MagicString(code)
const [full, hash] = match
const filename = publicAssetPathCache.get(hash)!
const outputFilepath = toRelativePath(filename, normalizePath(path.join(outDir, chunk.fileName)))
const replacement = JSON.stringify(outputFilepath)
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true
})
}
}
if (s) {
return {
code: s.toString(),
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
}
} else {
return null
}
}
}
}

View File

@ -1,379 +0,0 @@
import path from 'node:path'
import fs from 'node:fs'
import { spawn } from 'node:child_process'
import { createRequire } from 'node:module'
import colors from 'picocolors'
import { type Plugin, type ResolvedConfig, normalizePath, createFilter } from 'vite'
import * as babel from '@babel/core'
import MagicString from 'magic-string'
import type { SourceMapInput, OutputChunk } from 'rollup'
import { getElectronPath } from '../electron'
import { toRelativePath } from '../utils'
// Inspired by https://github.com/bytenode/bytenode
const _require = createRequire(import.meta.url)
function getBytecodeCompilerPath(): string {
return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', 'electron-bytecode.cjs')
}
function compileToBytecode(code: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
let data = Buffer.from([])
const electronPath = getElectronPath()
const bytecodePath = getBytecodeCompilerPath()
const proc = spawn(electronPath, [bytecodePath], {
env: { ELECTRON_RUN_AS_NODE: '1' },
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
})
if (proc.stdin) {
proc.stdin.write(code)
proc.stdin.end()
}
if (proc.stdout) {
proc.stdout.on('data', chunk => {
data = Buffer.concat([data, chunk])
})
proc.stdout.on('error', err => {
console.error(err)
})
proc.stdout.on('end', () => {
resolve(data)
})
}
if (proc.stderr) {
proc.stderr.on('data', chunk => {
console.error('Error: ', chunk.toString())
})
proc.stderr.on('error', err => {
console.error('Error: ', err)
})
}
proc.addListener('message', message => console.log(message))
proc.addListener('error', err => console.error(err))
proc.on('error', err => reject(err))
proc.on('exit', () => {
resolve(data)
})
})
}
const bytecodeModuleLoaderCode = [
`"use strict";`,
`const fs = require("fs");`,
`const path = require("path");`,
`const vm = require("vm");`,
`const v8 = require("v8");`,
`const Module = require("module");`,
`v8.setFlagsFromString("--no-lazy");`,
`v8.setFlagsFromString("--no-flush-bytecode");`,
`const FLAG_HASH_OFFSET = 12;`,
`const SOURCE_HASH_OFFSET = 8;`,
`let dummyBytecode;`,
`function setFlagHashHeader(bytecodeBuffer) {`,
` if (!dummyBytecode) {`,
` const script = new vm.Script("", {`,
` produceCachedData: true`,
` });`,
` dummyBytecode = script.createCachedData();`,
` }`,
` dummyBytecode.slice(FLAG_HASH_OFFSET, FLAG_HASH_OFFSET + 4).copy(bytecodeBuffer, FLAG_HASH_OFFSET);`,
`};`,
`function getSourceHashHeader(bytecodeBuffer) {`,
` return bytecodeBuffer.slice(SOURCE_HASH_OFFSET, SOURCE_HASH_OFFSET + 4);`,
`};`,
`function buffer2Number(buffer) {`,
` let ret = 0;`,
` ret |= buffer[3] << 24;`,
` ret |= buffer[2] << 16;`,
` ret |= buffer[1] << 8;`,
` ret |= buffer[0];`,
` return ret;`,
`};`,
`Module._extensions[".jsc"] = Module._extensions[".cjsc"] = function (module, filename) {`,
` const bytecodeBuffer = fs.readFileSync(filename);`,
` if (!Buffer.isBuffer(bytecodeBuffer)) {`,
` throw new Error("BytecodeBuffer must be a buffer object.");`,
` }`,
` setFlagHashHeader(bytecodeBuffer);`,
` const length = buffer2Number(getSourceHashHeader(bytecodeBuffer));`,
` let dummyCode = "";`,
` if (length > 1) {`,
` dummyCode = "\\"" + "\\u200b".repeat(length - 2) + "\\"";`,
` }`,
` const script = new vm.Script(dummyCode, {`,
` filename: filename,`,
` lineOffset: 0,`,
` displayErrors: true,`,
` cachedData: bytecodeBuffer`,
` });`,
` if (script.cachedDataRejected) {`,
` throw new Error("Invalid or incompatible cached data (cachedDataRejected)");`,
` }`,
` const require = function (id) {`,
` return module.require(id);`,
` };`,
` require.resolve = function (request, options) {`,
` return Module._resolveFilename(request, module, false, options);`,
` };`,
` if (process.mainModule) {`,
` require.main = process.mainModule;`,
` }`,
` require.extensions = Module._extensions;`,
` require.cache = Module._cache;`,
` const compiledWrapper = script.runInThisContext({`,
` filename: filename,`,
` lineOffset: 0,`,
` columnOffset: 0,`,
` displayErrors: true`,
` });`,
` const dirname = path.dirname(filename);`,
` const args = [module.exports, require, module, filename, dirname, process, global];`,
` return compiledWrapper.apply(module.exports, args);`,
`};`
]
export interface BytecodeOptions {
chunkAlias?: string | string[]
transformArrowFunctions?: boolean
removeBundleJS?: boolean
protectedStrings?: string[]
}
/**
* Compile to v8 bytecode to protect source code.
*/
export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') {
return null
}
const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options
const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
const filter = createFilter(/\.(m?[jt]s|[jt]sx)$/)
const escapeRegExpString = (str: string): string => {
return str
.replace(/\\/g, '\\\\\\\\')
.replace(/[|{}()[\]^$+*?.]/g, '\\$&')
.replace(/-/g, '\\u002d')
}
const transformAllChunks = _chunkAlias.length === 0
const isBytecodeChunk = (chunkName: string): boolean => {
return transformAllChunks || _chunkAlias.some(alias => alias === chunkName)
}
const _transform = (code: string): string => {
const re = babel.transform(code, {
plugins: ['@babel/plugin-transform-arrow-functions']
})
return re.code || ''
}
const useStrict = '"use strict";'
const bytecodeModuleLoader = 'bytecode-loader.cjs'
let config: ResolvedConfig
let useInRenderer = false
let bytecodeRequired = false
let bytecodeFiles: { name: string; size: number }[] = []
return {
name: 'vite:bytecode',
apply: 'build',
enforce: 'post',
configResolved(resolvedConfig): void {
config = resolvedConfig
useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config')
if (useInRenderer) {
config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.'))
}
if (resolvedConfig.build.minify && protectedStrings.length > 0) {
config.logger.warn(colors.yellow('Strings cannot be protected when minification is enabled.'))
}
},
transform(code, id): void | { code: string; map: SourceMapInput } {
if (config.build.minify || protectedStrings.length === 0 || !filter(id)) return
let match: RegExpExecArray | null
let s: MagicString | undefined
protectedStrings.forEach(str => {
const escapedStr = escapeRegExpString(str)
const re = new RegExp(`\\u0027${escapedStr}\\u0027|\\u0022${escapedStr}\\u0022`, 'g')
const charCodes = Array.from(str).map(s => s.charCodeAt(0))
const replacement = `String.fromCharCode(${charCodes.toString()})`
while ((match = re.exec(code))) {
s ||= new MagicString(code)
const [full] = match
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true
})
}
})
if (s) {
return {
code: s.toString(),
map: config.build.sourcemap ? s.generateMap({ hires: '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)
}
}
}
return null
},
generateBundle(options): void {
if (options.format !== 'es' && !useInRenderer && bytecodeRequired) {
this.emitFile({
type: 'asset',
source: bytecodeModuleLoaderCode.join('\n') + '\n',
name: 'Bytecode Loader File',
fileName: bytecodeModuleLoader
})
}
},
async writeBundle(options, output): Promise<void> {
if (options.format === 'es' || useInRenderer || !bytecodeRequired) {
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 nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName))
const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|')
const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null
const keepBundle = (chunkFileName: string): void => {
const newFileName = path.resolve(path.dirname(chunkFileName), `_${path.basename(chunkFileName)}`)
fs.renameSync(chunkFileName, newFileName)
}
const getBytecodeLoaderBlock = (chunkFileName: string): string => {
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
}
await Promise.all(
bundles.map(async name => {
const chunk = output[name]
if (chunk.type === 'chunk') {
let _code = chunk.code
if (bytecodeRE && _code.match(bytecodeRE)) {
let match: RegExpExecArray | null
const s = new MagicString(_code)
while ((match = bytecodeRE.exec(_code))) {
const [prefix, chunkName] = match
const len = prefix.length + chunkName.length
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
contentOnly: true
})
}
_code = s.toString()
}
const chunkFileName = path.resolve(outDir, name)
if (bytecodeChunks.includes(name)) {
const bytecodeBuffer = await compileToBytecode(_code)
fs.writeFileSync(path.resolve(outDir, name + 'c'), bytecodeBuffer as unknown as Uint8Array)
if (chunk.isEntry) {
if (!removeBundleJS) {
keepBundle(chunkFileName)
}
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
fs.writeFileSync(chunkFileName, code)
} else {
if (removeBundleJS) {
fs.unlinkSync(chunkFileName)
} else {
keepBundle(chunkFileName)
}
}
bytecodeFiles.push({ name: name + 'c', size: bytecodeBuffer.length })
} else {
if (chunk.isEntry) {
let hasBytecodeMoudle = false
const idsToHandle = new Set([...chunk.imports, ...chunk.dynamicImports])
for (const moduleId of idsToHandle) {
if (bytecodeChunks.includes(moduleId)) {
hasBytecodeMoudle = true
break
}
const moduleInfo = this.getModuleInfo(moduleId)
if (moduleInfo && !moduleInfo.isExternal) {
const { importers, dynamicImporters } = moduleInfo
for (const importerId of importers) idsToHandle.add(importerId)
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
}
}
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
_code = hasBytecodeMoudle
? _code.replace(/("use strict";)|('use strict';)/, `${useStrict}\n${bytecodeLoaderBlock}`)
: _code
}
fs.writeFileSync(chunkFileName, _code)
}
}
})
)
},
closeBundle(): void {
if (!useInRenderer) {
const chunkLimit = config.build.chunkSizeWarningLimit
const outDir = normalizePath(path.relative(config.root, path.resolve(config.root, config.build.outDir))) + '/'
config.logger.info(`${colors.green(``)} ${bytecodeFiles.length} bundles compiled into bytecode.`)
let longest = 0
bytecodeFiles.forEach(file => {
const len = file.name.length
if (len > longest) longest = len
})
bytecodeFiles.forEach(file => {
const kbs = file.size / 1000
config.logger.info(
`${colors.gray(colors.white(colors.dim(outDir)))}${colors.green(file.name.padEnd(longest + 2))} ${
kbs > chunkLimit ? colors.yellow(`${kbs.toFixed(2)} kB`) : colors.dim(`${kbs.toFixed(2)} kB`)
}`
)
})
bytecodeFiles = []
}
}
}
}

View File

@ -1,417 +0,0 @@
import path from 'node:path'
import fs from 'node:fs'
import { builtinModules } from 'node:module'
import colors from 'picocolors'
import { type Plugin, type LibraryOptions, mergeConfig, normalizePath } from 'vite'
import type { OutputOptions } from 'rollup'
import { getElectronNodeTarget, getElectronChromeTarget, supportESM } from '../electron'
import { loadPackageData } from '../utils'
export interface ElectronPluginOptions {
root?: string
}
function findLibEntry(root: string, scope: string): string | undefined {
for (const name of ['index', scope]) {
for (const ext of ['js', 'ts', 'mjs', 'cjs']) {
const entryFile = path.resolve(root, 'src', scope, `${name}.${ext}`)
if (fs.existsSync(entryFile)) {
return entryFile
}
}
}
return undefined
}
function findInput(root: string, scope = 'renderer'): string {
const rendererDir = path.resolve(root, 'src', scope, 'index.html')
if (fs.existsSync(rendererDir)) {
return rendererDir
}
return ''
}
function processEnvDefine(): Record<string, string> {
return {
'process.env': `process.env`,
'global.process.env': `global.process.env`,
'globalThis.process.env': `globalThis.process.env`
}
}
function resolveBuildOutputs(
outputs: OutputOptions | OutputOptions[] | undefined,
libOptions: LibraryOptions | false
): OutputOptions | OutputOptions[] | undefined {
if (libOptions && !Array.isArray(outputs)) {
const libFormats = libOptions.formats || []
return libFormats.map(format => ({ ...outputs, format }))
}
return outputs
}
export function electronMainVitePlugin(options?: ElectronPluginOptions): Plugin[] {
return [
{
name: 'vite:electron-main-preset-config',
apply: 'build',
enforce: 'pre',
config(config): void {
const root = options?.root || process.cwd()
const nodeTarget = getElectronNodeTarget()
const pkg = loadPackageData() || { type: 'commonjs' }
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
const defaultConfig = {
resolve: {
browserField: false,
mainFields: ['module', 'jsnext:main', 'jsnext'],
conditions: ['node']
},
build: {
outDir: path.resolve(root, 'out', 'main'),
target: nodeTarget,
assetsDir: 'chunks',
rollupOptions: {
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {}
},
reportCompressedSize: false,
minify: false
}
}
const build = config.build || {}
const rollupOptions = build.rollupOptions || {}
if (!rollupOptions.input) {
const libOptions = build.lib
const outputOptions = rollupOptions.output
defaultConfig.build['lib'] = {
entry: findLibEntry(root, 'main'),
formats:
libOptions && libOptions.formats && libOptions.formats.length > 0
? []
: [
outputOptions && !Array.isArray(outputOptions) && outputOptions.format
? outputOptions.format
: format
]
}
} else {
defaultConfig.build.rollupOptions.output['format'] = format
}
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
build.assetsDir || defaultConfig.build.assetsDir,
'[name]-[hash].[ext]'
)
const buildConfig = mergeConfig(defaultConfig.build, build)
config.build = buildConfig
config.resolve = mergeConfig(defaultConfig.resolve, config.resolve || {})
config.define = config.define || {}
config.define = { ...processEnvDefine(), ...config.define }
config.envPrefix = config.envPrefix || ['MAIN_VITE_', 'VITE_']
config.publicDir = config.publicDir || 'resources'
// do not copy public dir
config.build.copyPublicDir = false
// module preload polyfill does not apply to nodejs (main process)
config.build.modulePreload = false
// enable ssr build
config.build.ssr = true
config.build.ssrEmitAssets = true
config.ssr = { ...config.ssr, ...{ noExternal: true } }
}
},
{
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 libOptions = build.lib
const rollupOptions = build.rollupOptions
if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
throw new Error(
'An entry point is required in the electron vite main config, ' +
'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
)
}
const resolvedOutputs = resolveBuildOutputs(rollupOptions.output, libOptions)
if (resolvedOutputs) {
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
if (outputs.length > 1) {
throw new Error('The electron vite main config does not support multiple outputs.')
} else {
const outpout = outputs[0]
if (['es', 'cjs'].includes(outpout.format || '')) {
if (outpout.format === 'es' && !supportESM()) {
throw new Error(
'The electron vite main config output format does not support "es", ' +
'you can upgrade electron to the latest version or switch to "cjs" format.'
)
}
} else {
throw new Error(
`The electron vite main config output format must be "cjs"${supportESM() ? ' or "es"' : ''}.`
)
}
}
}
}
}
]
}
export function electronPreloadVitePlugin(options?: ElectronPluginOptions): Plugin[] {
return [
{
name: 'vite:electron-preload-preset-config',
apply: 'build',
enforce: 'pre',
config(config): void {
const root = options?.root || process.cwd()
const 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
}
},
{
name: 'vite:electron-preload-resolved-config',
apply: 'build',
enforce: 'post',
configResolved(config): void {
const build = config.build
if (!build.target) {
throw new Error('build.target 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 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
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.')
}
}
}
]
}

View File

@ -1,79 +0,0 @@
/*
* The core of this plugin was conceived by pi0 and is taken from the following repository:
* https://github.com/unjs/unbuild/blob/main/src/builder/plugins/cjs.ts
* license: https://github.com/unjs/unbuild/blob/main/LICENSE
*/
import MagicString from 'magic-string'
import type { SourceMapInput } from 'rollup'
import type { Plugin } from 'vite'
import { getElectronMajorVersion } from '../electron'
const CJSyntaxRe = /__filename|__dirname|require\(|require\.resolve\(/
const CJSShim_normal = `
// -- CommonJS Shims --
import __cjs_url__ from 'node:url';
import __cjs_path__ from 'node:path';
import __cjs_mod__ from 'node:module';
const __filename = __cjs_url__.fileURLToPath(import.meta.url);
const __dirname = __cjs_path__.dirname(__filename);
const require = __cjs_mod__.createRequire(import.meta.url);
`
const CJSShim_node_20_11 = `
// -- CommonJS Shims --
import __cjs_mod__ from 'node:module';
const __filename = import.meta.filename;
const __dirname = import.meta.dirname;
const require = __cjs_mod__.createRequire(import.meta.url);
`
const ESMStaticImportRe =
/(?<=\s|^|;)import\s*([\s"']*(?<imports>[\p{L}\p{M}\w\t\n\r $*,/{}@.]+)from\s*)?["']\s*(?<specifier>(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][\s;]*/gmu
interface StaticImport {
end: number
}
function findStaticImports(code: string): StaticImport[] {
const matches: StaticImport[] = []
for (const match of code.matchAll(ESMStaticImportRe)) {
matches.push({ end: (match.index || 0) + match[0].length })
}
return matches
}
export default function esmShimPlugin(): Plugin {
let sourcemap: boolean | 'inline' | 'hidden' = false
const CJSShim = getElectronMajorVersion() >= 30 ? CJSShim_node_20_11 : CJSShim_normal
return {
name: 'vite:esm-shim',
apply: 'build',
enforce: 'post',
configResolved(config): void {
sourcemap = config.build.sourcemap
},
renderChunk(code, _chunk, options): { code: string; map?: SourceMapInput } | null {
if (options.format === 'es') {
if (code.includes(CJSShim) || !CJSyntaxRe.test(code)) {
return null
}
const lastESMImport = findStaticImports(code).pop()
const indexToAppend = lastESMImport ? lastESMImport.end : 0
const s = new MagicString(code)
s.appendRight(indexToAppend, CJSShim)
return {
code: s.toString(),
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
}
}
return null
}
}
}

View File

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

View File

@ -1,21 +0,0 @@
import type { Plugin } from 'vite'
export default function importMetaPlugin(): Plugin {
return {
name: 'vite:import-meta',
apply: 'build',
enforce: 'pre',
resolveImportMeta(property, { format }): string | null {
if (property === 'url' && format === 'cjs') {
return `require("url").pathToFileURL(__filename).href`
}
if (property === 'filename' && format === 'cjs') {
return `__filename`
}
if (property === 'dirname' && format === 'cjs') {
return `__dirname`
}
return null
}
}
}

View File

@ -1,65 +0,0 @@
import type { Plugin } from 'vite'
import type { SourceMapInput } from 'rollup'
import MagicString from 'magic-string'
import { cleanUrl, parseRequest, toRelativePath } from '../utils'
const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g
/**
* Resolve `?modulePath` import and return the module bundle path.
*/
export default function modulePathPlugin(): Plugin {
let sourcemap: boolean | 'inline' | 'hidden' = false
return {
name: 'vite:module-path',
apply: 'build',
enforce: 'pre',
configResolved(config): void {
sourcemap = config.build.sourcemap
},
resolveId(id, importer): string | void {
const query = parseRequest(id)
if (query && typeof query.modulePath === 'string') {
return id + `&importer=${importer}`
}
},
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({
type: 'chunk',
id: cleanPath,
importer: query.importer
})
const refId = `__VITE_MODULE_PATH__${hash}__`
return `
import { join } from 'path'
export default join(__dirname, ${refId})`
}
},
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null {
if (code.match(modulePathRE)) {
let match: RegExpExecArray | null
const s = new MagicString(code)
while ((match = modulePathRE.exec(code))) {
const [full, hash] = match
const filename = this.getFileName(hash)
const outputFilepath = toRelativePath(filename, chunk.fileName)
const replacement = JSON.stringify(outputFilepath)
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true
})
}
return {
code: s.toString(),
map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null
}
}
return null
}
}
}

View File

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

View File

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

View File

@ -4,14 +4,12 @@ import type { InlineConfig } from './config'
import { startElectron } from './electron'
import { build } from './build'
export async function preview(inlineConfig: InlineConfig = {}, options: { skipBuild?: boolean }): Promise<void> {
if (!options.skipBuild) {
await build(inlineConfig)
}
export async function preview(inlineConfig: InlineConfig = {}): Promise<void> {
await build(inlineConfig)
const logger = createLogger(inlineConfig.logLevel)
startElectron(inlineConfig.root)
startElectron(inlineConfig.root, logger)
logger.info(colors.green(`\nstart electron app...\n`))
logger.info(colors.green(`\nstart electron app...`))
}

View File

@ -1,8 +1,8 @@
import type { ChildProcess } from 'node:child_process'
import type { ChildProcessWithoutNullStreams } from 'child_process'
import {
type UserConfig as ViteConfig,
type ViteDevServer,
createServer as viteCreateServer,
createServer as ViteCreateServer,
build as viteBuild,
createLogger,
mergeConfig
@ -12,24 +12,16 @@ import { type InlineConfig, resolveConfig } from './config'
import { resolveHostname } from './utils'
import { startElectron } from './electron'
export async function createServer(
inlineConfig: InlineConfig = {},
options: { rendererOnly?: boolean }
): Promise<void> {
process.env.NODE_ENV_ELECTRON_VITE = 'development'
export async function createServer(inlineConfig: InlineConfig = {}): Promise<void> {
const config = await resolveConfig(inlineConfig, 'serve', 'development')
if (config.config) {
const logger = createLogger(inlineConfig.logLevel)
let server: ViteDevServer | undefined
let ps: ChildProcess | undefined
const errorHook = (e): void => {
logger.error(`${colors.bgRed(colors.white(' ERROR '))} ${colors.red(e.message)}`)
}
let ps: ChildProcessWithoutNullStreams | undefined
const mainViteConfig = config.config?.main
if (mainViteConfig && !options.rendererOnly) {
if (mainViteConfig) {
const watchHook = (): void => {
logger.info(colors.green(`\nrebuild the electron main process successfully`))
@ -38,19 +30,19 @@ export async function createServer(
ps.removeAllListeners()
ps.kill()
ps = startElectron(inlineConfig.root)
ps = startElectron(inlineConfig.root, logger)
logger.info(colors.green(`\nrestart electron app...`))
}
}
await doBuild(mainViteConfig, watchHook, errorHook)
await doBuild(mainViteConfig, watchHook)
logger.info(colors.green(`\nbuild the electron main process successfully`))
}
const preloadViteConfig = config.config?.preload
if (preloadViteConfig && !options.rendererOnly) {
if (preloadViteConfig) {
logger.info(colors.gray(`\n-----\n`))
const watchHook = (): void => {
@ -63,24 +55,16 @@ export async function createServer(
}
}
await doBuild(preloadViteConfig, watchHook, errorHook)
await doBuild(preloadViteConfig, watchHook)
logger.info(colors.green(`\nbuild the electron preload files successfully`))
}
if (options.rendererOnly) {
logger.warn(
`\n${colors.yellow(colors.bold('warn'))}:${colors.yellow(
' you have skipped the main process and preload scripts building'
)}`
)
}
const rendererViteConfig = config.config?.renderer
if (rendererViteConfig) {
logger.info(colors.gray(`\n-----\n`))
server = await viteCreateServer(rendererViteConfig)
server = await ViteCreateServer(rendererViteConfig)
if (!server.httpServer) {
throw new Error('HTTP server not available')
@ -98,21 +82,21 @@ export async function createServer(
const slogger = server.config.logger
slogger.info(colors.green(`dev server running for the electron renderer process at:\n`), {
clear: !slogger.hasWarned && !options.rendererOnly
clear: !slogger.hasWarned
})
server.printUrls()
}
ps = startElectron(inlineConfig.root)
ps = startElectron(inlineConfig.root, logger)
logger.info(colors.green(`\nstart electron app...\n`))
logger.info(colors.green(`\nstart electron app...`))
}
}
type UserConfig = ViteConfig & { configFile?: string | false }
async function doBuild(config: UserConfig, watchHook: () => void, errorHook: (e: Error) => void): Promise<void> {
async function doBuild(config: UserConfig, watchHook: () => void): Promise<void> {
return new Promise(resolve => {
if (config.build?.watch) {
let firstBundle = true
@ -135,12 +119,10 @@ async function doBuild(config: UserConfig, watchHook: () => void, errorHook: (e:
})
}
viteBuild(config)
.then(() => {
if (!config.build?.watch) {
resolve()
}
})
.catch(e => errorHook(e))
viteBuild(config).then(() => {
if (!config.build?.watch) {
resolve()
}
})
})
}

View File

@ -1,89 +1,11 @@
import { URL, URLSearchParams } from 'node:url'
import path from 'node:path'
import fs from 'node:fs'
import { createHash } from 'node:crypto'
import { createRequire } from 'node:module'
import { loadEnv as viteLoadEnv } from 'vite'
export function isObject(value: unknown): value is Record<string, unknown> {
return Object.prototype.toString.call(value) === '[object Object]'
}
export const dynamicImport = new Function('file', 'return import(file)')
export const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000'])
export function resolveHostname(optionsHost: string | boolean | undefined): string {
return typeof optionsHost === 'string' && !wildcardHosts.has(optionsHost) ? optionsHost : 'localhost'
}
export const queryRE = /\?.*$/s
export const hashRE = /#.*$/s
export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '')
export function 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 {
return createHash('sha256')
.update(text as unknown as Uint8Array)
.digest('hex')
.substring(0, 8)
}
export function toRelativePath(filename: string, importer: string): string {
const relPath = path.posix.relative(path.dirname(importer), filename)
return relPath.startsWith('.') ? relPath : `./${relPath}`
}
/**
* Load `.env` files within the `envDir` (default: `process.cwd()`) .
* By default, only env variables prefixed with `VITE_`, `MAIN_VITE_`, `PRELOAD_VITE_` and
* `RENDERER_VITE_` are loaded, unless `prefixes` is changed.
*/
export function loadEnv(
mode: string,
envDir: string = process.cwd(),
prefixes: string | string[] = ['VITE_', 'MAIN_VITE_', 'PRELOAD_VITE_', 'RENDERER_VITE_']
): Record<string, string> {
return viteLoadEnv(mode, envDir, prefixes)
}
interface PackageData {
main?: string
type?: 'module' | 'commonjs'
dependencies?: Record<string, string>
}
let packageCached: PackageData | null = null
export function loadPackageData(root = process.cwd()): PackageData | null {
if (packageCached) return packageCached
const pkg = path.join(root, 'package.json')
if (fs.existsSync(pkg)) {
const _require = createRequire(import.meta.url)
const data = _require(pkg)
packageCached = {
main: data.main,
type: data.type,
dependencies: data.dependencies
}
return packageCached
}
return null
}
export function isFilePathESM(filePath: string): boolean {
if (/\.m[jt]s$/.test(filePath) || filePath.endsWith('.ts')) {
return true
} else if (/\.c[jt]s$/.test(filePath)) {
return false
} else {
const pkg = loadPackageData()
return pkg?.type === 'module'
}
}

View File

@ -1,20 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ESNext"],
"target": "es2019",
"module": "esnext",
"lib": ["esnext"],
"sourceMap": false,
"strict": true,
"allowJs": true,
"esModuleInterop": true,
"moduleResolution": "Node",
"moduleResolution": "node",
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": false,
"noImplicitReturns": true
"noImplicitReturns": true,
"declaration": true,
"declarationDir": "dist/types",
"outDir": "dist"
},
"include": ["src", "rollup.config.ts"]
"include": ["src"]
}