chore: init

This commit is contained in:
alex8088 2022-03-17 16:21:02 +08:00
commit ef99b02b18
33 changed files with 3841 additions and 0 deletions

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

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'
}
}
]
}

51
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,51 @@
name: "\U0001F41E Bug Report"
description: Report an issue with electron-vite
labels: ['bug', 'triage']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
placeholder: Bug description
validations:
required: true
- type: input
id: electron-vite-version
attributes:
label: Electron-Vite Version
description: What version of Electron-Vite are you using?
validations:
required: true
- type: input
id: electron-version
attributes:
label: Electron Version
description: What version of Electron are you using?
validations:
required: true
- type: input
id: vite-version
attributes:
label: Vite Version
description: What version of Vite are you using?
validations:
required: true
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Follow the [Code of Conduct](https://github.com/alex8088/electron-vite/blob/master/CODE_OF_CONDUCT.md).
required: true
- label: Read the [Contributing Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md).
required: true
- 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

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1 @@
blank_issues_enabled: false

View file

@ -0,0 +1,46 @@
name: "\U0001F680 New Feature Proposal"
description: Propose a new feature to be added to electron-vite
labels: ['enhancement']
body:
- type: markdown
attributes:
value: |
Thanks for your interest in the project and taking the time to fill out this feature report!
- type: textarea
id: feature-description
attributes:
label: Clear and concise description of the problem
description: 'As a developer using Vite I want [goal / wish] so that [benefit]. If you intend to submit a PR for this issue, tell us in the description. Thanks!'
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Suggested solution
description: 'In module [xy] we could provide following implementation...'
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Alternative
description: Clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Any other context or screenshots about the feature request here.
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Follow the [Code of Conduct](https://github.com/alex8088/electron-vite/blob/master/CODE_OF_CONDUCT.md).
required: true
- label: Read the [Contributing Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md).
required: true
- 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

24
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,24 @@
<!-- Thank you for contributing! -->
### Description
<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->
### Additional context
<!-- e.g. is there anything you'd like reviewers to focus on? -->
---
### What is the purpose of this pull request? <!-- (put an "X" next to an item) -->
- [ ] Bug fix
- [ ] New Feature
- [ ] Documentation update
- [ ] Other
### Before submitting the PR, please make sure you do the following
- [ ] Read the [Contributing Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md).
- [ ] Read the [Pull Request Guidelines](https://github.com/alex8088/electron-vite/blob/master/CONTRIBUTING.md#pull-request) and follow the [Commit Convention](https://github.com/alex8088/electron-vite/blob/master/.github/commit-convention.md).
- [ ] Provide a description in this PR that addresses **what** the PR is solving, or reference the issue that it solves (e.g. `fixes #123`).

92
.github/commit-convention.md vendored Normal file
View file

@ -0,0 +1,92 @@
## Git Commit Message Convention
> This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular).
#### TL;DR:
Messages must be matched by the following regex:
<!-- prettier-ignore -->
```js
/^(revert: )?(feat|fix|docs|dx|refactor|perf|test|workflow|build|ci|chore|types|wip|release|deps)(\(.+\))?: .{1,50}/
```
#### Examples
Appears under "Features" header, `dev` subheader:
```
feat(dev): add 'comments' option
```
Appears under "Bug Fixes" header, `dev` subheader, with a link to issue #28:
```
fix(dev): fix dev error
close #28
```
Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation:
```
perf(build): remove 'foo' option
BREAKING CHANGE: The 'foo' option has been removed.
```
The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.
```
revert: feat(compiler): add 'comments' option
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
```
### Full Message Format
A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
```
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
The **header** is mandatory and the **scope** of the header is optional.
### Revert
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body, it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
### Type
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However, if there is any [BREAKING CHANGE](#footer), the commit will always appear in the changelog.
Other prefixes are up to your discretion. Suggested prefixes are `docs`, `chore`, `style`, `refactor`, and `test` for non-changelog related tasks.
### Scope
The scope could be anything specifying the place of the commit change. For example `dev`, `build`, `workflow`, `cli` etc...
### Subject
The subject contains a succinct description of the change:
- use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize the first letter
- no dot (.) at the end
### Body
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
The body should include the motivation for the change and contrast this with previous behavior.
### Footer
The footer should contain any information about **Breaking Changes** and is also the place to
reference GitHub issues that this commit **Closes**.
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.

24
.github/workflows/release-tag.yml vendored Normal file
View file

@ -0,0 +1,24 @@
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Create Release
jobs:
build:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Create Release for Tag
id: release_tag
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body: |
Please refer to [CHANGELOG.md](https://github.com/alex8088/electron-vite/blob/${{ github.ref_name }}/CHANGELOG.md) for details.

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules
dist
*.log*

2
.prettierignore Normal file
View file

@ -0,0 +1,2 @@
dist
pnpm-lock.yaml

5
.prettierrc.yaml Normal file
View file

@ -0,0 +1,5 @@
singleQuote: true
semi: false
printWidth: 120
trailingComma: none
arrowParens: avoid

3
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

5
CHANGELOG.md Normal file
View file

@ -0,0 +1,5 @@
### v1.0.0 (_2022-03-17_)
#### Features
- electron-vite

45
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,45 @@
# Code Of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, political party, or sexual identity and orientation. Note, however, that religion, political party, or other ideological affiliation provide no exemptions for the behavior we outline as unacceptable in this Code of Conduct.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at alexwei114@163.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org

21
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,21 @@
# Contributing
Thanks for being interested in contributing to this project!
## Repo Setup
Clone this repo to your local machine and install the dependencies.
```bash
pnpm install
```
**NOTE**: The package manager used to install and link dependencies must be pnpm.
## Pull Request
- Checkout a topic branch from a base branch, e.g. master, and merge back against that branch.
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
- To check that your contributions match the project coding style make sure `pnpm lint` && `pnpm typecheck` passes. To build project run: `pnpm build`.
- Commit messages must follow the [commit message convention](./.github/commit-convention.md). Commit messages are automatically validated before commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks)).
- No need to worry about code style as long as you have installed the dev dependencies - modified files are automatically formatted with Prettier on commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks)).

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022, Alex Wei
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

328
README.md Normal file
View file

@ -0,0 +1,328 @@
# electron-vite
<p>
<img src="https://img.shields.io/badge/node->14.0.0-blue.svg" alt="node" />
<img src="https://img.shields.io/badge/vite->2.6.0-747bff.svg" alt="vite" />
</p>
> An Electron CLI integrated with Vite
---
## Features
- ⚡Use the same way as [Vite](https://vitejs.dev)
- 🔨Both main process and renderer process source code are built using Vite
- 📃The main process and the renderer process Vite configuration are combined into one file
- 📦Preset optimal build configuration
- 🚀HMR for renderer processes
## Usage
### Install
```sh
npm i electron-vite -D
```
### Development & Build
In a project where `electron-vite` is installed, you can use `electron-vite` binary directly with `npx electron-vite` or add the npm scripts to your `package.json` file like this:
```json
{
"scripts": {
"start": "electron-vite preview", // start electron app to preview production build
"dev": "electron-vite dev", // start dev server and electron app
"prebuild": "electron-vite build" // build for production
}
}
```
In order to use the renderer process HMR, you need to use the `environment variables` to determine whether the window browser loads a local html file or a remote URL.
```js
function createWindow() {
// Create the browser window
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js')
}
})
// Load the remote URL for development or the local html file for production
if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}
}
```
**Note**: For development, the renderer process `index.html` file needs to reference your script code via `<script type="module">`.
### Recommended project directory
```shell
├──src
| ├──main
| | ├──index.js
| | └──...
| ├──preload
| | ├──index.js
| | └──...
| ├──renderer
| | ├──src
| | ├──index.html
| | └──...
├──electron.vite.config.js
└──package.json
```
### Get started
Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project.
## Configure
### Config file
When running `electron-vite` from the command line, electron-vite will automatically try to resolve a config file named `electron.vite.config.js` inside project root. The most basic config file looks like this:
```js
// electron.vite.config.js
export default {
main: {
// vite config options
},
preload: {
// vite config options
},
renderer: {
// vite config options
}
}
```
You can also explicitly specify a config file to use with the `--config` CLI option (resolved relative to `cwd`):
```sh
electron-vite --config my-config.js
```
**Tips**: `electron-vite` also supports `ts` or `mjs` config file.
### Config intellisense
Since `electron-vite` ships with TypeScript typings, you can leverage your IDE's intellisense with jsdoc type hints:
```js
/**
* @type {import('electron-vite').UserConfig}
*/
const config = {
// ...
}
export default config
```
Alternatively, you can use the `defineConfig` and `defineViteConfig` helper which should provide intellisense without the need for jsdoc annotations:
```js
import { defineConfig, defineViteConfig } from 'electron-vite'
export default defineConfig({
main: {
// ...
},
preload: {
// ...
},
renderer: defineViteConfig(({ command, mode }) => {
// conditional config use defineViteConfig
// ...
})
})
```
**Tips**: The `defineViteConfig` exports from `Vite`.
### Config reference
See [vitejs.dev](https://vitejs.dev/config)
### Config presets
#### Build options for `main`:
- **outDir**: `out\main`(relative to project root)
- **target**: `node*`, automatically match node target of `Electron`. For example, the node target of Electron 17 is `node16.13`
- **lib.entry**: `src\main\{index|main}.{js|ts|mjs|cjs}`(relative to project root), empty string if not found
- **lib.formats**: `cjs`
- **rollupOptions.external**: `electron` and all builtin modules
#### Build options for `preload`:
- **outDir**: `out\preload`(relative to project root)
- **target**: the same as `main`
- **lib.entry**: `src\preload\{index|preload}.{js|ts|mjs|cjs}`(relative to project root), empty string if not found
- **lib.formats**: `cjs`
- **rollupOptions.external**: the same as `main`
#### Build options for `renderer`:
- **root**: `src\renderer`(relative to project root)
- **outDir**: `out\renderer`(relative to project root)
- **target**: `chrome*`, automatically match chrome target of `Electron`. For example, the chrome target of Electron 17 is `chrome98`
- **lib.entry**: `src\renderer\index.html`(relative to project root), empty string if not found
- **rollupOptions.external**: the same as `main`
#### Define option for `main` and `preload`
In web development, Vite will transform `'process.env.'` to `'({}).'`. This is reasonable and correct. But in nodejs development, we sometimes need to use `process.env`, so `electron-vite` will automatically add config define field to redefine global variable replacements like this:
```js
export default {
main: {
define: {
'process.env': 'process.env'
}
}
}
```
**Note**: If you want to use these configurations in an existing project, please see the Vite plugin [vite-plugin-electron-config](https://github.com/alex8088/vite-plugin-electron-config)
### Config FAQs
#### How do I configure when the Electron app has multiple windows?
When your electron app has multiple windows, it means there are multiple html files or preload files. You can modify your config file like this:
```js
export default {
main: {},
preload: {
build: {
rollupOptions: {
input: {
browser: resolve(__dirname, 'src/preload/browser.ts'),
webview: resolve(__dirname, 'src/preload/webview.ts')
}
}
}
},
renderer: {
build: {
rollupOptions: {
input: {
browser: resolve(__dirname, 'src/renderer/browser.html'),
webview: resolve(__dirname, 'src/renderer/webview.html')
}
}
}
}
}
```
## CLI options
For the full list of CLI options, you can run `npx electron-vite -h` in your project. The flags listed below are only available via the command line interface:
- `--ignoreConfigWarning`: boolean, allow you ignore warning when config missing
- `--outDir`: string, output directory (default: out)
## API
### build
Type Signature:
```js
async function build(inlineConfig: InlineConfig = {}): Promise<void>
```
Example Usage:
```js
const path = require('path')
const { build } = require('electron-vite')
;(async () => {
await build({
build: {
outDir: 'out'
rollupOptions: {
// ...
}
}
})
})()
```
### createServer
Type Signature:
```js
async function createServer(inlineConfig: InlineConfig = {}): Promise<void>
```
Example Usage:
```js
const { createServer } = require('electron-vite')
;(async () => {
await createServer({
server: {
port: 1337
}
})
})()
```
### preview
Type Signature:
```js
async function preview(inlineConfig: InlineConfig = {}): Promise<void>
```
Example Usage:
```js
const { preview } = require('electron-vite')
;(async () => {
await preview({})
})()
```
### InlineConfig
The InlineConfig interface extends Vite [UserConfig](https://vitejs.dev/guide/api-javascript.html#inlineconfig) with additional properties:
- `ignoreConfigWarning`: set to `false` to ignore warning when config missing
And omit `base` property because it is not necessary to set the base public path in Electron.
### resolveConfig
Type Signature:
```js
async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
defaultMode = 'development'
): Promise<ResolvedConfig>
```
## License
[MIT](./LICENSE)

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

30
bin/electron-vite.js Normal file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env node
const debugIndex = process.argv.findIndex(arg => /^(?:-d|--debug)$/.test(arg))
const filterIndex = process.argv.findIndex(arg => /^(?:-f|--filter)$/.test(arg))
if (debugIndex > 0) {
let value = process.argv[debugIndex + 1]
if (!value || value.startsWith('-')) {
value = 'vite:*'
} else {
value = value
.split(',')
.map(v => `vite:${v}`)
.join(',')
}
process.env.DEBUG = `${process.env.DEBUG ? process.env.DEBUG + ',' : ''}${value}`
if (filterIndex > 0) {
const filter = process.argv[filterIndex + 1]
if (filter && !filter.startsWith('-')) {
process.env.VITE_DEBUG_FILTER = filter
}
}
}
function run() {
require('../dist/cli')
}
run()

79
package.json Normal file
View file

@ -0,0 +1,79 @@
{
"name": "electron-vite",
"version": "1.0.0",
"description": "Use vite for your electron app.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"electron-vite": "bin/electron-vite.js"
},
"files": [
"bin",
"dist"
],
"engines": {
"node": ">=12.2.0"
},
"author": "Alex Wei<https://github.com/alex8088>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/alex8088/electron-vite.git"
},
"bugs": {
"url": "https://github.com/alex8088/electron-vite/issues"
},
"homepage": "https://github.com/alex8088/electron-vite#readme",
"keywords": [
"electron",
"vite",
"cli",
"plugin"
],
"scripts": {
"format": "prettier --write .",
"lint": "eslint --ext .ts src/**",
"typecheck": "tsc --noEmit",
"build": "npm run lint && node scripts/build.js"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged",
"commit-msg": "node scripts/verifyCommit.js $1"
},
"lint-staged": {
"*.js": [
"prettier --write"
],
"*.ts?(x)": [
"eslint",
"prettier --parser=typescript --write"
]
},
"peerDependencies": {
"vite": "^2.6.0"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.19.4",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-typescript": "^8.3.0",
"@types/node": "16.11.22",
"@typescript-eslint/eslint-plugin": "^5.11.0",
"@typescript-eslint/parser": "^5.11.0",
"eslint": "^8.7.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"fs-extra": "^10.0.0",
"lint-staged": "^12.3.6",
"prettier": "^2.5.1",
"rollup": "^2.64.0",
"simple-git-hooks": "^2.7.0",
"tslib": "^2.3.1",
"typescript": "^4.5.5",
"vite": "^2.8.0"
},
"dependencies": {
"cac": "^6.7.12",
"esbuild": "^0.14.21",
"picocolors": "^1.0.0"
}
}

1888
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

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

22
scripts/verifyCommit.js Normal file
View file

@ -0,0 +1,22 @@
// Invoked on the commit-msg git hook by simple-git-hooks.
const colors = require('picocolors')
const fs = require('fs')
const msgPath = process.argv[2]
const msg = fs.readFileSync(msgPath, 'utf-8').trim()
const commitRE =
/^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?: .{1,50}/
if (!commitRE.test(msg)) {
console.log()
console.error(
` ${colors.bgRed.white(' ERROR ')} ${colors.red(`invalid commit message format.`)}\n\n` +
colors.red(` Proper commit message format is required for automated changelog generation. Examples:\n\n`) +
` ${colors.green(`feat: add 'comments' option`)}\n` +
` ${colors.green(`fix: handle events on blur (close #28)`)}\n\n` +
colors.red(` See .github/commit-convention.md for more details.\n`)
)
process.exit(1)
}

23
src/build.ts Normal file
View file

@ -0,0 +1,23 @@
import { build as viteBuild } from 'vite'
import { InlineConfig, resolveConfig } from './config'
/**
* Bundles the electron app for production.
*/
export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
const config = await resolveConfig(inlineConfig, 'build', 'production')
if (config.config) {
const mainViteConfig = config.config?.main
if (mainViteConfig) {
await viteBuild(mainViteConfig)
}
const preloadViteConfig = config.config?.preload
if (preloadViteConfig) {
await viteBuild(preloadViteConfig)
}
const rendererViteConfig = config.config?.renderer
if (rendererViteConfig) {
await viteBuild(rendererViteConfig)
}
}
}

104
src/cli.ts Normal file
View file

@ -0,0 +1,104 @@
import { cac } from 'cac'
import colors from 'picocolors'
import { LogLevel, createLogger } from 'vite'
import { InlineConfig } from './config'
const cli = cac('electron-vite')
// global options
interface GlobalCLIOptions {
'--'?: string[]
c?: boolean | string
config?: string
l?: LogLevel
logLevel?: LogLevel
clearScreen?: boolean
d?: boolean | string
debug?: boolean | string
f?: string
filter?: string
m?: string
mode?: string
ignoreConfigWarning?: boolean
outDir?: string
}
function createInlineConfig(root: string, options: GlobalCLIOptions): InlineConfig {
return {
root,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
ignoreConfigWarning: options.ignoreConfigWarning,
build: {
outDir: options.outDir
}
}
}
cli
.option('-c, --config <file>', `[string] use specified config file`)
.option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
.option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
.option('-d, --debug [feat]', `[string | boolean] show debug logs`)
.option('-f, --filter <filter>', `[string] filter debug logs`)
.option('-m, --mode <mode>', `[string] set env mode`)
.option('--ignoreConfigWarning', `[boolean] ignore config warning`)
.option('--outDir <dir>', `[string] output directory (default: out)`)
// dev
cli
.command('[root]', 'start dev server and electron app')
.alias('serve')
.alias('dev')
.action(async (root: string, options: GlobalCLIOptions) => {
const { createServer } = await import('./server')
const inlineConfig = createInlineConfig(root, options)
try {
await createServer(inlineConfig)
} catch (e) {
const error = e as Error
createLogger(options.logLevel).error(
colors.red(`error during start dev server and electron app:\n${error.stack}`),
{ error }
)
process.exit(1)
}
})
// build
cli.command('build [root]', 'build for production').action(async (root: string, options: GlobalCLIOptions) => {
const { build } = await import('./build')
const inlineConfig = createInlineConfig(root, options)
try {
await build(inlineConfig)
} catch (e) {
const error = e as Error
createLogger(options.logLevel).error(colors.red(`error during build:\n${error.stack}`), { error })
process.exit(1)
}
})
// preview
cli
.command('preview [root]', 'start electron app to preview production build')
.action(async (root: string, options: GlobalCLIOptions) => {
const { preview } = await import('./preview')
const inlineConfig = createInlineConfig(root, options)
try {
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 })
process.exit(1)
}
})
cli.help()
cli.version(require('../package.json').version)
cli.parse()

340
src/config.ts Normal file
View file

@ -0,0 +1,340 @@
import * as path from 'path'
import * as fs from 'fs'
import colors from 'picocolors'
import { UserConfig as ViteConfig, ConfigEnv, Plugin, LogLevel, createLogger, mergeConfig, normalizePath } from 'vite'
import { build } from 'esbuild'
import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugin'
import { isObject, dynamicImport } from './utils'
export { defineConfig as defineViteConfig } from 'vite'
export interface UserConfig {
/**
* Vite config options for electron main process
*
* https://cn.vitejs.dev/config/
*/
main?: ViteConfig
/**
* Vite config options for electron renderer process
*
* https://cn.vitejs.dev/config/
*/
renderer?: ViteConfig
/**
* Vite config options for electron preload files
*
* https://cn.vitejs.dev/config/
*/
preload?: ViteConfig
}
export type InlineConfig = Omit<ViteConfig, 'base'> & {
configFile?: string | false
envFile?: false
ignoreConfigWarning?: boolean
}
export type UserConfigExport = UserConfig | Promise<UserConfig>
/**
* Type helper to make it easier to use `electron.vite.config.ts`
* accepts a direct {@link UserConfig} object, or a function that returns it.
*/
export function defineConfig(config: UserConfigExport): UserConfigExport {
return config
}
export interface ResolvedConfig {
config?: UserConfig
configFile?: string
configFileDependencies: string[]
}
export async function resolveConfig(
inlineConfig: InlineConfig,
command: 'build' | 'serve',
defaultMode = 'development'
): Promise<ResolvedConfig> {
const config = inlineConfig
const mode = inlineConfig.mode || defaultMode
config.mode = mode
if (mode === 'production') {
process.env.NODE_ENV = 'production'
}
let userConfig: UserConfig | undefined
let configFileDependencies: string[] = []
let { configFile } = config
if (configFile !== false) {
const configEnv = {
mode,
command
}
const loadResult = await loadConfigFromFile(
configEnv,
configFile,
config.root,
config.logLevel,
config.ignoreConfigWarning
)
if (loadResult) {
const root = config.root
delete config.root
delete config.configFile
const outDir = config.build?.outDir
if (loadResult.config.main) {
const mainViteConfig: ViteConfig = mergeConfig(loadResult.config.main, deepClone(config))
if (outDir) {
resetOutDir(mainViteConfig, outDir, 'main')
}
mergePlugins(mainViteConfig, electronMainVitePlugin({ root }))
loadResult.config.main = mainViteConfig
}
if (loadResult.config.preload) {
const preloadViteConfig: ViteConfig = mergeConfig(loadResult.config.preload, deepClone(config))
if (outDir) {
resetOutDir(preloadViteConfig, outDir, 'preload')
}
mergePlugins(preloadViteConfig, electronPreloadVitePlugin({ root }))
loadResult.config.preload = preloadViteConfig
}
if (loadResult.config.renderer) {
const rendererViteConfig: ViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config))
if (outDir) {
resetOutDir(rendererViteConfig, outDir, 'renderer')
}
mergePlugins(rendererViteConfig, electronRendererVitePlugin({ root }))
loadResult.config.renderer = rendererViteConfig
}
userConfig = loadResult.config
configFile = loadResult.path
configFileDependencies = loadResult.dependencies
}
}
const resolved: ResolvedConfig = {
config: userConfig,
configFile: configFile ? normalizePath(configFile) : undefined,
configFileDependencies
}
return resolved
}
function deepClone<T>(data: T): T {
return JSON.parse(JSON.stringify(data))
}
function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void {
let userOutDir = config.build?.outDir
if (outDir === userOutDir) {
userOutDir = path.resolve(config.root || process.cwd(), outDir, subOutDir)
if (config.build) {
config.build.outDir = userOutDir
} else {
config.build = { outDir: userOutDir }
}
}
}
function mergePlugins(config: ViteConfig, plugins: Plugin[]): void {
const userPlugins = config.plugins || []
config.plugins = userPlugins.concat(plugins)
}
const CONFIG_FILE_NAME = 'electron.vite.config'
export async function loadConfigFromFile(
configEnv: ConfigEnv,
configFile?: string,
configRoot: string = process.cwd(),
logLevel?: LogLevel,
ignoreConfigWarning = false
): Promise<{
path: string
config: UserConfig
dependencies: string[]
}> {
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}.`)
}
resolvedPath = configFile ? path.resolve(configFile) : findConfigFile(configRoot, ['js', 'ts', 'mjs', 'cjs'])
if (!resolvedPath) {
return {
path: '',
config: { main: {}, preload: {}, renderer: {} },
dependencies: []
}
}
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)
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`)
}
const configRequired: string[] = []
let mainConfig
if (config.main) {
const mainViteConfig = config.main
mainConfig = await (typeof mainViteConfig === 'function' ? mainViteConfig(configEnv) : mainViteConfig)
if (!isObject(mainConfig)) {
throw new Error(`main config must export or return an object`)
}
} else {
configRequired.push('main')
}
let rendererConfig
if (config.renderer) {
const rendererViteConfig = config.renderer
rendererConfig = await (typeof rendererViteConfig === 'function'
? rendererViteConfig(configEnv)
: rendererViteConfig)
if (!isObject(rendererConfig)) {
throw new Error(`renderer config must export or return an object`)
}
} else {
configRequired.push('renderer')
}
let preloadConfig
if (config.preload) {
const preloadViteConfig = config.preload
preloadConfig = await (typeof preloadViteConfig === 'function' ? preloadViteConfig(configEnv) : preloadViteConfig)
if (!isObject(preloadViteConfig)) {
throw new Error(`preload config must export or return an object`)
}
} else {
configRequired.push('preload')
}
if (!ignoreConfigWarning && configRequired.length > 0) {
createLogger(logLevel).warn(colors.yellow(`${configRequired.join(' and ')} config is missing`))
}
return {
path: normalizePath(configFilePath),
config: {
main: mainConfig,
renderer: rendererConfig,
preload: preloadConfig
},
dependencies: bundled.dependencies
}
} catch (e) {
createLogger(logLevel).error(colors.red(`failed to load config from ${configFilePath}`), { error: e as Error })
throw e
}
}
function findConfigFile(configRoot: string, extensions: string[]): string {
for (const ext of extensions) {
const configFile = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${ext}`)
if (fs.existsSync(configFile)) {
return configFile
}
}
return ''
}
async function bundleConfigFile(fileName: string): Promise<{ code: string; dependencies: string[] }> {
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: [fileName],
write: false,
platform: 'node',
bundle: true,
format: 'esm',
sourcemap: false,
metafile: true,
plugins: [
{
name: 'externalize-deps',
setup(build): void {
build.onResolve({ filter: /.*/ }, args => {
const id = args.path
if (id[0] !== '.' && !path.isAbsolute(id)) {
return {
external: true
}
}
return null
})
}
},
{
name: 'replace-import-meta',
setup(build): void {
build.onLoad({ filter: /\.[jt]s$/ }, async args => {
const contents = await fs.promises.readFile(args.path, 'utf8')
return {
loader: args.path.endsWith('.ts') ? 'ts' : 'js',
contents: contents
.replace(/\bimport\.meta\.url\b/g, JSON.stringify(`file://${args.path}`))
.replace(/\b__dirname\b/g, JSON.stringify(path.dirname(args.path)))
.replace(/\b__filename\b/g, JSON.stringify(args.path))
}
})
}
}
]
})
const { text } = result.outputFiles[0]
return {
code: text,
dependencies: result.metafile ? Object.keys(result.metafile.inputs) : []
}
}

6
src/index.ts Normal file
View file

@ -0,0 +1,6 @@
export { LogLevel, createLogger } from 'vite'
export * from './config'
export { createServer } from './server'
export { build } from './build'
export { preview } from './preview'
export * from './plugin'

350
src/plugin.ts Normal file
View file

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

27
src/preview.ts Normal file
View file

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

70
src/server.ts Normal file
View file

@ -0,0 +1,70 @@
import { spawn } from 'child_process'
import { createServer as ViteCreateServer, build as viteBuild, createLogger } from 'vite'
import colors from 'picocolors'
import { InlineConfig, resolveConfig } from './config'
import { ensureElectronEntryFile, getElectronPath } from './utils'
export async function createServer(inlineConfig: InlineConfig = {}): Promise<void> {
const config = await resolveConfig(inlineConfig, 'serve', 'development')
if (config.config) {
const rendererViteConfig = config.config?.renderer
if (rendererViteConfig) {
const server = await ViteCreateServer(rendererViteConfig)
if (!server.httpServer) {
throw new Error('HTTP server not available')
}
await server.listen()
const conf = server.config.server
const protocol = conf.https ? 'https:' : 'http:'
const host = conf.host || 'localhost'
const port = conf.port
process.env.ELECTRON_RENDERER_URL = `${protocol}//${host}:${port}`
const logger = server.config.logger
logger.info(colors.green(`dev server running for the electron renderer process at:\n`), {
clear: !logger.hasWarned
})
server.printUrls()
}
const logger = createLogger(inlineConfig.logLevel)
const mainViteConfig = config.config?.main
if (mainViteConfig) {
logger.info(colors.gray(`\n-----\n`))
await viteBuild(mainViteConfig)
logger.info(colors.green(`\nbuild the electron main process successfully`))
}
const preloadViteConfig = config.config?.preload
if (preloadViteConfig) {
logger.info(colors.gray(`\n-----\n`))
await viteBuild(preloadViteConfig)
logger.info(colors.green(`\nbuild the electron preload files successfully`))
}
ensureElectronEntryFile(inlineConfig.root)
const electronPath = getElectronPath()
const ps = spawn(electronPath, ['.'])
ps.stdout.on('data', chunk => {
chunk.toString().trim() && logger.info(chunk.toString())
})
ps.stderr.on('data', chunk => {
chunk.toString().trim() && logger.error(chunk.toString())
})
ps.on('close', process.exit)
logger.info(colors.green(`\nstart electron app...`))
}
}

39
src/utils.ts Normal file
View file

@ -0,0 +1,39 @@
import * as path from 'path'
import * as fs from 'fs'
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 function ensureElectronEntryFile(root = process.cwd()): void {
const pkg = path.join(root, 'package.json')
if (fs.existsSync(pkg)) {
const main = require(pkg).main
if (!main) {
throw new Error('not found an entry point to electorn app, please add main field for your package.json')
} else {
const entryPath = path.resolve(root, main)
if (!fs.existsSync(entryPath)) {
throw new Error(`not found the electorn app entry file: ${entryPath}`)
}
}
} else {
throw new Error('no package.json')
}
}
export function getElectronPath(): string {
const electronModulePath = path.resolve(process.cwd(), 'node_modules', 'electron')
const pathFile = path.join(electronModulePath, 'path.txt')
let executablePath
if (fs.existsSync(pathFile)) {
executablePath = fs.readFileSync(pathFile, 'utf-8')
}
if (executablePath) {
return path.join(electronModulePath, 'dist', executablePath)
} else {
throw new Error('Electron uninstall')
}
}

23
tsconfig.json Normal file
View file

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