diff --git a/package.json b/package.json index 262ee23eab..79b6d82d06 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "lint:fix": "prettier --write .", "link:prepare": "lerna exec -- node ../../../tools/silent.js yarn link --silent --no-bin-links --link-folder ../../../.links", "link:remove": "lerna exec -- node ../../../tools/silent.js yarn unlink --silent --no-bin-links --link-folder ../../../.links", - "test": "xvfb-maybe cross-env LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TS_NODE_FILES=1 mocha './tools/test-globber.ts'", - "test:fast": "xvfb-maybe cross-env LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TEST_FAST_ONLY=1 TS_NODE_FILES=1 mocha './tools/test-globber.ts'", - "test:slow": "xvfb-maybe cross-env LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TEST_SLOW_ONLY=1 TS_NODE_FILES=1 mocha './tools/test-globber.ts'", + "test": "xvfb-maybe cross-env NODE_ENV=test LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TS_NODE_FILES=1 mocha './tools/test-globber.ts'", + "test:fast": "xvfb-maybe cross-env NODE_ENV=test LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TEST_FAST_ONLY=1 TS_NODE_FILES=1 mocha './tools/test-globber.ts'", + "test:slow": "xvfb-maybe cross-env NODE_ENV=test LINK_FORGE_DEPENDENCIES_ON_INIT=1 TS_NODE_PROJECT='./tsconfig.test.json' TEST_SLOW_ONLY=1 TS_NODE_FILES=1 mocha './tools/test-globber.ts'", "postinstall": "rimraf node_modules/.bin/*.ps1 && ts-node ./tools/gen-tsconfigs.ts && ts-node ./tools/gen-ts-glue.ts", "prepare": "husky install", "preversion": "yarn build" diff --git a/packages/template/vite-typescript/.eslintignore b/packages/template/vite-typescript/.eslintignore new file mode 100644 index 0000000000..14e485a5bc --- /dev/null +++ b/packages/template/vite-typescript/.eslintignore @@ -0,0 +1 @@ +tmpl diff --git a/packages/template/vite-typescript/package.json b/packages/template/vite-typescript/package.json new file mode 100644 index 0000000000..a91d1ee4f2 --- /dev/null +++ b/packages/template/vite-typescript/package.json @@ -0,0 +1,36 @@ +{ + "name": "@electron-forge/template-vite-typescript", + "version": "6.3.0", + "description": "Vite-TypeScript template for Electron Forge, gets you started with Vite really quickly", + "repository": { + "type": "git", + "url": "https://github.com/electron/forge", + "directory": "packages/template/vite-typescript" + }, + "author": "caoxiemeihao", + "license": "MIT", + "main": "dist/ViteTypeScriptTemplate.js", + "typings": "dist/ViteTypeScriptTemplate.d.ts", + "scripts": { + "test": "mocha --config ../../../.mocharc.js test/**/*_spec_slow.ts" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "dependencies": { + "@electron-forge/shared-types": "6.3.0", + "@electron-forge/template-base": "6.3.0", + "fs-extra": "^10.0.0" + }, + "devDependencies": { + "@electron-forge/core-utils": "6.3.0", + "@electron-forge/test-utils": "6.3.0", + "chai": "^4.3.3", + "fast-glob": "^3.2.7" + }, + "files": [ + "dist", + "src", + "tmpl" + ] +} diff --git a/packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts b/packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts new file mode 100644 index 0000000000..34c94311ff --- /dev/null +++ b/packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts @@ -0,0 +1,72 @@ +import path from 'path'; + +import { ForgeListrTaskDefinition, InitTemplateOptions } from '@electron-forge/shared-types'; +import { BaseTemplate } from '@electron-forge/template-base'; +import fs from 'fs-extra'; + +class ViteTypeScriptTemplate extends BaseTemplate { + public templateDir = path.resolve(__dirname, '..', 'tmpl'); + + public async initializeTemplate(directory: string, options: InitTemplateOptions): Promise { + const superTasks = await super.initializeTemplate(directory, options); + return [ + ...superTasks, + { + title: 'Setting up Forge configuration', + task: async () => { + await this.copyTemplateFile(directory, 'forge.config.ts'); + await fs.remove(path.resolve(directory, 'forge.config.js')); + }, + }, + { + title: 'Preparing TypeScript files and configuration', + task: async () => { + const filePath = (fileName: string) => path.join(directory, 'src', fileName); + + // Copy Vite files + await this.copyTemplateFile(directory, 'vite.main.config.ts'); + await this.copyTemplateFile(directory, 'vite.renderer.config.ts'); + await this.copyTemplateFile(directory, 'vite.preload.config.ts'); + + // Copy tsconfig with a small set of presets + await this.copyTemplateFile(directory, 'tsconfig.json'); + + // Copy eslint config with recommended settings + await this.copyTemplateFile(directory, '.eslintrc.json'); + + // Remove index.js and replace with main.ts + await fs.remove(filePath('index.js')); + await this.copyTemplateFile(path.join(directory, 'src'), 'main.ts'); + + await this.copyTemplateFile(path.join(directory, 'src'), 'renderer.ts'); + await this.copyTemplateFile(path.join(directory, 'src'), 'types.d.ts'); + + // Remove preload.js and replace with preload.ts + await fs.remove(filePath('preload.js')); + await this.copyTemplateFile(path.join(directory, 'src'), 'preload.ts'); + + // TODO: Compatible with any path entry. + // Vite uses index.html under the root path as the entry point. + await fs.move(filePath('index.html'), path.join(directory, 'index.html')); + await this.updateFileByLine(path.join(directory, 'index.html'), (line) => { + if (line.includes('link rel="stylesheet"')) return ''; + if (line.includes('')) return ' \n '; + return line; + }); + + // update package.json + const packageJSONPath = path.resolve(directory, 'package.json'); + const packageJSON = await fs.readJson(packageJSONPath); + packageJSON.main = '.vite/build/main.js'; + // Configure scripts for TS template + packageJSON.scripts.lint = 'eslint --ext .ts,.tsx .'; + await fs.writeJson(packageJSONPath, packageJSON, { + spaces: 2, + }); + }, + }, + ]; + } +} + +export default new ViteTypeScriptTemplate(); diff --git a/packages/template/vite-typescript/test/ViteTypeScriptTemplate_spec_slow.ts b/packages/template/vite-typescript/test/ViteTypeScriptTemplate_spec_slow.ts new file mode 100644 index 0000000000..565769df86 --- /dev/null +++ b/packages/template/vite-typescript/test/ViteTypeScriptTemplate_spec_slow.ts @@ -0,0 +1,144 @@ +import cp from 'child_process'; +import path from 'path'; + +import { yarnOrNpmSpawn } from '@electron-forge/core-utils'; +import * as testUtils from '@electron-forge/test-utils'; +import { expect } from 'chai'; +import glob from 'fast-glob'; +import fs from 'fs-extra'; + +import { api } from '../../../api/core'; +import { initLink } from '../../../api/core/src/api/init-scripts/init-link'; + +describe('ViteTypeScriptTemplate', () => { + let dir: string; + + before(async () => { + await yarnOrNpmSpawn(['link:prepare']); + dir = await testUtils.ensureTestDirIsNonexistent(); + }); + + after(async () => { + await yarnOrNpmSpawn(['link:remove']); + await killWindowsEsbuildExe(); + await fs.remove(dir); + }); + + describe('template files are copied to project', () => { + it('should succeed in initializing the typescript template', async () => { + await api.init({ + dir, + template: path.resolve(__dirname, '..', 'src', 'ViteTypeScriptTemplate'), + interactive: false, + }); + }); + + const expectedFiles = [ + 'tsconfig.json', + '.eslintrc.json', + 'forge.config.ts', + 'vite.main.config.ts', + 'vite.renderer.config.ts', + 'vite.preload.config.ts', + path.join('src', 'main.ts'), + path.join('src', 'renderer.ts'), + path.join('src', 'preload.ts'), + path.join('src', 'types.d.ts'), + ]; + for (const filename of expectedFiles) { + it(`${filename} should exist`, async () => { + await testUtils.expectProjectPathExists(dir, filename, 'file'); + }); + } + + it('should ensure js source files from base template are removed', async () => { + const jsFiles = await glob(path.join(dir, 'src', '**', '*.js')); + expect(jsFiles.length).to.equal(0, `The following unexpected js files were found in the src/ folder: ${JSON.stringify(jsFiles)}`); + }); + }); + + describe('lint', () => { + it('should initially pass the linting process', async () => { + delete process.env.TS_NODE_PROJECT; + await testUtils.expectLintToPass(dir); + }); + }); + + describe('package', () => { + let cwd: string; + + before(async () => { + delete process.env.TS_NODE_PROJECT; + // Vite resolves plugins via cwd + cwd = process.cwd(); + process.chdir(dir); + // We need the version of vite to match exactly during development due to a quirk in + // typescript type-resolution. In prod no one has to worry about things like this + const pj = await fs.readJson(path.resolve(dir, 'package.json')); + pj.resolutions = { + // eslint-disable-next-line @typescript-eslint/no-var-requires + vite: `${require('../../../../node_modules/vite/package.json').version}`, + }; + await fs.writeJson(path.resolve(dir, 'package.json'), pj); + await yarnOrNpmSpawn(['install'], { + cwd: dir, + }); + + // Installing deps removes symlinks that were added at the start of this + // spec via `api.init`. So we should re-link local forge dependencies + // again. + await initLink(dir); + }); + + after(() => { + process.chdir(cwd); + }); + + it('should pass', async () => { + await api.package({ + dir, + interactive: false, + }); + }); + }); +}); + +/** + * TODO: resolve `esbuild` can not exit normally on the Windows platform. + * @deprecated + */ +async function killWindowsEsbuildExe() { + if (process.platform !== 'win32') { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + cp.exec('tasklist', (error, stdout) => { + if (error) { + reject(error); + return; + } + + const esbuild = stdout + .toString() + .split('\n') + .map((line) => line.split(/\s+/)) + .find((line) => line.includes('esbuild.exe')); + + if (!esbuild) { + resolve(); + return; + } + + // ['esbuild.exe', '4564', 'Console', '1', '14,400', 'K', ''] + const [, pid] = esbuild; + const result = process.kill(+pid, 'SIGINT'); + + if (result) { + resolve(); + } else { + reject(new Error('kill esbuild process failed')); + } + }); + }); +} diff --git a/packages/template/vite-typescript/tmpl/.eslintrc.json b/packages/template/vite-typescript/tmpl/.eslintrc.json new file mode 100644 index 0000000000..2d7aa60744 --- /dev/null +++ b/packages/template/vite-typescript/tmpl/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "env": { + "browser": true, + "es6": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/electron", + "plugin:import/typescript" + ], + "parser": "@typescript-eslint/parser" +} diff --git a/packages/template/vite-typescript/tmpl/forge.config.ts b/packages/template/vite-typescript/tmpl/forge.config.ts new file mode 100644 index 0000000000..ec2b06b858 --- /dev/null +++ b/packages/template/vite-typescript/tmpl/forge.config.ts @@ -0,0 +1,37 @@ +import type { ForgeConfig } from '@electron-forge/shared-types'; +import { MakerSquirrel } from '@electron-forge/maker-squirrel'; +import { MakerZIP } from '@electron-forge/maker-zip'; +import { MakerDeb } from '@electron-forge/maker-deb'; +import { MakerRpm } from '@electron-forge/maker-rpm'; +import { VitePlugin } from '@electron-forge/plugin-vite'; + +const config: ForgeConfig = { + packagerConfig: {}, + rebuildConfig: {}, + makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin']), new MakerRpm({}), new MakerDeb({})], + plugins: [ + new VitePlugin({ + // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc. + // If you are familiar with Vite configuration, it will look really familiar. + build: [ + { + // `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`. + entry: 'src/main.ts', + config: 'vite.main.config.ts', + }, + { + entry: 'src/preload.ts', + config: 'vite.preload.config.ts', + }, + ], + renderer: [ + { + name: 'main_window', + config: 'vite.renderer.config.ts', + }, + ], + }), + ], +}; + +export default config; diff --git a/packages/template/vite-typescript/tmpl/main.ts b/packages/template/vite-typescript/tmpl/main.ts new file mode 100644 index 0000000000..cbd9c22b0d --- /dev/null +++ b/packages/template/vite-typescript/tmpl/main.ts @@ -0,0 +1,53 @@ +import { app, BrowserWindow } from 'electron'; +import path from 'path'; + +// Handle creating/removing shortcuts on Windows when installing/uninstalling. +if (require('electron-squirrel-startup')) { + app.quit(); +} + +const createWindow = () => { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + }, + }); + + // and load the index.html of the app. + if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { + mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); + } else { + mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)); + } + + // Open the DevTools. + mainWindow.webContents.openDevTools(); +}; + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', createWindow); + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('activate', () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and import them here. diff --git a/packages/template/vite-typescript/tmpl/package.json b/packages/template/vite-typescript/tmpl/package.json new file mode 100644 index 0000000000..4e477a9ce6 --- /dev/null +++ b/packages/template/vite-typescript/tmpl/package.json @@ -0,0 +1,11 @@ +{ + "devDependencies": { + "@electron-forge/plugin-vite": "ELECTRON_FORGE/VERSION", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.0", + "ts-node": "^10.0.0", + "typescript": "~4.5.4" + } +} diff --git a/packages/template/vite-typescript/tmpl/preload.ts b/packages/template/vite-typescript/tmpl/preload.ts new file mode 100644 index 0000000000..5e9d369cc9 --- /dev/null +++ b/packages/template/vite-typescript/tmpl/preload.ts @@ -0,0 +1,2 @@ +// See the Electron documentation for details on how to use preload scripts: +// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts diff --git a/packages/template/vite-typescript/tmpl/renderer.ts b/packages/template/vite-typescript/tmpl/renderer.ts new file mode 100644 index 0000000000..d75993cde8 --- /dev/null +++ b/packages/template/vite-typescript/tmpl/renderer.ts @@ -0,0 +1,31 @@ +/** + * This file will automatically be loaded by vite and run in the "renderer" context. + * To learn more about the differences between the "main" and the "renderer" context in + * Electron, visit: + * + * https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes + * + * By default, Node.js integration in this file is disabled. When enabling Node.js integration + * in a renderer process, please be aware of potential security implications. You can read + * more about security risks here: + * + * https://electronjs.org/docs/tutorial/security + * + * To enable Node.js integration in this file, open up `main.ts` and enable the `nodeIntegration` + * flag: + * + * ``` + * // Create the browser window. + * mainWindow = new BrowserWindow({ + * width: 800, + * height: 600, + * webPreferences: { + * nodeIntegration: true + * } + * }); + * ``` + */ + +import './index.css'; + +console.log('👋 This message is being logged by "renderer.ts", included via Vite'); diff --git a/packages/template/vite-typescript/tmpl/tsconfig.json b/packages/template/vite-typescript/tmpl/tsconfig.json new file mode 100644 index 0000000000..01a588b529 --- /dev/null +++ b/packages/template/vite-typescript/tmpl/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "commonjs", + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "noImplicitAny": true, + "sourceMap": true, + "baseUrl": ".", + "outDir": "dist", + "moduleResolution": "node", + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/packages/template/vite-typescript/tmpl/types.d.ts b/packages/template/vite-typescript/tmpl/types.d.ts new file mode 100644 index 0000000000..eae3393f14 --- /dev/null +++ b/packages/template/vite-typescript/tmpl/types.d.ts @@ -0,0 +1,5 @@ +// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite +// plugin that tells the Electron app where to look for the Vite-bundled app code (depending on +// whether you're running in development or production). +declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string; +declare const MAIN_WINDOW_VITE_NAME: string; diff --git a/packages/template/vite-typescript/tmpl/vite.main.config.ts b/packages/template/vite-typescript/tmpl/vite.main.config.ts new file mode 100644 index 0000000000..c93ad03824 --- /dev/null +++ b/packages/template/vite-typescript/tmpl/vite.main.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config +export default defineConfig({ + resolve: { + // Some libs that can run in both Web and Node.js, such as `axios`, we need to tell Vite to build them in Node.js. + browserField: false, + mainFields: ['module', 'jsnext:main', 'jsnext'], + }, +}); diff --git a/packages/template/vite-typescript/tmpl/vite.preload.config.ts b/packages/template/vite-typescript/tmpl/vite.preload.config.ts new file mode 100644 index 0000000000..690be5b1a9 --- /dev/null +++ b/packages/template/vite-typescript/tmpl/vite.preload.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config +export default defineConfig({}); diff --git a/packages/template/vite-typescript/tmpl/vite.renderer.config.ts b/packages/template/vite-typescript/tmpl/vite.renderer.config.ts new file mode 100644 index 0000000000..690be5b1a9 --- /dev/null +++ b/packages/template/vite-typescript/tmpl/vite.renderer.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config +export default defineConfig({});