├── pnpm-workspace.yaml ├── src ├── types │ ├── index.ts │ └── mainCommand.ts ├── utils │ ├── repestString.ts │ ├── delay.ts │ ├── readJson.ts │ ├── index.ts │ ├── fs.ts │ ├── viteLoggerPlugin.ts │ ├── childProcess.ts │ ├── removeJunk.ts │ └── findPathsOrExit.ts ├── commands │ ├── index.ts │ ├── clean.ts │ ├── build.ts │ ├── runElectron.ts │ ├── runVite.ts │ ├── run.ts │ └── esbuild.ts ├── common │ ├── index.ts │ ├── notFound.ts │ ├── prompt.ts │ ├── defaultViteConfig.ts │ ├── diagnose.ts │ ├── compileError.ts │ ├── loggerMeta.ts │ ├── defaultTsConfig.ts │ └── pathManager.ts └── index.ts ├── bin └── elecrun.js ├── .prettierignore ├── docs ├── images │ └── screen-shot.webp ├── index.html ├── zh.html ├── zh.html.md └── README.md ├── fixtures ├── demo │ ├── src │ │ ├── main │ │ │ ├── add.ts │ │ │ ├── tsconfig.json │ │ │ ├── preload.ts │ │ │ └── index.ts │ │ └── renderer │ │ │ ├── app.ts │ │ │ ├── tsconfig.json │ │ │ └── index.html │ ├── vite.config.ts │ ├── package.json │ └── tsconfig.json ├── esm-demo │ ├── src │ │ ├── main │ │ │ ├── add.ts │ │ │ ├── tsconfig.json │ │ │ ├── preload.ts │ │ │ └── index.ts │ │ └── renderer │ │ │ ├── app.ts │ │ │ ├── tsconfig.json │ │ │ └── index.html │ ├── vite.config.ts │ ├── package.json │ └── tsconfig.json ├── exit-sig │ ├── index.html │ ├── package.json │ └── index.js ├── simple │ ├── index.html │ ├── index.ts │ └── package.json └── js-simple │ ├── index.html │ ├── index.js │ └── package.json ├── tsup.config.ts ├── vitest.config.ts ├── tests ├── utils │ ├── repeatString.test.ts │ └── removeJunk.test.ts └── commands │ └── runElectron.test.ts ├── .gitignore ├── LICENSE ├── .github └── workflows │ ├── static.yml │ ├── CI.yml │ └── publish.yml ├── tsconfig.json ├── package.json ├── eslint.config.mjs ├── CLAUDE.md ├── CONTRIBUTE.md ├── CHANGELOG.md ├── README_CN.md └── README.md /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - '.' 3 | - 'fixtures/*' -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export { MainCommand } from './mainCommand'; 2 | -------------------------------------------------------------------------------- /bin/elecrun.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../build/index.js"); -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /docs/images/screen-shot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jctaoo/elecrun/HEAD/docs/images/screen-shot.webp -------------------------------------------------------------------------------- /fixtures/demo/src/main/add.ts: -------------------------------------------------------------------------------- 1 | export function add(lhs: number, rhs: number) { 2 | return lhs + rhs; 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/esm-demo/src/main/add.ts: -------------------------------------------------------------------------------- 1 | export function add(lhs: number, rhs: number) { 2 | return lhs + rhs; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/repestString.ts: -------------------------------------------------------------------------------- 1 | export function repeatString(char: string, len: number): string { 2 | return Array(len).fill(char).join(''); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/delay.ts: -------------------------------------------------------------------------------- 1 | export function delay(duration: number): Promise { 2 | return new Promise((r) => { 3 | setTimeout(() => { 4 | r(); 5 | }, duration); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './esbuild'; 2 | export * from './runElectron'; 3 | export * from './runVite'; 4 | export * from './run'; 5 | export * from './build'; 6 | export * from './clean'; 7 | -------------------------------------------------------------------------------- /src/utils/readJson.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | export async function readJson(path: fs.PathLike) { 4 | const content = await fs.promises.readFile(path, 'utf-8'); 5 | return JSON.parse(content); 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/exit-sig/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron App 6 | 7 | 8 |

Hello World

9 | 10 | 11 | -------------------------------------------------------------------------------- /fixtures/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron App 6 | 7 | 8 |

Hello World

9 | 10 | 11 | -------------------------------------------------------------------------------- /fixtures/js-simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron App 6 | 7 | 8 |

Hello World

9 | 10 | 11 | -------------------------------------------------------------------------------- /fixtures/demo/src/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../app" 5 | }, 6 | "include": ["../main/**/*", "../common/**/*"], 7 | "exclude": ["../renderer/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/esm-demo/src/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../app" 5 | }, 6 | "include": ["../main/**/*", "../common/**/*"], 7 | "exclude": ["../renderer/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compileError'; 2 | export * from './pathManager'; 3 | export * from './diagnose'; 4 | export * from './loggerMeta'; 5 | export * from './notFound'; 6 | export * from './defaultTsConfig'; 7 | export * from './defaultViteConfig'; 8 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | bundle: true, 6 | outDir: "build", 7 | splitting: false, 8 | sourcemap: true, 9 | format: "cjs", 10 | clean: true, 11 | }) -------------------------------------------------------------------------------- /fixtures/simple/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | 3 | function createWindow() { 4 | const win = new BrowserWindow({ 5 | width: 800, 6 | height: 600 7 | }); 8 | 9 | win.loadURL('http://localhost:5173'); 10 | } 11 | 12 | app.whenReady().then(createWindow); 13 | -------------------------------------------------------------------------------- /fixtures/js-simple/index.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | 3 | function createWindow() { 4 | const win = new BrowserWindow({ 5 | width: 800, 6 | height: 600 7 | }); 8 | 9 | win.loadURL('http://localhost:5173'); 10 | } 11 | 12 | app.whenReady().then(createWindow); 13 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { repeatString } from './repestString'; 2 | export { LoggerPlugin } from './viteLoggerPlugin'; 3 | export { removeJunkTransformOptions } from './removeJunk'; 4 | export { delay } from './delay'; 5 | export * from './fs'; 6 | export * from './readJson'; 7 | export * from './childProcess'; 8 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | pool: "vmForks", 6 | coverage: { 7 | enabled: true, 8 | provider: "v8", 9 | reporter: ["html"], 10 | reportsDirectory: "coverage", 11 | } 12 | }, 13 | }) -------------------------------------------------------------------------------- /fixtures/demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | const rendererPath = "./src/renderer" 4 | const outDirRenderer = "./build" 5 | 6 | export default defineConfig({ 7 | base: "./", 8 | root: rendererPath, 9 | build: { 10 | outDir: outDirRenderer, 11 | emptyOutDir: true, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /fixtures/esm-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | const rendererPath = "./src/renderer" 4 | const outDirRenderer = "./build" 5 | 6 | export default defineConfig({ 7 | base: "./", 8 | root: rendererPath, 9 | build: { 10 | outDir: outDirRenderer, 11 | emptyOutDir: true, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /fixtures/demo/src/renderer/app.ts: -------------------------------------------------------------------------------- 1 | window.onload = () => { 2 | let count = 0; 3 | 4 | const counterLabel = document.querySelector(".label")!; 5 | const btn = document.querySelector(".add-btn")!; 6 | 7 | btn.addEventListener('click', () => { 8 | count += 1; 9 | counterLabel.textContent = `updated ${count}`; 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /fixtures/esm-demo/src/renderer/app.ts: -------------------------------------------------------------------------------- 1 | window.onload = () => { 2 | let count = 0; 3 | 4 | const counterLabel = document.querySelector(".label")!; 5 | const btn = document.querySelector(".add-btn")!; 6 | 7 | btn.addEventListener('click', () => { 8 | count += 1; 9 | counterLabel.textContent = `updated ${count}`; 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /fixtures/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "elecrun --vite", 8 | "build": "elecrun build", 9 | "clean": "elecrun clean" 10 | }, 11 | "dependencies": { 12 | "electron": "37.3.1" 13 | }, 14 | "devDependencies": { 15 | "elecrun": "workspace:*" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fixtures/exit-sig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-simple", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "elecrun --vite", 8 | "build": "elecrun build", 9 | "clean": "elecrun clean" 10 | }, 11 | "dependencies": { 12 | "electron": "37.3.1" 13 | }, 14 | "devDependencies": { 15 | "elecrun": "workspace:*" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fixtures/js-simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exit-sig", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "elecrun --vite", 8 | "build": "elecrun build", 9 | "clean": "elecrun clean" 10 | }, 11 | "dependencies": { 12 | "electron": "37.3.1" 13 | }, 14 | "devDependencies": { 15 | "elecrun": "workspace:*" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/utils/repeatString.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest'; 2 | 3 | import { repeatString } from '../../src/utils'; 4 | 5 | it('test repeat-string', () => { 6 | expect(repeatString("ab", 3)).toBe("ababab"); 7 | expect(repeatString("~", 3)).toBe("~~~"); 8 | expect(repeatString("b", 2)).toBe("bb"); 9 | expect(repeatString(" ", 3)).toBe(" "); 10 | expect(repeatString('a', 3)).not.toBe("ava"); 11 | }); 12 | 13 | -------------------------------------------------------------------------------- /fixtures/demo/src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import { 2 | contextBridge, 3 | ipcRenderer, 4 | IpcRendererEvent, 5 | } from "electron"; 6 | 7 | contextBridge.exposeInMainWorld("bridge", { 8 | sendIpc: ipcRenderer.send, 9 | listenIpc: ( 10 | channel: string, 11 | callBack: (event: IpcRendererEvent, arg: any) => void 12 | ) => { 13 | ipcRenderer.on(channel, (event, arg) => { 14 | callBack(event, arg); 15 | }); 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /fixtures/demo/src/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "lib": [ 7 | "DOM", 8 | "DOM.Iterable", 9 | "ESNext" 10 | ], 11 | "types": [ 12 | "vite/client" 13 | ] 14 | }, 15 | "include": [ 16 | "../renderer/**/*", 17 | "../common/**/*" 18 | ], 19 | "exclude": [ 20 | "../main/**/*" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /fixtures/esm-demo/src/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "lib": [ 7 | "DOM", 8 | "DOM.Iterable", 9 | "ESNext" 10 | ], 11 | "types": [ 12 | "vite/client" 13 | ] 14 | }, 15 | "include": [ 16 | "../renderer/**/*", 17 | "../common/**/*" 18 | ], 19 | "exclude": [ 20 | "../main/**/*" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /fixtures/esm-demo/src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import { 2 | contextBridge, 3 | ipcRenderer, 4 | IpcRendererEvent, 5 | } from "electron"; 6 | 7 | contextBridge.exposeInMainWorld("bridge", { 8 | sendIpc: ipcRenderer.send, 9 | listenIpc: ( 10 | channel: string, 11 | callBack: (event: IpcRendererEvent, arg: any) => void 12 | ) => { 13 | ipcRenderer.on(channel, (event, arg) => { 14 | callBack(event, arg); 15 | }); 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /fixtures/exit-sig/index.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | 3 | function createWindow() { 4 | const win = new BrowserWindow({ 5 | width: 800, 6 | height: 600 7 | }); 8 | 9 | win.loadURL('http://localhost:5173'); 10 | } 11 | 12 | // listen to exit signal 13 | process.on('exit', () => { 14 | console.log('exit signal received'); 15 | }); 16 | 17 | app.on("before-quit", () => { 18 | console.log("before-quit signal received"); 19 | }); 20 | 21 | 22 | app.whenReady().then(createWindow); 23 | -------------------------------------------------------------------------------- /fixtures/demo/src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | Title 8 | 9 | 10 |
11 |

Hello World

12 |
0
13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /fixtures/esm-demo/src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | Title 8 | 9 | 10 |
11 |

Hello World

12 |
0
13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | export async function exists(path: fs.PathLike): Promise { 5 | try { 6 | await fs.promises.stat(path); 7 | return true; 8 | } catch { 9 | return false; 10 | } 11 | } 12 | 13 | export async function* walk(dir: string): AsyncGenerator { 14 | for await (const d of await fs.promises.opendir(dir)) { 15 | const entry = path.join(dir, d.name); 16 | if (d.isDirectory()) yield* walk(entry); 17 | else if (d.isFile()) yield entry; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/types/mainCommand.ts: -------------------------------------------------------------------------------- 1 | import { CompileError } from '../common'; 2 | 3 | export type MainCommand = ( 4 | options: { 5 | isBuild: boolean; 6 | outDir: string; 7 | preloadScript?: string; 8 | entryPath: string; 9 | esbuildConfigFile?: string /** Config js file to use with esbuild */; 10 | format?: 'cjs' | 'esm'; 11 | }, 12 | reportError: (...errs: CompileError[]) => void, 13 | buildStart: () => void, 14 | buildComplete: (dir: string, count: number) => void, 15 | notFoundTSConfig: () => Promise, 16 | ) => Promise; 17 | -------------------------------------------------------------------------------- /fixtures/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "elecrun": "elecrun", 9 | "dev": "elecrun --vite --preload preload.ts --esm", 10 | "build": "elecrun build", 11 | "clean": "elecrun clean" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "24.3.0", 15 | "vite": "7.1.3", 16 | "esbuild": "^0.14.11" 17 | }, 18 | "dependencies": { 19 | "elecrun": "workspace:*", 20 | "electron": "37.3.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /fixtures/esm-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm-demo", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "elecrun": "elecrun", 9 | "dev": "elecrun --vite --preload preload.ts --esm", 10 | "build": "elecrun build --preload preload.ts --esm", 11 | "clean": "elecrun clean" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "24.3.0", 15 | "vite": "7.1.3" 16 | }, 17 | "dependencies": { 18 | "electron": "37.3.1", 19 | "elecrun": "workspace:*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/viteLoggerPlugin.ts: -------------------------------------------------------------------------------- 1 | import { gray, yellow } from 'colorette'; 2 | import type { Plugin } from 'vite'; 3 | 4 | import { consoleViteMessagePrefix, PathManager } from '../common'; 5 | 6 | // TODO 打印 vite 错误 7 | export function LoggerPlugin(): Plugin { 8 | return { 9 | name: 'electron-scripts-logger', 10 | handleHotUpdate: (ctx) => { 11 | for (const file of ctx.modules) { 12 | if (!file.file) continue; 13 | const path = file.file.replace(PathManager.shard.srcPath, ''); 14 | console.log( 15 | yellow(consoleViteMessagePrefix), 16 | yellow('hmr update'), 17 | gray(path), 18 | ); 19 | } 20 | return ctx.modules; 21 | }, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/common/notFound.ts: -------------------------------------------------------------------------------- 1 | import { red } from 'colorette'; 2 | 3 | import { 4 | cannotFoundESBuildConfigMessage, 5 | cannotFoundPackageJsonMessage, 6 | cannotFoundTSConfigMessage, 7 | cannotFoundViteConfigMessage, 8 | } from './loggerMeta'; 9 | 10 | export function notFoundTSConfig(writePath: string) { 11 | console.warn(cannotFoundTSConfigMessage(writePath)); 12 | } 13 | 14 | export function notFoundViteConfig(writePath: string) { 15 | console.warn(cannotFoundViteConfigMessage(writePath)); 16 | } 17 | 18 | export function notFoundPackageJson() { 19 | console.error(red(cannotFoundPackageJsonMessage)); 20 | process.exit(); 21 | } 22 | 23 | export function notFoundESBuildConfig() { 24 | console.warn(cannotFoundESBuildConfigMessage); 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/childProcess.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from 'child_process'; 2 | 3 | function delay(ms: number) { 4 | return new Promise((resolve) => setTimeout(resolve, ms)); 5 | } 6 | 7 | // 优雅终止子进程(先 SIGTERM,再等,最后 SIGKILL) 8 | export async function terminateChild(cp: ChildProcess, { waitMs = 5000 } = {}) { 9 | if (!cp || cp.killed) return; 10 | try { 11 | cp.kill('SIGTERM'); 12 | } catch { 13 | // ignore 14 | } 15 | const exited = new Promise((resolve) => { 16 | const done = () => resolve(); 17 | cp.once('exit', done); 18 | cp.once('close', done); 19 | }); 20 | const timed = Promise.race([exited, delay(waitMs)]); 21 | await timed; 22 | if (!cp.killed) { 23 | try { 24 | cp.kill('SIGKILL'); 25 | } catch { 26 | // ignore 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | .nyc_output 3 | build 4 | node_modules 5 | test 6 | src/**.js 7 | coverage 8 | *.log 9 | package-lock.json 10 | docs/dist 11 | 12 | .vscode/settings.json 13 | 14 | # ---------- Universal (any depth) ---------- 15 | # Build / caches anywhere 16 | **/dist/ 17 | **/build/ 18 | **/node_modules/ 19 | **/.cache/ 20 | **/.parcel-cache/ 21 | **/.turbo/ 22 | 23 | # Yarn Berry (v2+), at any depth 24 | **/.yarn/* 25 | !**/.yarn/patches 26 | !**/.yarn/plugins 27 | !**/.yarn/releases 28 | !**/.yarn/sdks 29 | !**/.yarn/versions 30 | 31 | # Keep Yarn config files anywhere 32 | !**/.yarnrc.yml 33 | 34 | # Ignore cache by default (turn this OFF if you use zero-installs) 35 | **/.yarn/cache/ 36 | 37 | # Yarn PnP files should be committed (don’t ignore) 38 | !.pnp.cjs 39 | !.pnp.loader.mjs 40 | 41 | # claude code 42 | .claude/* -------------------------------------------------------------------------------- /fixtures/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "bundler", 5 | "moduleDetection": "force", 6 | "target": "ESNext", 7 | "noImplicitAny": true, 8 | "removeComments": true, 9 | "preserveConstEnums": true, 10 | "allowJs": true, 11 | "checkJs": true, 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "strictBindCallApply": true, 17 | "noImplicitThis": true, 18 | "noImplicitReturns": true, 19 | "experimentalDecorators": true, 20 | "allowSyntheticDefaultImports": true, 21 | "esModuleInterop": true, 22 | "importHelpers": true, 23 | "sourceMap": true, 24 | "baseUrl": "./src" 25 | }, 26 | "exclude": ["node_modules", "app", "dist"] 27 | } 28 | -------------------------------------------------------------------------------- /fixtures/esm-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "bundler", 5 | "moduleDetection": "force", 6 | "target": "ESNext", 7 | "noImplicitAny": true, 8 | "removeComments": true, 9 | "preserveConstEnums": true, 10 | "allowJs": true, 11 | "checkJs": true, 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "strictBindCallApply": true, 17 | "noImplicitThis": true, 18 | "noImplicitReturns": true, 19 | "experimentalDecorators": true, 20 | "allowSyntheticDefaultImports": true, 21 | "esModuleInterop": true, 22 | "importHelpers": true, 23 | "sourceMap": true, 24 | "baseUrl": "./src" 25 | }, 26 | "exclude": ["node_modules", "app", "dist"] 27 | } 28 | -------------------------------------------------------------------------------- /src/common/prompt.ts: -------------------------------------------------------------------------------- 1 | import readline from 'readline'; 2 | 3 | import { green } from 'colorette'; 4 | 5 | export function prompt(question: string): [() => Promise, () => void] { 6 | const input = process.stdin; 7 | const output = process.stdout; 8 | 9 | const rl = readline.createInterface({ 10 | input, 11 | output, 12 | }); 13 | 14 | const questionAndPrompt = `${green('?')} ${question} (Y/n) `; 15 | 16 | let answerResolve: (answer: boolean) => void = () => {}; 17 | const answerPromise = new Promise((r) => { 18 | answerResolve = r; 19 | }); 20 | 21 | rl.question(questionAndPrompt, (answer) => { 22 | answerResolve(answer === 'Y' || answer == 'y'); 23 | rl.close(); 24 | }); 25 | 26 | return [ 27 | () => answerPromise, 28 | () => { 29 | console.log(''); 30 | rl.close(); 31 | }, 32 | ]; 33 | } 34 | -------------------------------------------------------------------------------- /fixtures/demo/src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import path from 'path'; 3 | import { add } from './add'; 4 | 5 | function createWindow() { 6 | const win = new BrowserWindow({ 7 | width: 800, 8 | height: 600, 9 | webPreferences: { 10 | preload: path.join(import.meta.dirname, 'preload.cjs'), 11 | nodeIntegration: false, 12 | contextIsolation: true, 13 | }, 14 | }); 15 | 16 | console.log('1 + 1 =', add(1, 1)); 17 | win.loadURL('http://localhost:5173').then(); 18 | win.webContents.openDevTools({ mode: 'detach' }); 19 | } 20 | 21 | app.whenReady().then(() => { 22 | createWindow(); 23 | 24 | app.on('activate', () => { 25 | if (BrowserWindow.getAllWindows().length === 0) { 26 | createWindow(); 27 | } 28 | }); 29 | }); 30 | 31 | app.on('window-all-closed', () => { 32 | if (process.platform !== 'darwin') { 33 | app.quit(); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /fixtures/esm-demo/src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import path from 'path'; 3 | import { add } from './add'; 4 | 5 | function createWindow() { 6 | const win = new BrowserWindow({ 7 | width: 800, 8 | height: 600, 9 | webPreferences: { 10 | preload: path.join(import.meta.dirname, 'preload.cjs'), 11 | nodeIntegration: false, 12 | contextIsolation: true, 13 | }, 14 | }); 15 | 16 | console.log('1 + 1 =', add(1, 1)); 17 | win.loadURL('http://localhost:5173').then(); 18 | win.webContents.openDevTools({ mode: 'detach' }); 19 | } 20 | 21 | app.whenReady().then(() => { 22 | createWindow(); 23 | 24 | app.on('activate', () => { 25 | if (BrowserWindow.getAllWindows().length === 0) { 26 | createWindow(); 27 | } 28 | }); 29 | }); 30 | 31 | app.on('window-all-closed', () => { 32 | if (process.platform !== 'darwin') { 33 | app.quit(); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /src/common/defaultViteConfig.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import { PathManager } from './pathManager'; 5 | 6 | export const defaultViteConfig = (root: string): string => ` 7 | import { defineConfig } from "vite"; 8 | 9 | const rendererPath = ${JSON.stringify(root)}; 10 | const outDirRenderer = "./build"; 11 | 12 | export default defineConfig({ 13 | base: "./", 14 | root: rendererPath, 15 | build: { 16 | outDir: outDirRenderer, 17 | emptyOutDir: true, 18 | }, 19 | }); 20 | `; 21 | 22 | export const writeDefaultViteConfig = async (root: string): Promise => { 23 | await fs.promises.mkdir(PathManager.shard.defaultViteConfigDir, { 24 | recursive: true, 25 | }); 26 | const filePath = path.join( 27 | PathManager.shard.defaultViteConfigDir, 28 | 'vite.config.ts', 29 | ); 30 | await fs.promises.writeFile(filePath, defaultViteConfig(root)); 31 | return filePath; 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/removeJunk.ts: -------------------------------------------------------------------------------- 1 | import stream from 'stream'; 2 | 3 | export const removeJunkTransformOptions: stream.TransformOptions = { 4 | decodeStrings: false, 5 | transform: (chunk, _encoding, done) => { 6 | const source = chunk.toString(); 7 | // Example: 2018-08-10 22:48:42.866 Electron[90311:4883863] *** WARNING: Textured window 8 | if ( 9 | /\d+-\d+-\d+ \d+:\d+:\d+\.\d+ Electron(?: Helper)?\[\d+:\d+] /.test( 10 | source, 11 | ) 12 | ) { 13 | done(); 14 | return; 15 | } 16 | // Example: [90789:0810/225804.894349:ERROR:CONSOLE(105)] "Uncaught (in promise) Error: Could not instantiate: ProductRegistryImpl.Registry", source: chrome-devtools://devtools/bundled/inspector.js (105) 17 | if (/\[\d+:\d+\/|\d+\.\d+:ERROR:CONSOLE\(\d+\)\]/.test(source)) { 18 | done(); 19 | return; 20 | } 21 | // Example: ALSA lib confmisc.c:767:(parse_card) cannot find card '0' 22 | if (/ALSA lib [a-z]+\.c:\d+:\([a-z_]+\)/.test(source)) { 23 | done(); 24 | return; 25 | } 26 | done(null, chunk); 27 | return; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 jctaoo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/common/diagnose.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | import { magentaBright } from 'colorette'; 4 | 5 | import { CompileError, formatCompileError } from './compileError'; 6 | import { consoleMessagePrefix } from './loggerMeta'; 7 | 8 | function formatDiagnosticsMessage(errors: CompileError[]): string { 9 | const messages = errors.map((e) => formatCompileError(e)); 10 | const errorMessage = `Found ${errors.length} errors. Watching for file changes.`; 11 | 12 | let diagnosticDetail = ''; 13 | messages.forEach((item, index, { length }) => { 14 | diagnosticDetail += item 15 | .split(os.EOL) 16 | .map((i) => ' ' + i) 17 | .join(os.EOL); 18 | if (index + 1 !== length) { 19 | diagnosticDetail += os.EOL; 20 | } 21 | }); 22 | 23 | const res = 24 | magentaBright( 25 | `${consoleMessagePrefix} Some typescript compilation errors occurred:`, 26 | ) + 27 | '\n' + 28 | diagnosticDetail + 29 | '\n' + 30 | magentaBright(errorMessage); 31 | 32 | return res; 33 | } 34 | 35 | export function diagnose(...errors: CompileError[]): void { 36 | const output = formatDiagnosticsMessage(errors); 37 | console.error(output); 38 | } 39 | -------------------------------------------------------------------------------- /tests/utils/removeJunk.test.ts: -------------------------------------------------------------------------------- 1 | import stream from 'stream'; 2 | 3 | import { describe, expect, it } from 'vitest'; 4 | 5 | import { removeJunkTransformOptions } from '../../src/utils'; 6 | 7 | describe('test remove junk', () => { 8 | it('should remove junk log from stream', async () => { 9 | const testStream = new stream.Readable(); 10 | 11 | testStream.push('Hello World'); 12 | testStream.push( 13 | '2018-08-10 22:48:42.866 Electron[90311:4883863] *** ' + 14 | 'WARNING: Textured window ' 15 | ); 16 | testStream.push( 17 | '[90789:0810/225804.894349:ERROR:CONSOLE(105)] "Uncaught' + 18 | ' (in promise) Error: Could not instantiate: ProductRegistryImpl.' + 19 | 'Registry", source: chrome-devtools://devtools/bundled/inspector.js (105)' 20 | ); 21 | testStream.push( 22 | "ALSA lib confmisc.c:767:(parse_card) cannot find card '0'" 23 | ); 24 | testStream.push(null); 25 | 26 | const res = await new Promise((resolve) => { 27 | testStream 28 | .pipe(new stream.Transform(removeJunkTransformOptions)) 29 | .on('data', (data) => { 30 | resolve(data.toString()); 31 | }); 32 | }); 33 | 34 | expect(res).toEqual('Hello World'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/common/compileError.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | import { cyan, gray, red, yellow } from 'colorette'; 4 | 5 | import { repeatString } from '../utils'; 6 | 7 | export interface CompileError { 8 | location: 9 | | { 10 | column: number; 11 | file: string; 12 | length: number; 13 | line: number; 14 | lineText: string; 15 | } 16 | | undefined 17 | | null; 18 | message: string; 19 | } 20 | 21 | export function formatCompileError(error: CompileError): string { 22 | if (!error.location) return error.message; 23 | 24 | const pathMessage = 25 | cyan(error.location.file) + 26 | ':' + 27 | yellow(error.location.line) + 28 | ':' + 29 | yellow(error.location.column); 30 | const categoryMessage = red('error:'); 31 | 32 | const code = 33 | gray(error.location.line) + 34 | ' ' + 35 | error.location.lineText + 36 | os.EOL + 37 | repeatString( 38 | ' ', 39 | error.location.column + `${error.location.line}`.length + 1 + 1, 40 | ) + 41 | red(repeatString('~', error.location.length)) + 42 | repeatString( 43 | ' ', 44 | error.location.lineText.length - 45 | error.location.column - 46 | error.location.length, 47 | ); 48 | 49 | return `${pathMessage} - ${categoryMessage} ${error.message} ${os.EOL} ${code}`; 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/findPathsOrExit.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { exists } from './'; 4 | 5 | /* 6 | Until elecrun v2.0, doesn't have an argument 'entry file' on dev command. 7 | It means we lose some flexibility. So I add this argument. 8 | 9 | But when the user migrate from v2.0 to a newer version, the old command 'dev' 10 | won't work anymore because 'entry file' is not be set. So I add a list that 11 | indicates the default places to find the entry file. Then, the old command 12 | will work in the new version. 13 | 14 | Anyway, this function does this. Besides 'entry file', like 'entry file' in 15 | build command and 'vite root path' need the logic in this function. 16 | */ 17 | export async function findPathOrExit( 18 | specificPath: string | undefined, 19 | defaultPaths: string[], 20 | notFoundMessage: string, 21 | ): Promise { 22 | // TODO 也许可以在这里校验是否存在 23 | // 但是不应该在这里做太多事情,也许用户有其他 hack 将失效?? 24 | if (specificPath) { 25 | return specificPath; 26 | } 27 | 28 | let res: string | undefined = specificPath; 29 | 30 | for (const defaultPlace of defaultPaths) { 31 | const entry = path.join(process.cwd(), defaultPlace); 32 | if (await exists(entry)) { 33 | res = entry; 34 | break; 35 | } 36 | } 37 | 38 | if (!res) { 39 | console.error(notFoundMessage); 40 | process.exit(); 41 | } 42 | 43 | return res; 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload entire repository 40 | path: './docs' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | elecrun doc 8 | 12 | 27 | 28 | 29 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /docs/zh.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | elecrun doc 8 | 12 | 27 | 28 | 29 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "rootDir": ".", 5 | "moduleResolution": "bundler", 6 | "module": "esnext", 7 | "declaration": true, 8 | "inlineSourceMap": true, 9 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 10 | "resolveJsonModule": true /* Include modules imported with .json extension. */, 11 | 12 | "strict": true /* Enable all strict type-checking options. */, 13 | 14 | /* Additional Checks */ 15 | "noUnusedLocals": false /* Report errors on unused locals. */, 16 | "noUnusedParameters": false /* Report errors on unused parameters. */, 17 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 18 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 19 | 20 | /* Debugging Options */ 21 | "traceResolution": false /* Report module resolution log messages. */, 22 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 23 | "listFiles": false /* Print names of files part of the compilation. */, 24 | "pretty": true /* Stylize errors and messages using color and context. */, 25 | 26 | "lib": ["es2022"], 27 | "types": ["node"], 28 | "typeRoots": ["node_modules/@types", "src/types"] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "tests/**/*.test.ts", 33 | "tsup.config.ts", 34 | "vitest.config.ts" 35 | ], 36 | "exclude": ["node_modules/**"], 37 | "compileOnSave": false 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | # This action works with pull requests and pushes 4 | on: 5 | pull_request: 6 | push: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | os: [macos-latest, windows-latest] 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Force LF globally before checkout 19 | if: matrix.os == 'windows-latest' 20 | run: | 21 | git config --global core.autocrlf false 22 | git config --global core.eol lf 23 | shell: bash 24 | 25 | - uses: actions/checkout@v4 26 | 27 | - uses: actions/cache@v4 28 | id: cache 29 | with: 30 | path: ~/.pnpm-store 31 | key: ${{ runner.os }}-modules-${{ hashFiles('**/pnpm-lock.yaml') }} 32 | 33 | - uses: actions/setup-node@v4 34 | with: 35 | node-version: '20' 36 | check-latest: true 37 | 38 | - name: Enable pnpm 39 | run: corepack enable pnpm 40 | 41 | - name: Install dependencies 42 | if: steps.cache.outputs.cache-hit != 'true' 43 | run: | 44 | pnpm install --frozen-lockfile 45 | 46 | - name: Install dependencies for fixtures 47 | run: | 48 | cd fixtures/demo 49 | pnpm install --frozen-lockfile 50 | cd ../../ 51 | 52 | - name: Run tests on macOS 53 | if: matrix.os == 'macos-latest' 54 | uses: GabrielBB/xvfb-action@v1.7 55 | with: 56 | working-directory: ./ 57 | run: pnpm run test 58 | 59 | - name: Run tests on Windows 60 | if: matrix.os == 'windows-latest' 61 | run: pnpm run test 62 | 63 | -------------------------------------------------------------------------------- /src/commands/clean.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { join, resolve } from 'path'; 3 | 4 | import { PathManager } from '../common'; 5 | import { exists } from '../utils'; 6 | 7 | async function rmRecursively(path: string, excludes?: Array) { 8 | if (!(await exists(path))) { 9 | return; 10 | } 11 | if (path.substr(0, 1) !== '/' && path.indexOf(':') === -1) { 12 | path = resolve(path); 13 | } 14 | const excludeFiles = (excludes ?? []).map((i) => { 15 | return resolve(path, i); 16 | }); 17 | let files: Array = [path]; 18 | while (files.length > 0) { 19 | const last = files.pop()!; 20 | 21 | const stat = await fs.promises.lstat(last); 22 | if (last === path && !stat.isDirectory) { 23 | return; 24 | } 25 | if (excludeFiles.includes(last)) { 26 | continue; 27 | } 28 | if (stat.isSymbolicLink()) { 29 | await fs.promises.unlink(last); 30 | } else if (!stat.isDirectory()) { 31 | await fs.promises.rm(last); 32 | } else { 33 | const children = await ( 34 | await fs.promises.readdir(last) 35 | ).map((p) => { 36 | return join(last, p); 37 | }); 38 | if (children.length === 0) { 39 | await fs.promises.rmdir(last); 40 | } else { 41 | if (last !== path) { 42 | files.push(last); 43 | } 44 | files = files.concat(children); 45 | } 46 | } 47 | } 48 | if ((await fs.promises.readdir(path)).length === 0) { 49 | await fs.promises.rmdir(path); 50 | } 51 | } 52 | 53 | export async function clean() { 54 | await rmRecursively(PathManager.shard.devPath).then(); 55 | await rmRecursively(PathManager.shard.outDir, [ 56 | 'package.json', 57 | 'yarn.lock', 58 | 'package-lock.json', 59 | ]).then(); 60 | await rmRecursively(PathManager.shard.distDir).then(); 61 | } 62 | -------------------------------------------------------------------------------- /src/commands/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cannotFoundEntryScriptOrViteRootPath, 3 | diagnose, 4 | finishBuildMessage, 5 | notFoundTSConfig, 6 | PathManager, 7 | startMessage, 8 | writeMainTSConfig, 9 | } from '../common'; 10 | import { findPathOrExit } from '../utils/findPathsOrExit'; 11 | 12 | import { runESBuildForMainProcess } from './esbuild'; 13 | 14 | interface RunBuildOptions { 15 | entry?: string /** Entry Point */; 16 | preloadScript?: string /** Filename of the preload script */; 17 | esbuildConfigFile?: string /** Filename of the esbuild config to use */; 18 | mainProcessEsm?: boolean /** Use ESM for the main process */; 19 | } 20 | 21 | export async function runBuild({ 22 | entry, 23 | preloadScript, 24 | esbuildConfigFile, 25 | mainProcessEsm, 26 | }: RunBuildOptions) { 27 | // find entry first 28 | // TODO move to PathManager.ts 29 | const defaultEntryList = [ 30 | './src/main/index.js', 31 | './src/main/index.ts', 32 | './src/index.js', 33 | './src/index.ts', 34 | './index.js', 35 | './index.ts', 36 | ]; 37 | const entryScriptPath = await findPathOrExit( 38 | entry, 39 | defaultEntryList, 40 | cannotFoundEntryScriptOrViteRootPath(process.cwd()), 41 | ); 42 | 43 | await runESBuildForMainProcess( 44 | { 45 | isBuild: true, 46 | outDir: PathManager.shard.outDir, 47 | preloadScript, 48 | entryPath: entryScriptPath, 49 | esbuildConfigFile, 50 | format: mainProcessEsm ? 'esm' : 'cjs', 51 | }, 52 | (...errors) => diagnose(...errors), 53 | () => console.log(startMessage), 54 | () => {}, 55 | async () => { 56 | const tsconfigPath = await writeMainTSConfig(); 57 | notFoundTSConfig(tsconfigPath); 58 | return tsconfigPath; 59 | }, 60 | ); 61 | 62 | // TODO print some useful information when build finished. 63 | console.log(finishBuildMessage); 64 | } 65 | -------------------------------------------------------------------------------- /tests/commands/runElectron.test.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from 'child_process'; 2 | 3 | import { processExists } from 'process-exists'; 4 | import { describe, expect, it } from 'vitest'; 5 | 6 | import { startElectron } from '../../src/commands'; 7 | import { delay } from '../../src/utils'; 8 | 9 | const COUNT = 5; 10 | 11 | describe('test run electron', () => { 12 | it('should run electron correctly synchronously and stop correctly', async function () { 13 | const processList: Array = []; 14 | 15 | let stop = () => {}; 16 | for (let i = 0; i < COUNT; i++) { 17 | const [cp, s] = await startElectron({ silent: true }); 18 | stop = s; 19 | processList.push(cp); 20 | } 21 | 22 | await delay(200); 23 | for (const [i, cp] of processList.entries()) { 24 | if (i + 1 < COUNT) { 25 | expect(await processExists(cp.pid!)).toBe(false); 26 | } else { 27 | expect(await processExists(cp.pid!)).toBe(true); 28 | } 29 | } 30 | 31 | await stop(); 32 | }); 33 | 34 | it('should run electron correctly concurrently and stop correctly', async function () { 35 | const processList: Array = []; 36 | expect.assertions(5); 37 | 38 | return new Promise((resolve) => { 39 | for (let i = 0; i < COUNT; i++) { 40 | startElectron({ silent: true }).then(async ([cp, stop]) => { 41 | processList.push(cp); 42 | 43 | if (i + 1 === COUNT) { 44 | await delay(2000); 45 | 46 | for (const [i, cp] of processList.entries()) { 47 | if (i + 1 < COUNT) { 48 | expect(await processExists(cp.pid!)).toBe(false); 49 | } else { 50 | expect(await processExists(cp.pid!)).toBe(true); 51 | } 52 | } 53 | 54 | await stop(); 55 | resolve(); 56 | } 57 | }); 58 | } 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/commands/runElectron.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from 'child_process'; 2 | import * as childProcess from 'child_process'; 3 | import * as stream from 'stream'; 4 | 5 | import { gray } from 'colorette'; 6 | 7 | import { removeJunkTransformOptions, terminateChild } from '../utils'; 8 | 9 | const stopList: Array<() => Promise> = []; 10 | let exitByScripts = false; 11 | 12 | export async function startElectron({ 13 | path, 14 | silent = false, 15 | }: { 16 | path?: string; 17 | silent?: boolean; 18 | }): Promise<[ChildProcess, () => Promise]> { 19 | for (const stop of stopList) { 20 | await stop(); 21 | } 22 | 23 | // eslint-disable-next-line @typescript-eslint/no-require-imports 24 | const electronPath = require('electron'); 25 | 26 | const electronProcess = childProcess.spawn(electronPath, [ 27 | path ?? '', 28 | '--color', 29 | ]); 30 | electronProcess.on('exit', (code) => { 31 | if (!exitByScripts) { 32 | console.log(gray(`Electron exited with code ${code}`)); 33 | process.exit(); 34 | } 35 | exitByScripts = true; 36 | }); 37 | 38 | function createStop() { 39 | let called = false; 40 | let isStopping = false; 41 | return async () => { 42 | if (isStopping) return; 43 | 44 | if (!called && electronProcess && !isStopping) { 45 | isStopping = true; 46 | electronProcess.removeAllListeners(); 47 | await terminateChild(electronProcess); 48 | exitByScripts = true; 49 | } 50 | called = true; 51 | }; 52 | } 53 | const stop = createStop(); 54 | 55 | stopList.push(stop); 56 | 57 | if (!silent) { 58 | const removeElectronLoggerJunkOut = new stream.Transform( 59 | removeJunkTransformOptions, 60 | ); 61 | const removeElectronLoggerJunkErr = new stream.Transform( 62 | removeJunkTransformOptions, 63 | ); 64 | 65 | electronProcess 66 | .stdout!.pipe(removeElectronLoggerJunkOut) 67 | .pipe(process.stdout); 68 | electronProcess 69 | .stderr!.pipe(removeElectronLoggerJunkErr) 70 | .pipe(process.stderr); 71 | } 72 | 73 | return [electronProcess, stop]; 74 | } 75 | -------------------------------------------------------------------------------- /src/commands/runVite.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { gray, green } from 'colorette'; 4 | import type { Plugin } from 'vite'; 5 | 6 | import { 7 | consoleViteMessagePrefix, 8 | notFoundViteConfig, 9 | PathManager, 10 | writeDefaultViteConfig, 11 | } from '../common'; 12 | import { exists, LoggerPlugin } from '../utils'; 13 | 14 | // serve electron preload script sourcemap 15 | const ElectronPreloadSourceMapPlugin = (): Plugin => { 16 | return { 17 | name: 'electron-preload-sourcemap', 18 | configureServer(server) { 19 | server.middlewares.use((req, res, next) => { 20 | if ( 21 | req.originalUrl && 22 | req.originalUrl == PathManager.shard.preloadSourceMapPath 23 | ) { 24 | fs.createReadStream(PathManager.shard.preloadSourceMapPath).pipe(res); 25 | return; 26 | } 27 | next(); 28 | }); 29 | }, 30 | }; 31 | }; 32 | 33 | async function tryViteConfig(basePath: string): Promise { 34 | const tryExt = ['.js', '.ts']; 35 | for (const ext of tryExt) { 36 | const fullPath = basePath + ext; 37 | if (await exists(fullPath)) return fullPath; 38 | } 39 | return; 40 | } 41 | 42 | export async function startViteServer(options: { 43 | configPath: string; 44 | root: string; 45 | }) { 46 | const { configPath, root } = options; 47 | 48 | let viteConfigPath = await tryViteConfig(configPath); 49 | if (!viteConfigPath) { 50 | // vite config not exits 51 | const writePath = await writeDefaultViteConfig(root); 52 | notFoundViteConfig(writePath); 53 | viteConfigPath = writePath; 54 | } 55 | 56 | const { createServer, version } = await import('vite'); 57 | 58 | const server = await createServer({ 59 | configFile: viteConfigPath, 60 | logLevel: 'silent', 61 | plugins: [LoggerPlugin(), ElectronPreloadSourceMapPlugin()], 62 | }); 63 | 64 | await server.listen(); 65 | 66 | const address = server.httpServer!.address(); 67 | if (address && typeof address === 'object') { 68 | const port = address.port; 69 | console.log( 70 | green(consoleViteMessagePrefix), 71 | green(`Dev server running at: localhost:${port}`), 72 | gray(`vite version: ${version}`), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/common/loggerMeta.ts: -------------------------------------------------------------------------------- 1 | import { cyan, green, red, yellow } from 'colorette'; 2 | 3 | import pkg from '../../package.json'; 4 | 5 | export const consoleMessagePrefix = `[${pkg.name}]`; 6 | export const consoleViteMessagePrefix = '[vite]'; 7 | 8 | export const cannotFoundTSConfigMessage = (writePath: string): string => 9 | yellow( 10 | `Could not find a valid 'tsconfig.json'. A default one has been written in:\n`, 11 | ) + writePath; 12 | 13 | export const cannotFoundViteConfigMessage = (writePath: string): string => 14 | yellow( 15 | `Could not find a valid vite config. A default one has been written in:\n`, 16 | ) + writePath; 17 | 18 | export const cannotFoundESBuildConfigMessage: string = yellow( 19 | `Could not find the specified esbuild config.`, 20 | ); 21 | 22 | export const cannotFoundEntryScriptOrViteRootPath = (cwd: string): string => 23 | red( 24 | `Could not find the entry script path or vite root directory path for main process in ${cwd}. See the solutions below:`, 25 | ) + 26 | cyan(` 27 | - 1. Add an argument that indicates the entry path for the main process and the option 28 | that indicates the root path for vite. Example: 29 | run \`elecrun dev ./index.js --vite ./index.html\` 30 | - 2. Elecrun will automatically find the entry path and vite root path by the following 31 | list while you didn't specify the entry path argument. 32 | Entry script for main process: 33 | - ./src/main/index.js 34 | - ./src/main/index.ts 35 | - ./src/index.js 36 | - ./src/index.ts 37 | - ./index.js 38 | - ./index.ts 39 | Vite root directory path: 40 | - ./src/renderer/ 41 | - ./src/ 42 | - ./ 43 | `); 44 | 45 | export const cannotFoundPackageJsonMessage = 46 | "Could not find a valid 'package.json'."; 47 | export const startMessage = cyan( 48 | `${consoleMessagePrefix} Start compile main process...`, 49 | ); 50 | export const finishMessage = green( 51 | `${consoleMessagePrefix} Finished compiled. Rerun electron main process...`, 52 | ); 53 | export const finishBuildMessage = green( 54 | `${consoleMessagePrefix} Finish Build.`, 55 | ); 56 | export const warnPreloadMessage = `warn preload path.`; 57 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to GitHub Packages 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | packages: write 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - uses: actions/cache@v4 22 | id: cache 23 | with: 24 | path: ~/.pnpm-store 25 | key: ${{ runner.os }}-modules-${{ hashFiles('**/pnpm-lock.yaml') }} 26 | 27 | # Setup .npmrc to use GitHub Packages 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: '22' 32 | registry-url: 'https://npm.pkg.github.com' 33 | scope: '@${{ github.repository_owner }}' 34 | 35 | - name: Enable corepack (optional) 36 | run: corepack enable pnpm 37 | 38 | - name: Install dependencies 39 | if: steps.cache.outputs.cache-hit != 'true' 40 | run: | 41 | pnpm install --frozen-lockfile 42 | 43 | - name: Build 44 | run: pnpm run build 45 | 46 | - name: Prepare package.json for GitHub Packages 47 | run: | 48 | cp package.json package.json.bak 49 | 50 | node -e ' 51 | const fs = require("fs"); 52 | const owner = process.env.GITHUB_REPOSITORY_OWNER || ""; 53 | if (!owner) { 54 | console.error("GITHUB_REPOSITORY_OWNER is empty."); 55 | process.exit(1); 56 | } 57 | const repo = process.env.GITHUB_REPOSITORY; // e.g. jctaoo/elecrun 58 | const pkg = JSON.parse(fs.readFileSync("package.json","utf8")); 59 | 60 | pkg.name = "@" + owner + "/elecrun"; 61 | pkg.repository = { type: "git", url: "git+https://github.com/" + repo + ".git" }; 62 | 63 | pkg.publishConfig = { registry: "https://npm.pkg.github.com" }; 64 | 65 | fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2)); 66 | ' 67 | 68 | - name: Publish to GitHub Packages 69 | run: npm publish --registry=https://npm.pkg.github.com 70 | env: 71 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /src/common/defaultTsConfig.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import { PathManager } from './pathManager'; 5 | 6 | export const defaultBaseTSConfig = { 7 | compilerOptions: { 8 | target: 'ES2018', 9 | noImplicitAny: true, 10 | removeComments: true, 11 | preserveConstEnums: true, 12 | allowJs: true, 13 | checkJs: true, 14 | strict: true, 15 | strictNullChecks: true, 16 | strictFunctionTypes: true, 17 | strictPropertyInitialization: true, 18 | strictBindCallApply: true, 19 | noImplicitThis: true, 20 | noImplicitReturns: true, 21 | experimentalDecorators: true, 22 | allowSyntheticDefaultImports: true, 23 | esModuleInterop: true, 24 | moduleResolution: 'node', 25 | importHelpers: true, 26 | sourceMap: true, 27 | baseUrl: './src', 28 | }, 29 | exclude: ['node_modules', 'app', 'dist'], 30 | }; 31 | 32 | export const defaultMainTSConfig = { 33 | extends: '../../tsconfig.json', 34 | compilerOptions: { 35 | target: 'ES2018', 36 | module: 'CommonJS', 37 | outDir: '../../app', 38 | }, 39 | include: ['../main/**/*', '../common/**/*'], 40 | exclude: ['../renderer/**/*'], 41 | }; 42 | 43 | export const defaultRendererTSConfig = { 44 | extends: '../../tsconfig.json', 45 | compilerOptions: { 46 | target: 'esnext', 47 | module: 'esnext', 48 | lib: ['DOM', 'DOM.Iterable', 'ESNext'], 49 | types: ['vite/client'], 50 | }, 51 | include: ['../renderer/**/*', '../common/**/*'], 52 | exclude: ['../main/**/*'], 53 | }; 54 | 55 | async function writeTSConfig( 56 | config: Config, 57 | dir: string, 58 | ): Promise { 59 | await fs.promises.mkdir(dir, { recursive: true }); 60 | const str = JSON.stringify(config); 61 | const filePath = path.join(dir, 'tsconfig.json'); 62 | await fs.promises.writeFile(filePath, str); 63 | return filePath; 64 | } 65 | 66 | export const writeBaseTSConfig = () => 67 | writeTSConfig(defaultBaseTSConfig, PathManager.shard.defaultBaseTSConfigDir); 68 | 69 | export const writeMainTSConfig = () => 70 | writeTSConfig(defaultMainTSConfig, PathManager.shard.defaultMainTSConfigDir); 71 | 72 | export const writeRendererTSConfig = () => 73 | writeTSConfig( 74 | defaultRendererTSConfig, 75 | PathManager.shard.defaultRendererTSConfigDir, 76 | ); 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elecrun", 3 | "version": "2.4.8", 4 | "description": "elecrun is a tool to run your electron app easily.", 5 | "main": "build/src/index.js", 6 | "bin": { 7 | "elecrun": "bin/elecrun.js", 8 | "electron-run": "bin/elecrun.js" 9 | }, 10 | "repository": "https://github.com/jctaoo/elecrun", 11 | "license": "MIT", 12 | "keywords": [ 13 | "typescript", 14 | "esbuild", 15 | "vite", 16 | "electron" 17 | ], 18 | "scripts": { 19 | "build": "tsup", 20 | "build:watch": "tsup --watch", 21 | "test": "xvfb-maybe run-s test:lint test:prettier test:unit", 22 | "test:lint": "eslint src --ext .ts", 23 | "test:prettier": "prettier \"src/**/*.ts\" --list-different", 24 | "test:unit": "vitest run", 25 | "test:unit:watch": "vitest watch", 26 | "test:unit:ui": "vitest --ui", 27 | "fix": "run-s fix:*", 28 | "fix:prettier": "prettier \"src/**/*.ts\" --write", 29 | "fix:lint": "eslint src --ext .ts --fix", 30 | "doc": "vite docs", 31 | "version": "standard-version", 32 | "version:major": "standard-version -r major", 33 | "version:minor": "standard-version -r minor", 34 | "version:patch": "standard-version -r patch", 35 | "tsx": "tsx", 36 | "electron": "electron" 37 | }, 38 | "engines": { 39 | "node": ">=20" 40 | }, 41 | "dependencies": { 42 | "colorette": "^2.0.16", 43 | "commander": "^7.2.0", 44 | "esbuild": "^0.14.11", 45 | "process-exists": "^5.0.0", 46 | "vite": "^7.1.3" 47 | }, 48 | "devDependencies": { 49 | "@eslint/compat": "^1.3.2", 50 | "@eslint/eslintrc": "^3.3.1", 51 | "@eslint/js": "^9.34.0", 52 | "@types/node": "^22.17.2", 53 | "@typescript-eslint/eslint-plugin": "^8.40.0", 54 | "@typescript-eslint/parser": "^8.40.0", 55 | "@vitest/coverage-v8": "^3.2.4", 56 | "@vitest/ui": "^3.2.4", 57 | "electron": "^37.3.1", 58 | "eslint": "^9.34.0", 59 | "eslint-config-prettier": "^10.1.8", 60 | "eslint-plugin-eslint-comments": "^3.2.0", 61 | "eslint-plugin-import": "^2.32.0", 62 | "globals": "^16.3.0", 63 | "npm-run-all": "^4.1.5", 64 | "prettier": "^3.6.2", 65 | "standard-version": "^9.3.2", 66 | "tsup": "^8.5.0", 67 | "typescript": "^5.9.2", 68 | "vitest": "^3.2.4", 69 | "xvfb-maybe": "^0.2.1" 70 | }, 71 | "files": [ 72 | "build/", 73 | "build/package.json", 74 | "!tests/**/*.test.ts", 75 | "!**/*.json", 76 | "CHANGELOG.md", 77 | "LICENSE", 78 | "README.md" 79 | ], 80 | "peerDependencies": { 81 | "electron": ">=30.0.0" 82 | }, 83 | "prettier": { 84 | "singleQuote": true 85 | }, 86 | "packageManager": "pnpm@9.15.2" 87 | } 88 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import commander from 'commander'; 2 | 3 | import pkg from '../package.json'; 4 | 5 | import { clean, run, runBuild } from './commands'; 6 | 7 | const program = new commander.Command(pkg.name).version(pkg.version); 8 | 9 | program 10 | .command('dev [entry]', { isDefault: true }) 11 | .description('⚡️Start to dev your electron app.') 12 | .option( 13 | '--vite [root dir]', 14 | 'The flag indicates whether to open the vite server.', 15 | ) 16 | .option( 17 | '--preload ', 18 | "Electron preload filer relative to the main src. Won't be bundled.", 19 | ) 20 | .option( 21 | '--esbuild-config-file ', 22 | 'Custom config js file to use with esbuild', 23 | ) 24 | .option( 25 | '--esm', 26 | 'Use ESM instead of CJS for the main process. Default is CJS.', 27 | ) 28 | .option('--clean-cache', 'Clean build cache.') 29 | .action( 30 | async ( 31 | entryFile: string | undefined, 32 | options: { 33 | vite: string | boolean; 34 | preload: string; 35 | cleanCache: boolean; 36 | esbuildConfigFile: string; 37 | esm: boolean; 38 | }, 39 | ) => { 40 | const withVite = !!options.vite; 41 | let viteRootPath: string | undefined; 42 | 43 | if (typeof options.vite === 'string') { 44 | viteRootPath = options.vite; 45 | } 46 | 47 | if (options.cleanCache) { 48 | await clean(); 49 | } 50 | 51 | await run({ 52 | entry: entryFile, 53 | withVite, 54 | preloadScript: options.preload, 55 | viteRoot: viteRootPath, 56 | esbuildConfigFile: options.esbuildConfigFile, 57 | mainProcessEsm: options.esm, 58 | }); 59 | }, 60 | ); 61 | 62 | program 63 | .command('build [entry]') 64 | .description('Build your Electron main process code in main src.') 65 | .option( 66 | '--preload ', 67 | "Electron preload script path relative to the main src. Won't be bundled.", 68 | ) 69 | .option( 70 | '--esbuild-config-file ', 71 | 'Custom config js file to use with esbuild', 72 | ) 73 | .option( 74 | '--esm', 75 | 'Use ESM instead of CJS for the main process. Default is CJS.', 76 | ) 77 | .action( 78 | async ( 79 | entryFile: string | undefined, 80 | options: { preload: string; esbuildConfigFile: string; esm: boolean }, 81 | ) => { 82 | await runBuild({ 83 | preloadScript: options.preload, 84 | entry: entryFile, 85 | esbuildConfigFile: options.esbuildConfigFile, 86 | mainProcessEsm: options.esm, 87 | }); 88 | }, 89 | ); 90 | 91 | program.command('clean').action(clean); 92 | 93 | program.addHelpText('beforeAll', `Repository: ${pkg.repository}\n`); 94 | 95 | program.parseAsync(process.argv).then(); 96 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | import { fixupConfigRules, fixupPluginRules } from "@eslint/compat"; 5 | import { FlatCompat } from "@eslint/eslintrc"; 6 | import js from "@eslint/js"; 7 | import tsParser from "@typescript-eslint/parser"; 8 | import { defineConfig, globalIgnores } from "eslint/config"; 9 | import eslintComments from "eslint-plugin-eslint-comments"; 10 | import _import from "eslint-plugin-import"; 11 | import globals from "globals"; 12 | 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | const compat = new FlatCompat({ 16 | baseDirectory: __dirname, 17 | recommendedConfig: js.configs.recommended, 18 | allConfig: js.configs.all 19 | }); 20 | 21 | export default defineConfig([globalIgnores([ 22 | "**/node_modules", 23 | "**/build", 24 | "**/coverage", 25 | "**/fixtures", 26 | "**/bin", 27 | "**/docs", 28 | ]), { 29 | extends: fixupConfigRules(compat.extends( 30 | "eslint:recommended", 31 | "plugin:eslint-comments/recommended", 32 | "plugin:@typescript-eslint/recommended", 33 | "plugin:import/typescript", 34 | "prettier" 35 | )), 36 | 37 | plugins: { 38 | import: fixupPluginRules(_import), 39 | "eslint-comments": fixupPluginRules(eslintComments), 40 | }, 41 | 42 | languageOptions: { 43 | globals: { 44 | BigInt: true, 45 | console: true, 46 | WebAssembly: true, 47 | }, 48 | }, 49 | 50 | rules: { 51 | "@typescript-eslint/explicit-module-boundary-types": "off", 52 | "@typescript-eslint/no-empty-function": "off", 53 | 54 | "eslint-comments/disable-enable-pair": ["error", { 55 | allowWholeFile: true, 56 | }], 57 | 58 | "eslint-comments/no-unused-disable": "error", 59 | 60 | "import/order": ["error", { 61 | "newlines-between": "always", 62 | 63 | alphabetize: { 64 | order: "asc", 65 | }, 66 | }], 67 | 68 | "sort-imports": ["error", { 69 | ignoreDeclarationSort: true, 70 | ignoreCase: true, 71 | }], 72 | }, 73 | }, { 74 | files: ["**/*.ts", "**/*.tsx"], 75 | 76 | languageOptions: { 77 | globals: { 78 | ...globals.node, 79 | }, 80 | 81 | parser: tsParser, 82 | ecmaVersion: 5, 83 | sourceType: "commonjs", 84 | 85 | parserOptions: { 86 | project: ["./tsconfig.json"], 87 | }, 88 | }, 89 | }, { 90 | files: ["**/*.js"], 91 | 92 | languageOptions: { 93 | globals: { 94 | ...globals.node, 95 | ...globals.browser, 96 | }, 97 | }, 98 | }]); -------------------------------------------------------------------------------- /src/common/pathManager.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export class PathManager { 4 | public static readonly shard = new PathManager(); 5 | 6 | public static from(cwd: string): PathManager { 7 | return new PathManager(cwd); 8 | } 9 | 10 | public cwd: string; 11 | 12 | constructor(cwd?: string) { 13 | if (cwd) { 14 | this.cwd = cwd; 15 | } else { 16 | this.cwd = process.cwd(); 17 | } 18 | } 19 | 20 | /** 21 | * Only valid during the development phase 22 | */ 23 | private _preloadScriptPath?: string; 24 | 25 | /** 26 | * Only valid during the development phase 27 | * @see setPreloadScriptPath 28 | */ 29 | public get preloadSourceMapPath(): string | undefined { 30 | if (!this._preloadScriptPath) { 31 | return undefined; 32 | } 33 | const basename = path.basename( 34 | this._preloadScriptPath, 35 | path.extname(this._preloadScriptPath), 36 | ); 37 | return path.join(this.devOutPath, basename + '.cjs.map'); 38 | } 39 | 40 | /** 41 | * Only valid during the development phase 42 | * @see preloadSourceMapPath 43 | */ 44 | public setPreloadScriptPath(path: string | undefined) { 45 | this._preloadScriptPath = path; 46 | } 47 | 48 | public get preloadScriptPath() { 49 | return this._preloadScriptPath; 50 | } 51 | 52 | public get nodeModulesPath() { 53 | return path.join(this.cwd, './node_modules'); 54 | } 55 | 56 | public get packageJsonPath() { 57 | return path.join(this.cwd, './package.json'); 58 | } 59 | 60 | public get defaultBaseTSConfigDir() { 61 | return path.join(this.devPath, 'tsconfig'); 62 | } 63 | 64 | public get defaultMainTSConfigDir() { 65 | return path.join(this.devPath, 'tsconfig/src/main'); 66 | } 67 | 68 | public get defaultRendererTSConfigDir() { 69 | return path.join(this.devPath, 'tsconfig/src/renderer'); 70 | } 71 | 72 | public get defaultViteConfigDir() { 73 | return this.devPath; 74 | } 75 | 76 | public get devPath() { 77 | return path.join(this.nodeModulesPath, '.elecrun'); 78 | } 79 | 80 | public get devOutPath() { 81 | return path.join(this.devPath, 'app'); 82 | } 83 | 84 | public get devOutDirPackageJson() { 85 | return path.join(this.devOutPath, 'package.json'); 86 | } 87 | 88 | public get srcPath() { 89 | return path.join(this.cwd, './src'); 90 | } 91 | 92 | public get mainPath() { 93 | return path.join(this.cwd, './src/main'); 94 | } 95 | 96 | public get rendererPath() { 97 | return path.join(this.cwd, './src/renderer'); 98 | } 99 | 100 | public get outDir() { 101 | return path.join(this.cwd, './app'); 102 | } 103 | 104 | public get outDirMain() { 105 | return path.join(this.cwd, './app/main'); 106 | } 107 | 108 | public get outDirRenderer() { 109 | return path.join(this.cwd, './app/renderer'); 110 | } 111 | 112 | public get distDir() { 113 | return path.join(this.cwd, './dist'); 114 | } 115 | 116 | public get viteConfigPath() { 117 | return path.join(this.cwd, 'vite.config'); 118 | } 119 | 120 | // TODO removing this, see run.ts and search defaultEntryList 121 | public get entryPath() { 122 | return path.join(this.mainPath, 'index.ts'); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/commands/run.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cannotFoundEntryScriptOrViteRootPath, 3 | CompileError, 4 | diagnose, 5 | notFoundTSConfig, 6 | PathManager, 7 | writeMainTSConfig, 8 | } from '../common'; 9 | import { finishMessage, startMessage } from '../common'; 10 | import { prompt } from '../common/prompt'; 11 | import { findPathOrExit } from '../utils/findPathsOrExit'; 12 | 13 | import { runESBuildForMainProcess } from './esbuild'; 14 | import { startElectron } from './runElectron'; 15 | import { startViteServer } from './runVite'; 16 | 17 | function reportError(...errors: CompileError[]) { 18 | diagnose(...errors); 19 | } 20 | 21 | function buildStart() { 22 | console.log(startMessage); 23 | } 24 | 25 | // =============== run electron start =============== 26 | 27 | let stopPromptToRunElectron: () => void = () => {}; 28 | 29 | async function runElectron(dir: string) { 30 | void (await startElectron({ path: dir })); 31 | } 32 | 33 | async function buildComplete(dir: string, count: number) { 34 | stopPromptToRunElectron(); 35 | console.log(finishMessage); 36 | 37 | if (count > 1) { 38 | const [readAnswer, stop] = prompt('Need rerun Electron?'); 39 | stopPromptToRunElectron = stop; 40 | 41 | if (await readAnswer()) { 42 | await runElectron(dir); 43 | } 44 | } else { 45 | await runElectron(dir); 46 | } 47 | } 48 | 49 | // =============== run electron end =============== 50 | 51 | export async function run(options: { 52 | withVite: boolean; 53 | preloadScript?: string; 54 | entry?: string; 55 | viteRoot?: string; 56 | esbuildConfigFile?: string; 57 | mainProcessEsm?: boolean; 58 | }) { 59 | const { withVite, preloadScript, entry, viteRoot, esbuildConfigFile } = 60 | options; 61 | 62 | // Start vite server 63 | if (withVite) { 64 | // find root first 65 | // TODO move to PathManager.ts 66 | const defaultRootList = ['./src/renderer/', './src/', './']; 67 | const viteRootPath = await findPathOrExit( 68 | viteRoot, 69 | defaultRootList, 70 | cannotFoundEntryScriptOrViteRootPath(process.cwd()), 71 | ); 72 | 73 | await startViteServer({ 74 | configPath: PathManager.shard.viteConfigPath, 75 | root: viteRootPath, 76 | }); 77 | } 78 | 79 | // Start dev for main process 80 | 81 | // find entry first 82 | // TODO move to PathManager.ts 83 | const defaultEntryList = [ 84 | './src/main/index.js', 85 | './src/main/index.ts', 86 | './src/index.js', 87 | './src/index.ts', 88 | './index.js', 89 | './index.ts', 90 | ]; 91 | const entryScriptPath = await findPathOrExit( 92 | entry, 93 | defaultEntryList, 94 | cannotFoundEntryScriptOrViteRootPath(process.cwd()), 95 | ); 96 | 97 | await runESBuildForMainProcess( 98 | { 99 | isBuild: false, 100 | outDir: PathManager.shard.devOutPath, 101 | preloadScript, 102 | entryPath: entryScriptPath, 103 | esbuildConfigFile, 104 | format: options.mainProcessEsm ? 'esm' : 'cjs', 105 | }, 106 | reportError, 107 | buildStart, 108 | buildComplete, 109 | async () => { 110 | const tsconfigPath = await writeMainTSConfig(); 111 | notFoundTSConfig(tsconfigPath); 112 | return tsconfigPath; 113 | }, 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | **elecrun** is a CLI tool that simplifies running Electron applications during development. It uses: 8 | - **esbuild** for transforming main process code (TypeScript/JavaScript) 9 | - **Vite** for renderer process development with HMR 10 | - **tsup** for building the CLI tool itself 11 | 12 | ## Essential Commands 13 | 14 | ### Development & Testing 15 | ```bash 16 | # Build the CLI tool 17 | pnpm build # Build once 18 | pnpm build:watch # Build with watch mode 19 | 20 | # Testing (run ALL before commits) 21 | pnpm test # Complete test suite: lint + prettier + unit tests 22 | pnpm test:unit # Unit tests only 23 | pnpm test:unit:watch # Unit tests in watch mode 24 | pnpm test:unit:ui # Unit tests with Vitest UI 25 | 26 | # Code quality 27 | pnpm fix # Fix all formatting and lint issues 28 | pnpm test:lint # Check linting 29 | pnpm test:prettier # Check code formatting 30 | ``` 31 | 32 | ### Running Individual Tests 33 | ```bash 34 | # Run specific test file 35 | npx vitest tests/utils/removeJunk.test.ts 36 | 37 | # Run tests matching pattern 38 | npx vitest --grep "removeJunk" 39 | ``` 40 | 41 | ### Version Management 42 | ```bash 43 | pnpm version # Interactive version bump 44 | pnpm version:patch # Patch version bump 45 | pnpm version:minor # Minor version bump 46 | pnpm version:major # Major version bump 47 | ``` 48 | 49 | ## Architecture 50 | 51 | ### Core Structure 52 | - **`src/index.ts`** - CLI entry point using commander.js 53 | - **`src/commands/`** - Command implementations 54 | - `run.ts` - Main dev command logic 55 | - `build.ts` - Build command 56 | - `esbuild.ts` - esbuild integration for main process 57 | - `runElectron.ts` - Electron process management 58 | - `runVite.ts` - Vite server integration 59 | - **`src/common/`** - Shared utilities and configuration 60 | - `pathManager.ts` - Centralized path management 61 | - `defaultViteConfig.ts`/`defaultTsConfig.ts` - Default configurations 62 | - **`src/utils/`** - General utilities 63 | 64 | ### Key Concepts 65 | 66 | **PathManager**: Centralized path management system that handles: 67 | - Development paths (`node_modules/.elecrun/`) 68 | - Output directories (`./app/` for builds) 69 | - Config file locations 70 | - Entry point discovery 71 | 72 | **Command Flow**: 73 | 1. Entry point discovery (searches for main process files in order): 74 | - `./src/main/index.js` 75 | - `./src/main/index.ts` 76 | - `./src/index.js` 77 | - `./src/index.ts` 78 | - `./index.js` 79 | - `./index.ts` 80 | 81 | 2. Vite root discovery (searches in order): 82 | - `./src/renderer/` 83 | - `./src/` 84 | - `./` 85 | 86 | **Build System**: Uses tsup for CLI tool builds, esbuild for main process transformation, and Vite for renderer development. 87 | 88 | ## Development Workflow 89 | 90 | 1. **Before starting**: Run `pnpm test` to ensure clean state 91 | 2. **During development**: Use `pnpm build:watch` and `pnpm test:unit:watch` 92 | 3. **Before committing**: Always run `pnpm fix` then `pnpm test` 93 | 4. **Testing changes**: Use test projects in `fixtures/` directory 94 | 95 | ## Key Files to Understand 96 | 97 | - **`src/commands/run.ts`** - Core development workflow logic 98 | - **`src/common/pathManager.ts`** - Path resolution strategy 99 | - **`tsup.config.ts`** - Build configuration (entry: src/index.ts, output: build/) 100 | - **`vitest.config.ts`** - Test configuration with coverage 101 | - **`eslint.config.mjs`** - Code quality rules (import ordering, TypeScript rules) 102 | 103 | ## Publishing 104 | 105 | Publishing is automated via GitHub Actions when creating GitHub Releases. The workflow builds and publishes to GitHub Packages. Never run `npm publish` manually. -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # Contributing to elecrun 2 | 3 | Thank you for your interest in contributing to elecrun! This guide will help you understand the development workflow and available scripts. 4 | 5 | ## Prerequisites 6 | 7 | - Node.js >= 20 8 | - pnpm (recommended package manager) 9 | - Git 10 | 11 | ## Development Setup 12 | 13 | 1. Fork and clone the repository: 14 | ```bash 15 | git clone https://github.com/your-username/elecrun.git 16 | cd elecrun 17 | ``` 18 | 19 | 2. Install dependencies: 20 | ```bash 21 | pnpm install 22 | ``` 23 | 24 | ## Available Scripts 25 | 26 | ### Building 27 | 28 | - `pnpm build` - Build the project using tsup 29 | - `pnpm build:watch` - Build in watch mode for development 30 | 31 | ### Testing 32 | 33 | - `pnpm test` - Run all tests (lint, prettier, and unit tests) 34 | - `pnpm test:lint` - Run ESLint on TypeScript files 35 | - `pnpm test:prettier` - Check code formatting with Prettier 36 | - `pnpm test:unit` - Run unit tests with Vitest 37 | - `pnpm test:unit:watch` - Run unit tests in watch mode 38 | - `pnpm test:unit:ui` - Run unit tests with Vitest UI 39 | 40 | ### Code Quality 41 | 42 | - `pnpm fix` - Fix all code quality issues (prettier and lint) 43 | - `pnpm fix:prettier` - Format code with Prettier 44 | - `pnpm fix:lint` - Fix ESLint issues automatically 45 | 46 | ### Documentation 47 | 48 | - `pnpm doc` - Start documentation server with Vite 49 | 50 | ### Versioning 51 | 52 | - `pnpm version` - Create a new version using standard-version 53 | - `pnpm version:major` - Bump major version 54 | - `pnpm version:minor` - Bump minor version 55 | - `pnpm version:patch` - Bump patch version 56 | 57 | ### Utilities 58 | 59 | - `pnpm tsx` - Run TypeScript files directly with tsx 60 | - `pnpm electron` - Run Electron 61 | 62 | ## Development Workflow 63 | 64 | 1. **Before starting**: Run `pnpm test` to ensure everything is working 65 | 2. **During development**: Use `pnpm build:watch` for automatic rebuilding 66 | 3. **Testing**: Use `pnpm test:unit:watch` for continuous testing 67 | 4. **Before committing**: Run `pnpm fix` to format code and fix lint issues 68 | 5. **Final check**: Run `pnpm test` to ensure all checks pass 69 | 70 | ## Testing 71 | 72 | The project uses Vitest for unit testing. Test files are located in the `tests/` directory with the `.test.ts` extension. 73 | 74 | To add new tests: 75 | 1. Create test files following the pattern `*.test.ts` 76 | 2. Place them in the appropriate subdirectory under `tests/` 77 | 3. Run `pnpm test:unit` to verify your tests 78 | 79 | ## Code Style 80 | 81 | - The project uses ESLint for linting and Prettier for formatting 82 | - Single quotes are preferred (configured in package.json) 83 | - Run `pnpm fix` before committing to ensure consistent formatting 84 | 85 | ## Project Structure 86 | 87 | - `src/` - Source code 88 | - `tests/` - Test files 89 | - `docs/` - Documentation 90 | - `build/` - Built files (generated) 91 | - `bin/` - CLI entry points 92 | 93 | ## Publishing 94 | 95 | Publishing is automated by GitHub Actions and runs when a GitHub Release is created. Do not run `npm publish` locally. 96 | 97 | How to publish a new version: 98 | 99 | 1. Bump version and generate changelog with standard-version: 100 | - `pnpm version` or `pnpm version:major | version:minor | version:patch` 101 | 2. Push commits and tags to GitHub. 102 | 3. Create a GitHub Release for the new tag. The workflow at `.github/workflows/publish.yml` will build and run `npm publish` to GitHub Packages. 103 | 104 | ## Getting Help 105 | 106 | If you need help or have questions: 107 | 1. Check existing issues on GitHub 108 | 2. Read the README.md for usage information 109 | 3. Look at the test files for examples 110 | 111 | ## Pull Request Guidelines 112 | 113 | 1. Fork the repository 114 | 2. Create a feature branch 115 | 3. Make your changes 116 | 4. Run `pnpm test` to ensure all checks pass 117 | 5. Commit with descriptive messages 118 | 6. Submit a pull request 119 | 120 | Thank you for contributing to elecrun! -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [2.4.8](https://github.com/jctaoo/elecrun/compare/v2.4.7...v2.4.8) (2025-09-05) 6 | 7 | 8 | ### Features 9 | 10 | * read project name from user's package.json and include it in the output package.json ([9c3b336](https://github.com/jctaoo/elecrun/commit/9c3b336555996ba26728b7e8078f5984a7ab466c)) 11 | 12 | ### [2.4.7](https://github.com/jctaoo/elecrun/compare/v2.4.6...v2.4.7) (2025-08-29) 13 | 14 | 15 | ### Features 16 | 17 | * add dependencies for exit-sig fixture and update signal handling to include before-quit event ([584692f](https://github.com/jctaoo/elecrun/commit/584692f773da33f61c9759a056f19325645e87be)) 18 | * add exit-sig fixture with basic Electron app structure, including HTML, JavaScript for window creation, and graceful shutdown handling ([e876f10](https://github.com/jctaoo/elecrun/commit/e876f1002f7c701d66978ce02d13516f63cdadda)) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * correct syntax in runElectron function and ensure proper export in utils index ([3b6117a](https://github.com/jctaoo/elecrun/commit/3b6117ad6c036e1882d3524962b4b755cf1a5292)) 24 | * Fix repl problem in bin file (Only allowed publish package in CI) 25 | 26 | ### [2.4.6](https://github.com/jctaoo/elecrun/compare/v2.4.5...v2.4.6) (2025-08-25) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * update rendererPath assignment to use JSON.stringify for proper string formatting and modify removeJunkTransformOptions to use done() for early returns ([64f79c4](https://github.com/jctaoo/elecrun/commit/64f79c4e104b4316543aa96ed1ab97bcd64cf6fd)) 32 | 33 | ### [2.4.5](https://github.com/jctaoo/elecrun/compare/v2.4.4...v2.4.5) (2025-08-24) 34 | 35 | ### Bug Fixes 36 | 37 | - Fix the dynamic import will throw error on windows. (fallback to require) 38 | 39 | ### [2.4.4](https://github.com/jctaoo/elecrun/compare/v2.4.2...v2.4.4) (2025-08-24) 40 | 41 | ### Updates 42 | 43 | - Update vite version to latest. (v7.1.3) 44 | - Update a series of dependencies. 45 | - Migrate yarn to pnpm. 46 | 47 | ### [2.4.3](https://github.com/jctaoo/elecrun/compare/v2.4.2...v2.4.3) (2024-08-27) 48 | 49 | ### Features 50 | 51 | - fix preload scripts in build phase and support esm option. 52 | 53 | ### [2.4.2](https://github.com/jctaoo/elecrun/compare/v2.4.1...v2.4.2) (2024-08-27) 54 | 55 | ### [2.4.1](https://github.com/jctaoo/elecrun/compare/v2.4.0...v2.4.1) (2024-08-27) 56 | 57 | ### Features 58 | 59 | - Fix the behavior of building prelaod script, now this building phase of preload script is standlone. 60 | 61 | ## [2.4.0](https://github.com/jctaoo/elecrun/compare/v2.3.1...v2.4.0) (2024-08-26) 62 | 63 | ### Features 64 | 65 | - Rename npm package to `elecrun`. 66 | - Add options `--esm`. 67 | 68 | ### [2.3.1](https://github.com/jctaoo/elecrun/compare/v2.3.0...v2.3.1) (2022-04-13) 69 | 70 | ### Features 71 | 72 | - [#60](https://github.com/jctaoo/elecrun/pull/60): Support custom esbuild config by option `--esbuild-config-file`([doc](https://github.com/jctaoo/elecrun#option---esbuild-config-file)). 73 | - [#45](https://github.com/jctaoo/elecrun/pull/45): Migrate chalk to colorette. 74 | 75 | ## [2.2.0](https://github.com/jctaoo/elecrun/compare/v2.0.1...v2.2.0) (2021-08-08) 76 | 77 | 78 | ### Features 79 | 80 | * **dev:** add `--clean-cache` option ([b805138](https://github.com/jctaoo/elecrun/commit/b805138172f5916fc2a318154bdc880039cac2bf)), closes [#42](https://github.com/jctaoo/elecrun/issues/42) 81 | 82 | ## [2.1.0](https://github.com/jctaoo/elecrun/compare/v2.0.1...v2.1.0) (2021-08-08) 83 | 84 | 85 | ### Features 86 | 87 | * **dev:** add `--clean-cache` option ([b805138](https://github.com/jctaoo/elecrun/commit/b805138172f5916fc2a318154bdc880039cac2bf)), closes [#42](https://github.com/jctaoo/elecrun/issues/42) 88 | 89 | ### [2.0.1](https://github.com/jctaoo/elecrun/compare/v2.0.0...v2.0.1) (2021-04-10) 90 | 91 | 92 | ### Features 93 | 94 | * support main process entry path argument and vite root dir option ([a5d4caf](https://github.com/jctaoo/elecrun/commit/a5d4caf5e4d0273f984b763f13fee255b5109691)) 95 | 96 | ## 2.0.0 (2021-04-05) 97 | 98 | 99 | ### Features 100 | 101 | * add prompt when need rerun electron ([cedfe2b](https://github.com/jctaoo/elecrun/commit/cedfe2bbcf96c8943d6c20e575eb8bd16cae7844)) 102 | * support run main process without tsconfig by write default one ([be77c00](https://github.com/jctaoo/elecrun/commit/be77c00aa64121cf3f9629d344e19a655bcbebba)) 103 | * support run renderer process without vite config by write default one ([0bb746b](https://github.com/jctaoo/elecrun/commit/0bb746b1ed68bfc41b3407b0ecbbb690dceab63d)) 104 | * using csp ([f7d2ec4](https://github.com/jctaoo/elecrun/commit/f7d2ec4c75a86de4518876c7afb457b25071278e)) 105 | * **esbuild.ts:** support ELectron prelaod ([df5833e](https://github.com/jctaoo/elecrun/commit/df5833e54dc82981cacdcd8238f1d089d3f84c27)) 106 | * **src/commands/build:** add Build Command ([9ac39c5](https://github.com/jctaoo/elecrun/commit/9ac39c55e6cb8ec4c017c7bfa1414c095997ee90)) 107 | * **src/commands/clean:** add Clean Command ([ccb474f](https://github.com/jctaoo/elecrun/commit/ccb474f7406b0ef9569cc4513092798a10fca10b)) 108 | -------------------------------------------------------------------------------- /docs/zh.html.md: -------------------------------------------------------------------------------- 1 | **elecrun** 是一个简单快速地运行你的 electron app 的工具。 2 | 3 | [![CI](https://github.com/jctaoo/elecrun/actions/workflows/CI.yml/badge.svg)](https://github.com/jctaoo/elecrun/actions/workflows/CI.yml) 4 | 5 | ## 特征 6 | 7 | - 在 [Node.js](https://nodejs.org/zh-cn/) 里编写 JavaScript, [TypeScript](https://www.typescriptlang.org/) 而不需要任何配置 8 | 9 | - 让 [Electron](https://www.electronjs.org/) 与任何前端框架一起工作 10 | 11 | - 使用 [esbuild](https://esbuild.github.io/) 转换主进程代码,非常快 ⚡️ 12 | 13 | - 在渲染进程中使用 [vite](https://cn.vitejs.dev/) 14 | 15 | ## 快速开始 16 | 17 | ### 安装 18 | 19 | - 全局安装 20 | 21 | ```shell 22 | # using npm 23 | npm install -g elecrun 24 | # using yarn 25 | yarn global add elecrun 26 | ``` 27 | 28 | - 安装到 `开发时依赖` (即项目依赖) 29 | 30 | ```shell 31 | # using npm 32 | npm install elecrun --save-dev 33 | # using yarn 34 | yarn global add elecrun --dev 35 | ``` 36 | 37 | ### 创建并运行 Electron 应用 38 | 39 | #### 开始一个新的项目 40 | 41 | ```shell 42 | # 创建项目目录 43 | mkdir my-electron-app && cd my-electron-app 44 | # 初始化项目 45 | yarn init -y 46 | # 安装 electron 作为依赖 47 | yarn add electron -D 48 | ``` 49 | 50 | #### 使用 `TypeScript` 编写 `主线程` 代码 (JavaScript 也可以) 51 | 52 | index.ts 53 | 54 | ```ts 55 | import { app, BrowserWindow } from 'electron'; 56 | 57 | function createWindow() { 58 | const win = new BrowserWindow({ 59 | width: 800, 60 | height: 600, 61 | }); 62 | win.loadURL('http://localhost:5173'); 63 | } 64 | 65 | app.whenReady().then(createWindow); 66 | ``` 67 | 68 | > 对于更多的 electron 的细节, 查看 [electron doc](https://www.electronjs.org/docs) 69 | 70 | #### 使用 `TypeScript` 编写 `渲染进程` 代码 71 | 72 | > 事实上, 你可以使用任何受 `vite` 支持的前端框架. 不过对于目前来说, 让我们使用一个简单的 html 文件. 73 | 74 | index.html 75 | 76 | ```html 77 | 78 | 79 | 80 | 81 | Electron App 82 | 83 | 84 |

你好世界

85 | 86 | 87 | ``` 88 | 89 | #### 在 `package.json` 中添加一条命令 90 | 91 | ```json 92 | { 93 | "scripts": { 94 | "dev": "elecrun --vite" 95 | } 96 | } 97 | ``` 98 | 99 | > `elecrun` 是 `elecrun` 的别名 100 | 101 | #### ⚡️ 开始运行您的 Electron 程序 102 | 103 | ```shell 104 | yarn dev 105 | ``` 106 | 107 |


截图

108 | 109 | #### 源代码 110 | 111 | - https://github.com/jctaoo/elecrun/tree/main/fixtures/demo 112 | - https://github.com/jctaoo/elecrun/tree/main/fixtures/simple 113 | 114 | ## 工作原理 115 | 116 | ### 渲染进程 117 | 118 | `elecrun` 使用 `vite` 来处理渲染进程代码 119 | 120 | `root directory` 里的 `index.html` 为渲染进程的入口(你可以指定 root directory 路径, 详见 [选项 --vite [renderer root]](#选项---vite-renderer-root)), 同时 vite 使用 [esm](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules) 来组织渲染进程的代码 121 | 122 | vite 提供一个支持模块热更新的开发服务器. 这意味着任何渲染进程的代码改变都会实时显示在界面中 123 | 124 | > 来自 vite 官方文档: 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。 125 | 126 | 对于更多 vite 的信息, 查看 [vite 官方网站](https://cn.vitejs.dev) 127 | 128 | ### 主进程 129 | 130 | `elecrun` 使用 `esbuild` 来转换也许不能被 nodejs 直接执行的代码 (比如 TypeScript 或者现代的 JavaScript 语法) 到可以被 nodejs 处理的目标代码. 除此之外, elecrun 还会将目标代码打包 (bundle) 进一个文件. 131 | 132 | 当您运行 `elecrun dev`, `elecrun` 会尝试寻找并读取入口文件(你可以指定入口文件路径, 详见 [开发阶段](#开发阶段))并将其作为主进程的入口文件并尝试静态分析它来转换您的代码, 然后, 转换后的代码会存储在 `node_modules/.elecrun` (这里有一个例外, see [options --preload](#选项---preload-file)). 代码被转换后, `elecrun` 会执行 `electron` 命令行工具来运行您的应用程序. 133 | 134 | 当您在主进程的代码被改变, `elecrun` 询问您是否要重新执行您的程序, 这在您不想打断当前的调试的情况很有用. 135 | 136 | ## 指引 137 | 138 | ### 开发阶段 139 | 140 | 运行 141 | 142 | ```shell 143 | elecrun dev --vite 144 | # or 145 | elecrun --vite 146 | ``` 147 | 148 | dev 命令的完整版本是 `elecrun [file-entry] [参数列表]`, 唯一的参数是 `file-entry`, 该参数代表了主线程的入口脚本路径. 您可以指定该参数, 若未指定, `elecrun` 会自动在以下列表依次寻找入口脚本路径: 149 | 150 | - ./src/main/index.js 151 | - ./src/main/index.ts 152 | - ./src/index.js 153 | - ./src/index.ts 154 | - ./index.js 155 | - ./index.ts 156 | 157 | 例子: 158 | ```shell 159 | elecrun dev ./main.ts 160 | ``` 161 | 162 | #### 选项 `--vite [renderer root]` 163 | 164 | `--vite` 选项一位置与 `elecrun` 一起运行 vite 服务器. 如果您不想这样, 只需去掉该选项即可. 165 | 166 | `renderer root` 代表 vite 的 root directory, 您可以指定该参数, 若未指定, `elecrun` 会自动在以下列表依次寻找 root directory: 167 | 168 | - ./src/renderer/ 169 | - ./src/ 170 | - ./ 171 | 172 | 例子: 173 | 174 | ```shell 175 | elecrun dev --vite ./src 176 | ``` 177 | 178 | #### 选项 `--preload ` 179 | 180 | 当您开启 `contextIsolation`, 可能会需要 `预加载脚本` (可以在 [BrowserWindow 的选项](https://www.electronjs.org/docs/api/browser-window#browserwindow) 找到). 但是 Electron 使用一个字符串变量来加载您的 `预加载脚本`, 这使得 `esbuild` 无法静态分析 `预加载脚本` 的位置也无法打包它. 解决方案是提供 `--preload` 选项来指定 `预加载脚本` 的路径, 然后, `elecrun` 会转换该脚本并把结果保存在与打包后的代码(bundled code)同样的目录下. 181 | 182 | `` 参数应该被设置为预加载脚本的路径, 且该路径相对于 `src/main`, 例如有如下文件结构: 183 | 184 | ``` 185 | +-src 186 | |--+-main 187 | | |---index.ts 188 | | |---preaload.ts 189 | |--+-renderer 190 | | |---index.html 191 | |--package.json 192 | ``` 193 | 194 | 运行如下命令即可 195 | 196 | ```shell 197 | elecrun --vite --preload preload.ts 198 | ``` 199 | 200 | #### 选项 `--clean-cache` 201 | 202 | `dev` 命令会存一些打包产物在 `node_modules/.elecrun/app` 下, 这条命令帮助你在开始 `dev` 前清除掉这些缓存. 203 | 204 | #### 选项 `--esm` 205 | 206 | `--esm` 选项用于指定是否使用 ESM 模块来运行主进程代码. 默认情况下, `elecrun` 会使用 `commonjs` 模块来运行主进程代码. 如果您想使用 ESM 模块, 只需添加该选项即可. 207 | 208 | > 有一些第三方库仅仅支持 `esm` 模块, 使用这样的第三方库时, 您可能需要添加该选项. 209 | 210 | ```shell 211 | 212 | ### 编译阶段 213 | 214 | 编译阶段基本与开发阶段相同(包括除了`--vite`之外的所有参数或选项), 但区别在于开发阶段的代码会存储在 `node_modules`, 而编译阶段则会存储在 app 目录下. 215 | 216 | ### 清理输出 217 | 218 | 运行 `elecrun clean` 来清理 `elecrun` 产生的输出 219 | 220 | ## 发布 221 | 222 | 当创建 GitHub Release 时,GitHub Actions 会自动处理发布。 223 | 224 | - 触发:Release created → 运行 `.github/workflows/publish.yml`。 225 | - 流程:检出、安装、构建,随后执行 `npm publish` 到 GitHub Packages。 226 | 227 | 发布步骤: 228 | 229 | 1. 使用 standard-version 提升版本(`pnpm version` 或 `pnpm version:patch|minor|major`)。 230 | 2. 推送提交与 tag。 231 | 3. 在 GitHub 上为新 tag 创建 Release;工作流会自动发布。 232 | 233 | 参考:[Publish 工作流](https://github.com/jctaoo/elecrun/blob/main/.github/workflows/publish.yml) · [CI 工作流](https://github.com/jctaoo/elecrun/blob/main/.github/workflows/CI.yml) 234 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | > 从 2.4.0 开始, 使用 `elecrun` 而不是 `elecrun` 包。 2 | 3 | **elecrun** 是一个简单快速地运行你的 electron app 的工具。 4 | 5 | [![CI](https://github.com/jctaoo/elecrun/actions/workflows/CI.yml/badge.svg)](https://github.com/jctaoo/elecrun/actions/workflows/CI.yml) 6 | 7 | ## 特征 8 | 9 | - 在 [Node.js](https://nodejs.org/zh-cn/) 里编写 JavaScript, [TypeScript](https://www.typescriptlang.org/) 而不需要任何配置 10 | 11 | - 让 [Electron](https://www.electronjs.org/) 与任何前端框架一起工作 12 | 13 | - 使用 [esbuild](https://esbuild.github.io/) 转换主进程代码,非常快 ⚡️ 14 | 15 | - 在渲染进程中使用 [vite](https://cn.vitejs.dev/) 16 | 17 | ## 快速开始 18 | 19 | ### 安装 20 | 21 | - 全局安装 22 | 23 | ```shell 24 | # using npm 25 | npm install -g elecrun 26 | # using yarn 27 | yarn global add elecrun 28 | ``` 29 | 30 | - 安装到 `开发时依赖` (即项目依赖) 31 | 32 | ```shell 33 | # using npm 34 | npm install elecrun --save-dev 35 | # using yarn 36 | yarn global add elecrun --dev 37 | ``` 38 | 39 | ### 创建并运行 Electron 应用 40 | 41 | #### 开始一个新的项目 42 | 43 | ```shell 44 | # 创建项目目录 45 | mkdir my-electron-app && cd my-electron-app 46 | # 初始化项目 47 | yarn init -y 48 | # 安装 electron 作为依赖 49 | yarn add electron -D 50 | ``` 51 | 52 | #### 使用 `TypeScript` 编写 `主线程` 代码 (JavaScript 也可以) 53 | 54 | index.ts 55 | 56 | ```ts 57 | import { app, BrowserWindow } from 'electron'; 58 | 59 | function createWindow() { 60 | const win = new BrowserWindow({ 61 | width: 800, 62 | height: 600, 63 | }); 64 | win.loadURL('http://localhost:5173'); 65 | } 66 | 67 | app.whenReady().then(createWindow); 68 | ``` 69 | 70 | > 对于更多的 electron 的细节, 查看 [electron doc](https://www.electronjs.org/docs) 71 | 72 | #### 使用 `TypeScript` 编写 `渲染进程` 代码 73 | 74 | > 事实上, 你可以使用任何受 `vite` 支持的前端框架. 不过对于目前来说, 让我们使用一个简单的 html 文件. 75 | 76 | index.html 77 | 78 | ```html 79 | 80 | 81 | 82 | 83 | Electron App 84 | 85 | 86 |

你好世界

87 | 88 | 89 | ``` 90 | 91 | #### 在 `package.json` 中添加一条命令 92 | 93 | ```json 94 | { 95 | "scripts": { 96 | "dev": "elecrun --vite" 97 | } 98 | } 99 | ``` 100 | 101 | > `elecrun` 是 `elecrun` 的别名 102 | 103 | #### ⚡️ 开始运行您的 Electron 程序 104 | 105 | ```shell 106 | yarn dev 107 | ``` 108 | 109 |


截图

110 | 111 | #### 源代码 112 | 113 | - https://github.com/jctaoo/elecrun/tree/main/fixtures/demo 114 | - https://github.com/jctaoo/elecrun/tree/main/fixtures/simple 115 | 116 | ## 工作原理 117 | 118 | ### 渲染进程 119 | 120 | `elecrun` 使用 `vite` 来处理渲染进程代码 121 | 122 | `root directory` 里的 `index.html` 为渲染进程的入口(你可以指定 root directory 路径, 详见 [选项 --vite [renderer root]](#选项---vite-renderer-root)), 同时 vite 使用 [esm](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules) 来组织渲染进程的代码 123 | 124 | vite 提供一个支持模块热更新的开发服务器. 这意味着任何渲染进程的代码改变都会实时显示在界面中 125 | 126 | > 来自 vite 官方文档: 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。 127 | 128 | 对于更多 vite 的信息, 查看 [vite 官方网站](https://cn.vitejs.dev) 129 | 130 | ### 主进程 131 | 132 | `elecrun` 使用 `esbuild` 来转换也许不能被 nodejs 直接执行的代码 (比如 TypeScript 或者现代的 JavaScript 语法) 到可以被 nodejs 处理的目标代码. 除此之外, elecrun 还会将目标代码打包 (bundle) 进一个文件. 133 | 134 | 当您运行 `elecrun dev`, `elecrun` 会尝试寻找并读取入口文件(你可以指定入口文件路径, 详见 [开发阶段](#开发阶段))并将其作为主进程的入口文件并尝试静态分析它来转换您的代码, 然后, 转换后的代码会存储在 `node_modules/.elecrun` (这里有一个例外, see [options --preload](#选项---preload-file)). 代码被转换后, `elecrun` 会执行 `electron` 命令行工具来运行您的应用程序. 135 | 136 | 当您在主进程的代码被改变, `elecrun` 询问您是否要重新执行您的程序, 这在您不想打断当前的调试的情况很有用. 137 | 138 | ## 指引 139 | 140 | ### 开发阶段 141 | 142 | 运行 143 | 144 | ```shell 145 | elecrun dev --vite 146 | # or 147 | elecrun --vite 148 | ``` 149 | 150 | dev 命令的完整版本是 `elecrun [file-entry] [参数列表]`, 唯一的参数是 `file-entry`, 该参数代表了主线程的入口脚本路径. 您可以指定该参数, 若未指定, `elecrun` 会自动在以下列表依次寻找入口脚本路径: 151 | 152 | - ./src/main/index.js 153 | - ./src/main/index.ts 154 | - ./src/index.js 155 | - ./src/index.ts 156 | - ./index.js 157 | - ./index.ts 158 | 159 | 例子: 160 | ```shell 161 | elecrun dev ./main.ts 162 | ``` 163 | 164 | #### 选项 `--vite [renderer root]` 165 | 166 | `--vite` 选项一位置与 `elecrun` 一起运行 vite 服务器. 如果您不想这样, 只需去掉该选项即可. 167 | 168 | `renderer root` 代表 vite 的 root directory, 您可以指定该参数, 若未指定, `elecrun` 会自动在以下列表依次寻找 root directory: 169 | 170 | - ./src/renderer/ 171 | - ./src/ 172 | - ./ 173 | 174 | 例子: 175 | 176 | ```shell 177 | elecrun dev --vite ./src 178 | ``` 179 | 180 | #### 选项 `--preload ` 181 | 182 | 当您开启 `contextIsolation`, 可能会需要 `预加载脚本` (可以在 [BrowserWindow 的选项](https://www.electronjs.org/docs/api/browser-window#browserwindow) 找到). 但是 Electron 使用一个字符串变量来加载您的 `预加载脚本`, 这使得 `esbuild` 无法静态分析 `预加载脚本` 的位置也无法打包它. 解决方案是提供 `--preload` 选项来指定 `预加载脚本` 的路径, 然后, `elecrun` 会转换该脚本并把结果保存在与打包后的代码(bundled code)同样的目录下. 183 | 184 | `` 参数应该被设置为预加载脚本的路径, 且该路径相对于 `src/main`, 例如有如下文件结构: 185 | 186 | ``` 187 | +-src 188 | |--+-main 189 | | |---index.ts 190 | | |---preaload.ts 191 | |--+-renderer 192 | | |---index.html 193 | |--package.json 194 | ``` 195 | 196 | 运行如下命令即可 197 | 198 | ```shell 199 | elecrun --vite --preload preload.ts 200 | ``` 201 | 202 | #### 选项 `--clean-cache` 203 | 204 | `dev` 命令会存一些打包产物在 `node_modules/.elecrun/app` 下, 这条命令帮助你在开始 `dev` 前清除掉这些缓存. 205 | 206 | #### 选项 `--esm` 207 | 208 | `--esm` 选项用于指定是否使用 ESM 模块来运行主进程代码. 默认情况下, `elecrun` 会使用 `commonjs` 模块来运行主进程代码. 如果您想使用 ESM 模块, 只需添加该选项即可. 209 | 210 | > 有一些第三方库仅仅支持 `esm` 模块, 使用这样的第三方库时, 您可能需要添加该选项. 211 | 212 | ```shell 213 | 214 | ### 编译阶段 215 | 216 | 编译阶段基本与开发阶段相同(包括除了`--vite`之外的所有参数或选项), 但区别在于开发阶段的代码会存储在 `node_modules`, 而编译阶段则会存储在 app 目录下. 217 | 218 | ### 清理输出 219 | 220 | 运行 `elecrun clean` 来清理 `elecrun` 产生的输出 221 | 222 | ## 发布 223 | 224 | 发布流程现已由 GitHub Actions 自动完成。 225 | 226 | - 触发方式:创建 GitHub Release(事件类型:created)会运行 `.github/workflows/publish.yml`。 227 | - 执行内容:检出仓库、安装依赖、构建,然后执行 `npm publish` 发布到 GitHub Packages。 228 | 229 | 如何发布新版本: 230 | 231 | 1. 使用 standard-version 升级版本并生成变更日志: 232 | - `pnpm version`(交互式)或 `pnpm version:major | version:minor | version:patch` 233 | 2. 将提交和 tag 推送到 GitHub。 234 | 3. 在 GitHub 上为新 tag 创建 Release。发布工作流会自动构建并发布。 235 | 236 | 参考:[Publish 工作流](https://github.com/jctaoo/elecrun/blob/main/.github/workflows/publish.yml) · [CI 工作流](https://github.com/jctaoo/elecrun/blob/main/.github/workflows/CI.yml) 237 | -------------------------------------------------------------------------------- /src/commands/esbuild.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Run main process using ESBuild 3 | */ 4 | 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | 8 | import { yellow } from 'colorette'; 9 | import { BuildFailure, BuildOptions } from 'esbuild'; 10 | 11 | import { 12 | CompileError, 13 | notFoundESBuildConfig, 14 | notFoundPackageJson, 15 | PathManager, 16 | warnPreloadMessage, 17 | } from '../common'; 18 | import { MainCommand } from '../types'; 19 | import { exists, readJson } from '../utils'; 20 | 21 | function transformErrors(error: BuildFailure): CompileError[] { 22 | return error.errors.map((e): CompileError => { 23 | return { 24 | location: e.location, 25 | message: e.text, 26 | }; 27 | }); 28 | } 29 | 30 | async function findExternal(): Promise { 31 | // find package.json 32 | if (!(await exists(PathManager.shard.packageJsonPath))) { 33 | notFoundPackageJson(); 34 | } 35 | 36 | const externals: Set = new Set(); 37 | const keys = ['dependencies', 'devDependencies', 'peerDependencies']; 38 | const pkg = await readJson(PathManager.shard.packageJsonPath); 39 | 40 | for (const key of keys) { 41 | const obj = pkg[key] ?? {}; 42 | for (const name of Object.keys(obj)) { 43 | externals.add(name); 44 | } 45 | } 46 | 47 | // find node_modules 48 | if (await exists(PathManager.shard.nodeModulesPath)) { 49 | const children = await fs.promises.readdir( 50 | PathManager.shard.nodeModulesPath, 51 | ); 52 | for (const child of children) { 53 | externals.add(child); 54 | } 55 | } 56 | 57 | return Array.from(externals); 58 | } 59 | 60 | /** When provided with a filename, loads the esbuild js config from the file as a default export */ 61 | export const loadESBuildConfigFromFile = async ( 62 | file?: string, 63 | ): Promise> => { 64 | // No file provided 65 | if (!file) return {}; 66 | 67 | const esbuildConfigPath = path.join(PathManager.shard.cwd, file); 68 | 69 | // File provided but does not exist. 70 | if (!fs.existsSync(esbuildConfigPath)) { 71 | notFoundESBuildConfig(); 72 | return {}; 73 | } 74 | 75 | try { 76 | // eslint-disable-next-line @typescript-eslint/no-require-imports 77 | return require(esbuildConfigPath); 78 | } catch (e) { 79 | // File exists but could not be loaded 80 | console.error('Could not load provided esbuild config file, ignoring'); 81 | console.error(e); 82 | } 83 | return {}; 84 | }; 85 | 86 | /** Attempt to return esbuild from the project, if it exists */ 87 | const findESBuildForProject = async () => { 88 | const esBuildPath = path.join(PathManager.shard.nodeModulesPath, 'esbuild'); 89 | if (fs.existsSync(esBuildPath)) { 90 | console.log('Using esbuild from ', esBuildPath); 91 | // eslint-disable-next-line @typescript-eslint/no-require-imports 92 | return require(esBuildPath); 93 | } else { 94 | // eslint-disable-next-line @typescript-eslint/no-require-imports 95 | return require('esbuild'); 96 | } 97 | }; 98 | 99 | const writeOutDirPackageJson = async (esm: boolean) => { 100 | const packageJsonPath = PathManager.shard.devOutDirPackageJson; 101 | 102 | // Read user's project package.json to get the project name 103 | let projectName: string | null = null; 104 | try { 105 | const userPackageJson = await readJson(PathManager.shard.packageJsonPath); 106 | if (userPackageJson.name) { 107 | projectName = userPackageJson.name; 108 | } 109 | } catch (error) { 110 | // If we can't read the user's package.json 111 | console.error( 112 | "Could not read user's package.json at ", 113 | PathManager.shard.packageJsonPath, 114 | ); 115 | console.error(error); 116 | } 117 | 118 | const packageJson: Record = { 119 | type: esm ? 'module' : 'commonjs', 120 | }; 121 | if (projectName) { 122 | packageJson.name = projectName; 123 | } 124 | 125 | await fs.promises.writeFile( 126 | packageJsonPath, 127 | JSON.stringify(packageJson, null, 2), 128 | ); 129 | }; 130 | 131 | export const runESBuildForMainProcess: MainCommand = async ( 132 | { isBuild, outDir, preloadScript, entryPath, esbuildConfigFile, format }, 133 | reportError, 134 | _buildStart, 135 | buildComplete, 136 | notFoundTSConfig, 137 | ) => { 138 | const esbuild = await findESBuildForProject(); 139 | 140 | // Load esbuild config file supplied by user 141 | const esbuildConfigExtra = await loadESBuildConfigFromFile(esbuildConfigFile); 142 | 143 | let tsconfigPath = path.join(PathManager.shard.mainPath, 'tsconfig.json'); 144 | if (!fs.existsSync(tsconfigPath)) { 145 | tsconfigPath = await notFoundTSConfig(); 146 | } 147 | 148 | let count = 0; 149 | const externals = await findExternal(); 150 | 151 | const entryPoints = [entryPath]; 152 | if (preloadScript) { 153 | if (!/^.*\.(js|ts|jsx|tsx)$/.test(preloadScript)) { 154 | console.log(yellow(`${warnPreloadMessage} ${preloadScript}`)); 155 | } 156 | const preloadScriptPath = path.join( 157 | PathManager.shard.mainPath, 158 | preloadScript, 159 | ); 160 | if (await exists(preloadScriptPath)) { 161 | // entryPoints.push(preloadScriptPath); 162 | // should buid preload script standalone 163 | 164 | PathManager.shard.setPreloadScriptPath(preloadScriptPath); 165 | } 166 | } 167 | 168 | const commonEsbuildConfig = { 169 | tsconfig: tsconfigPath, 170 | logLevel: 'silent', 171 | logLimit: 0, 172 | incremental: !isBuild, 173 | platform: 'node', 174 | sourcemap: true, 175 | bundle: true, 176 | external: externals, 177 | }; 178 | 179 | try { 180 | // build main process 181 | await esbuild.build({ 182 | outdir: outDir, 183 | format: format, 184 | entryPoints: entryPoints, 185 | ...commonEsbuildConfig, 186 | watch: !isBuild 187 | ? { 188 | onRebuild: async (error: BuildFailure) => { 189 | if (error) { 190 | reportError(...transformErrors(error)); 191 | } else { 192 | count++; 193 | buildComplete(outDir, count); 194 | } 195 | }, 196 | } 197 | : false, 198 | ...esbuildConfigExtra, 199 | }); 200 | // build preload script 201 | if ( 202 | PathManager.shard.preloadScriptPath && 203 | (await exists(PathManager.shard.preloadScriptPath)) 204 | ) { 205 | await esbuild.build({ 206 | outfile: path.join(outDir, 'preload.cjs'), 207 | format: 'cjs', 208 | entryPoints: [PathManager.shard.preloadScriptPath], 209 | ...commonEsbuildConfig, 210 | watch: !isBuild 211 | ? { 212 | onRebuild: async (error: BuildFailure) => { 213 | if (error) { 214 | reportError(...transformErrors(error)); 215 | } else { 216 | count++; 217 | buildComplete(outDir, count); 218 | } 219 | }, 220 | } 221 | : false, 222 | ...esbuildConfigExtra, 223 | }); 224 | } 225 | 226 | count++; 227 | buildComplete(outDir, count); 228 | await writeOutDirPackageJson(format === 'esm'); 229 | } catch (error) { 230 | const e = error as BuildFailure; 231 | if (!!e.errors && !!e.errors.length && e.errors.length > 0) { 232 | reportError(...transformErrors(e)); 233 | } 234 | } 235 | }; 236 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | **elecrun** is a tool to run your electron app easily. 2 | 3 | [![CI](https://github.com/jctaoo/elecrun/actions/workflows/CI.yml/badge.svg)](https://github.com/jctaoo/elecrun/actions/workflows/CI.yml) 4 | 5 | ## Features 6 | 7 | - Write modern JavaScript, [TypeScript](https://www.typescriptlang.org/) in [Node.js](https://nodejs.org/en/) with no config. 8 | 9 | - Let [Electron](https://www.electronjs.org/) work with any front-end framework. 10 | 11 | - Using [esbuild](https://esbuild.github.io/) to transform your main process code, It's very fast ⚡️. 12 | 13 | - Using [vite](https://vitejs.dev/) in renderer process. 14 | 15 | ## Quick Start 16 | 17 | ### Installation 18 | 19 | - Globally install 20 | 21 | ```shell 22 | # using npm 23 | npm install -g elecrun 24 | # using yarn 25 | yarn global add elecrun 26 | ``` 27 | 28 | - Install as devDependencies 29 | 30 | ```shell 31 | # using npm 32 | npm install elecrun --save-dev 33 | # using yarn 34 | yarn global add elecrun --dev 35 | ``` 36 | 37 | ### Create & Run electron app 38 | 39 | #### Start a new project 40 | 41 | ```shell 42 | # create project directory 43 | mkdir my-electron-app && cd my-electron-app 44 | # initialize your project 45 | yarn init -y 46 | # install electron as dependencies 47 | yarn add electron -D 48 | ``` 49 | 50 | #### Write your `main process` code in `TypeScript` (JavaScript is also ok) 51 | 52 | index.ts 53 | 54 | ```ts 55 | import { app, BrowserWindow } from 'electron'; 56 | 57 | function createWindow() { 58 | const win = new BrowserWindow({ 59 | width: 800, 60 | height: 600, 61 | }); 62 | win.loadURL('http://localhost:5173'); 63 | } 64 | 65 | app.whenReady().then(createWindow); 66 | ``` 67 | 68 | > For more information about Electron, see [electron doc](https://www.electronjs.org/docs) 69 | 70 | #### Write your `renderer process` code in `TypeScript`. 71 | 72 | > Actually, you can use any front-end framework supported by `vite` here. In a simple project, let's use a single html file. 73 | 74 | index.html 75 | 76 | ```html 77 | 78 | 79 | 80 | 81 | Electron App 82 | 83 | 84 |

Hello World

85 | 86 | 87 | ``` 88 | 89 | #### Add a script in `package.json`. 90 | 91 | ```json 92 | { 93 | "scripts": { 94 | "dev": "elecrun --vite" 95 | } 96 | } 97 | ``` 98 | 99 | > `elecrun` is alias of `elecrun` 100 | 101 | #### ⚡️ Start your electron app 102 | 103 | ```shell 104 | yarn dev 105 | ``` 106 | 107 |


screen shot

108 | 109 | #### Source codes 110 | 111 | - https://github.com/jctaoo/elecrun/tree/main/fixtures/demo 112 | - https://github.com/jctaoo/elecrun/tree/main/fixtures/simple 113 | 114 | ## How it works 115 | 116 | ### Renderer Process 117 | 118 | `elecrun` using `vite` to handle code in renderer process. 119 | 120 | The entry file is `index.html` in `root directory`(You can specify the root directory path, see [options --vite](#options---vite-renderer-root)) and vite using [esm](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules) to struct your renderer process code. 121 | 122 | Vite also provides a dev server support `Hot Module Replacement`. It's means your code changes can always be displayed on the interface. 123 | 124 | > From vite official website : A dev server that provides rich feature enhancements over native ES modules, for example extremely fast Hot Module Replacement (HMR). 125 | 126 | For more information, see [vite official website](https://vitejs.dev) 127 | 128 | ### Main Process 129 | 130 | `elecrun` using `esbuild` to transform your code may cannot directly run in nodejs such as TypeScript and modern JavaScript to the code nodejs can handle. Besides, `elecrun` also bundle your code to one file. 131 | 132 | When you run `elecrun dev`, `elecrun` will try to find and read entry file(You can specify the entry file path, see [development phase](#development-phase)) then statically analyze to transform your code. After that, save the target code to your `node_modules/.elecrun` (there is one exception, see [options --preload](#options---preload-file)). Finally, `elecrun` will execute `electron` command line tool to start your app. 133 | 134 | When your main process code has been changed, `elecrun` will ask if you want to rerun your app. This is useful when you don’t want to interrupt the current debugging. 135 | 136 | ## Guide 137 | 138 | ### development phase 139 | 140 | run 141 | 142 | ```shell 143 | elecrun dev --vite 144 | # or 145 | elecrun --vite 146 | ``` 147 | 148 | The full version of dev command is `elecrun [file-entry] [options]`. The only argument is `file-entry` that indicates the path of entry script for main process. You can specify this or `elecrun` will automatically find the entry script path by the following list: 149 | 150 | - ./src/main/index.js 151 | - ./src/main/index.ts 152 | - ./src/index.js 153 | - ./src/index.ts 154 | - ./index.js 155 | - ./index.ts 156 | 157 | example: 158 | 159 | ```shell 160 | elecrun dev ./main.ts 161 | ``` 162 | 163 | #### options `--vite [renderer root]` 164 | 165 | The option `--vite` means run vite server with `elecrun`. If you don't want using `vite`, just remove this option. 166 | 167 | The 'renderer root' is the root directory for vite. You can specify this or `elecrun` 168 | will automatically find the root directory by the following list: 169 | 170 | - ./src/renderer/ 171 | - ./src/ 172 | - ./ 173 | 174 | example: 175 | 176 | ```shell 177 | elecrun dev --vite ./src 178 | ``` 179 | 180 | #### options `--preload ` 181 | 182 | When you enable `contextIsolation`, you may need `preload` (You can find in [BrowserWindow options](https://www.electronjs.org/docs/api/browser-window#browserwindow)). But Electron loads your preload script based on string variable. It's means `esbuild` cannot statically analyze the location of preload script or bundle it. The solution is to provide an option `--preload` to specify location of preload script. Then, `elecrun` just transform it and save preload code's target code in the same path as bundled code. 183 | 184 | The parameter `` should be set as preload script path relative to the main src. Example: 185 | 186 | ``` 187 | +-src 188 | |--+-main 189 | | |---index.ts 190 | | |---preaload.ts 191 | |--+-renderer 192 | | |---index.html 193 | |--package.json 194 | ``` 195 | 196 | run 197 | 198 | ```shell 199 | elecrun --vite --preload preload.ts 200 | ``` 201 | 202 | #### option `--clean-cache` 203 | 204 | `dev` command save the build artifact to `node_modules/.elecrun/app` under your project by default. But sometimes you want to clean these files. This options help you clean cache files when you run `dev` command. 205 | 206 | #### options `--esm` 207 | 208 | The `--esm` option is used to specify whether to use ESM modules to run the main process code. By default, `elecrun` uses `commonjs` modules to run the main process code. If you want to use ESM modules, just add this option. 209 | 210 | > Some third-party libraries only support `esm` modules. When using such third-party libraries, you may need to add this option. 211 | 212 | ### build phase 213 | 214 | The build phase is almost the same as the development phase (also including all the options and arguments except `--vite`). The difference is that the compiled files are stored in `node_modules` in the development phase, while the build phase is stored in the app directory. 215 | 216 | ### clean output 217 | 218 | run `elecrun clean` to easily clean output by `elecrun` 219 | 220 | ## Publishing 221 | 222 | Publishing is handled automatically by GitHub Actions when a GitHub Release is created. 223 | 224 | - Trigger: Release created → runs `.github/workflows/publish.yml`. 225 | - Steps: checkout, install, build, then `npm publish` to GitHub Packages. 226 | 227 | To publish: 228 | 229 | 1. Bump version via standard-version (`pnpm version` or `pnpm version:patch|minor|major`). 230 | 2. Push commits and tags. 231 | 3. Create a GitHub Release for the new tag; the workflow will publish automatically. 232 | 233 | References: [Publish workflow](https://github.com/jctaoo/elecrun/blob/main/.github/workflows/publish.yml) · [CI workflow](https://github.com/jctaoo/elecrun/blob/main/.github/workflows/CI.yml) 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > From version 2.4.0, using package `elecrun` instead of `elecrun`. 2 | 3 | **elecrun** is a tool to run your electron app easily. 4 | 5 | [![CI](https://github.com/jctaoo/elecrun/actions/workflows/CI.yml/badge.svg)](https://github.com/jctaoo/elecrun/actions/workflows/CI.yml) 6 | 7 | ## Features 8 | 9 | - Write modern JavaScript, [TypeScript](https://www.typescriptlang.org/) in [Node.js](https://nodejs.org/en/) with no config. 10 | 11 | - Let [Electron](https://www.electronjs.org/) work with any front-end framework. 12 | 13 | - Using [esbuild](https://esbuild.github.io/) to transform your main process code, It's very fast ⚡️. 14 | 15 | - Using [vite](https://vitejs.dev/) in renderer process. 16 | 17 | ## Quick Start 18 | 19 | ### Installation 20 | 21 | - Globally install 22 | 23 | ```shell 24 | # using npm 25 | npm install -g elecrun 26 | # using yarn 27 | yarn global add elecrun 28 | ``` 29 | 30 | - Install as devDependencies 31 | 32 | ```shell 33 | # using npm 34 | npm install elecrun --save-dev 35 | # using yarn 36 | yarn global add elecrun --dev 37 | ``` 38 | 39 | ### Create & Run electron app 40 | 41 | #### Start a new project 42 | 43 | ```shell 44 | # create project directory 45 | mkdir my-electron-app && cd my-electron-app 46 | # initialize your project 47 | yarn init -y 48 | # install electron as dependencies 49 | yarn add electron -D 50 | ``` 51 | 52 | #### Write your `main process` code in `TypeScript` (JavaScript is also ok) 53 | 54 | index.ts 55 | 56 | ```ts 57 | import { app, BrowserWindow } from 'electron'; 58 | 59 | function createWindow() { 60 | const win = new BrowserWindow({ 61 | width: 800, 62 | height: 600, 63 | }); 64 | win.loadURL('http://localhost:5173'); 65 | } 66 | 67 | app.whenReady().then(createWindow); 68 | ``` 69 | 70 | > For more information about Electron, see [electron doc](https://www.electronjs.org/docs) 71 | 72 | #### Write your `renderer process` code in `TypeScript`. 73 | 74 | > Actually, you can use any front-end framework supported by `vite` here. In a simple project, let's use a single html file. 75 | 76 | index.html 77 | 78 | ```html 79 | 80 | 81 | 82 | 83 | Electron App 84 | 85 | 86 |

Hello World

87 | 88 | 89 | ``` 90 | 91 | #### Add a script in `package.json`. 92 | 93 | ```json 94 | { 95 | "scripts": { 96 | "dev": "elecrun --vite" 97 | } 98 | } 99 | ``` 100 | 101 | > `elecrun` is alias of `elecrun` 102 | 103 | #### ⚡️ Start your electron app 104 | 105 | ```shell 106 | yarn dev 107 | ``` 108 | 109 |


screen shot

110 | 111 | #### Source codes 112 | 113 | - https://github.com/jctaoo/elecrun/tree/main/fixtures/demo 114 | - https://github.com/jctaoo/elecrun/tree/main/fixtures/simple 115 | 116 | ## How it works 117 | 118 | ### Renderer Process 119 | 120 | `elecrun` using `vite` to handle code in renderer process. 121 | 122 | The entry file is `index.html` in `root directory`(You can specify the root directory path, see [options --vite](#options---vite-renderer-root)) and vite using [esm](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules) to struct your renderer process code. 123 | 124 | Vite also provides a dev server support `Hot Module Replacement`. It's means your code changes can always be displayed on the interface. 125 | 126 | > From vite official website : A dev server that provides rich feature enhancements over native ES modules, for example extremely fast Hot Module Replacement (HMR). 127 | 128 | For more information, see [vite official website](https://vitejs.dev) 129 | 130 | ### Main Process 131 | 132 | `elecrun` using `esbuild` to transform your code may cannot directly run in nodejs such as TypeScript and modern JavaScript to the code nodejs can handle. Besides, `elecrun` also bundle your code to one file. 133 | 134 | When you run `elecrun dev`, `elecrun` will try to find and read entry file(You can specify the entry file path, see [development phase](#development-phase)) then statically analyze to transform your code. After that, save the target code to your `node_modules/.elecrun` (there is one exception, see [options --preload](#options---preload-file)). Finally, `elecrun` will execute `electron` command line tool to start your app. 135 | 136 | When your main process code has been changed, `elecrun` will ask if you want to rerun your app. This is useful when you don’t want to interrupt the current debugging. 137 | 138 | ## Guide 139 | 140 | ### development phase 141 | 142 | run 143 | 144 | ```shell 145 | elecrun dev --vite 146 | # or 147 | elecrun --vite 148 | ``` 149 | 150 | The full version of dev command is `elecrun [file-entry] [options]`. The only argument is `file-entry` that indicates the path of entry script for main process. You can specify this or `elecrun` will automatically find the entry script path by the following list: 151 | 152 | - ./src/main/index.js 153 | - ./src/main/index.ts 154 | - ./src/index.js 155 | - ./src/index.ts 156 | - ./index.js 157 | - ./index.ts 158 | 159 | example: 160 | 161 | ```shell 162 | elecrun dev ./main.ts 163 | ``` 164 | 165 | #### options `--vite [renderer root]` 166 | 167 | The option `--vite` means run vite server with `elecrun`. If you don't want using `vite`, just remove this option. 168 | 169 | The 'renderer root' is the root directory for vite. You can specify this or `elecrun` 170 | will automatically find the root directory by the following list: 171 | 172 | - ./src/renderer/ 173 | - ./src/ 174 | - ./ 175 | 176 | example: 177 | 178 | ```shell 179 | elecrun dev --vite ./src 180 | ``` 181 | 182 | #### options `--preload ` 183 | 184 | When you enable `contextIsolation`, you may need `preload` (You can find in [BrowserWindow options](https://www.electronjs.org/docs/api/browser-window#browserwindow)). But Electron loads your preload script based on string variable. It's means `esbuild` cannot statically analyze the location of preload script or bundle it. The solution is to provide an option `--preload` to specify location of preload script. Then, `elecrun` just transform it and save preload code's target code in the same path as bundled code. 185 | 186 | The parameter `` should be set as preload script path relative to the main src. Example: 187 | 188 | ``` 189 | +-src 190 | |--+-main 191 | | |---index.ts 192 | | |---preaload.ts 193 | |--+-renderer 194 | | |---index.html 195 | |--package.json 196 | ``` 197 | 198 | run 199 | 200 | ```shell 201 | elecrun --vite --preload preload.ts 202 | ``` 203 | 204 | #### option `--clean-cache` 205 | 206 | `dev` command save the build artifact to `node_modules/.elecrun/app` under your project by default. But sometimes you want to clean these files. This options help you clean cache files when you run `dev` command. 207 | 208 | #### options `--esm` 209 | 210 | The `--esm` option is used to specify whether to use ESM modules to run the main process code. By default, `elecrun` uses `commonjs` modules to run the main process code. If you want to use ESM modules, just add this option. 211 | 212 | > Some third-party libraries only support `esm` modules. When using such third-party libraries, you may need to add this option. 213 | 214 | ### build phase 215 | 216 | The build phase is almost the same as the development phase (also including all the options and arguments except `--vite`). The difference is that the compiled files are stored in `node_modules` in the development phase, while the build phase is stored in the app directory. 217 | 218 | ### clean output 219 | 220 | run `elecrun clean` to easily clean output by `elecrun` 221 | 222 | ## Release & Publish 223 | 224 | Publishing is automated by GitHub Actions. 225 | 226 | - Trigger: creating a GitHub Release (event type: created) runs the workflow at `.github/workflows/publish.yml`. 227 | - What it does: checks out the repo, installs deps, builds, then runs `npm publish` to GitHub Packages. 228 | 229 | How to publish a new version: 230 | 231 | 1. Bump version and generate changelog using standard-version: 232 | - `pnpm version` (interactive) or `pnpm version:major | version:minor | version:patch` 233 | 2. Push commits and tags to GitHub. 234 | 3. Create a GitHub Release for the new tag. The publish workflow will build and publish automatically. 235 | 236 | References: [Publish workflow](https://github.com/jctaoo/elecrun/blob/main/.github/workflows/publish.yml) · [CI workflow](https://github.com/jctaoo/elecrun/blob/main/.github/workflows/CI.yml) 237 | --------------------------------------------------------------------------------