From 5c1372a681d9456e63fd4567ceb38087be0177bd Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Apr 2026 12:31:08 -0700 Subject: [PATCH 01/22] feat(template)!: unify JS/TS templates --- .../src/create-electron-app.ts | 12 +- .../src/init-scripts/find-template.ts | 6 + .../external/create-electron-app/src/init.ts | 6 + packages/template/base/package.json | 1 + packages/template/base/src/BaseTemplate.ts | 12 + .../template/vite-typescript/.eslintignore | 1 - .../template/vite-typescript/package.json | 34 --- ...eTypeScriptTemplate.slow.verdaccio.spec.ts | 113 ------- .../src/ViteTypeScriptTemplate.ts | 89 ------ .../vite-typescript/tmpl/.oxlintrc.json | 6 - .../vite-typescript/tmpl/package.json | 16 - .../template/vite/spec/ViteTemplate.spec.ts | 260 ++++++++++++---- packages/template/vite/src/ViteTemplate.ts | 148 +++++++-- packages/template/vite/tmpl/forge.config.js | 66 ---- .../tmpl/forge.config.mts} | 0 packages/template/vite/tmpl/index.js | 52 ---- .../{vite-typescript => vite}/tmpl/main.ts | 20 +- packages/template/vite/tmpl/package.json | 5 +- packages/template/vite/tmpl/preload.js | 3 - .../tmpl/preload.ts | 2 +- packages/template/vite/tmpl/renderer.js | 33 -- .../tmpl/renderer.ts | 0 .../tmpl/tsconfig.json | 0 .../template/vite/tmpl/vite.main.config.mjs | 4 - .../tmpl/vite.main.config.ts | 0 .../vite/tmpl/vite.preload.config.mjs | 4 - .../tmpl/vite.preload.config.ts | 0 .../vite/tmpl/vite.renderer.config.mjs | 4 - .../tmpl/vite.renderer.config.ts | 0 .../template/webpack-typescript/package.json | 41 --- .../WebpackTypeScript.slow.verdaccio.spec.ts | 109 ------- .../src/WebpackTypeScriptTemplate.ts | 81 ----- .../webpack-typescript/tmpl/.oxlintrc.json | 6 - .../webpack-typescript/tmpl/package.json | 20 -- .../webpack/spec/WebpackTemplate.spec.ts | 284 ++++++++++++++---- .../template/webpack/src/WebpackTemplate.ts | 142 +++++++-- .../template/webpack/tmpl/forge.config.js | 63 ---- .../tmpl/forge.config.mts} | 0 .../tmpl/{ => js}/webpack.main.config.js | 7 +- .../tmpl/{ => js}/webpack.renderer.config.js | 5 +- .../template/webpack/tmpl/js/webpack.rules.js | 16 + .../tmpl/index.ts => webpack/tmpl/main.ts} | 20 +- packages/template/webpack/tmpl/package.json | 8 +- packages/template/webpack/tmpl/preload.js | 3 - .../tmpl/preload.ts | 0 packages/template/webpack/tmpl/renderer.js | 33 -- .../tmpl/renderer.ts | 4 +- .../tmpl/tsconfig.json | 0 .../tmpl/webpack.main.config.ts | 2 +- .../tmpl/webpack.plugins.ts | 0 .../tmpl/webpack.renderer.config.ts | 0 .../template/webpack/tmpl/webpack.rules.js | 35 --- .../tmpl/webpack.rules.ts | 0 packages/utils/types/src/index.ts | 1 + 54 files changed, 760 insertions(+), 1017 deletions(-) delete mode 100644 packages/template/vite-typescript/.eslintignore delete mode 100644 packages/template/vite-typescript/package.json delete mode 100644 packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts delete mode 100644 packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts delete mode 100644 packages/template/vite-typescript/tmpl/.oxlintrc.json delete mode 100644 packages/template/vite-typescript/tmpl/package.json delete mode 100644 packages/template/vite/tmpl/forge.config.js rename packages/template/{vite-typescript/tmpl/forge.config.ts => vite/tmpl/forge.config.mts} (100%) delete mode 100644 packages/template/vite/tmpl/index.js rename packages/template/{vite-typescript => vite}/tmpl/main.ts (90%) delete mode 100644 packages/template/vite/tmpl/preload.js rename packages/template/{webpack-typescript => vite}/tmpl/preload.ts (74%) delete mode 100644 packages/template/vite/tmpl/renderer.js rename packages/template/{vite-typescript => vite}/tmpl/renderer.ts (100%) rename packages/template/{vite-typescript => vite}/tmpl/tsconfig.json (100%) delete mode 100644 packages/template/vite/tmpl/vite.main.config.mjs rename packages/template/{vite-typescript => vite}/tmpl/vite.main.config.ts (100%) delete mode 100644 packages/template/vite/tmpl/vite.preload.config.mjs rename packages/template/{vite-typescript => vite}/tmpl/vite.preload.config.ts (100%) delete mode 100644 packages/template/vite/tmpl/vite.renderer.config.mjs rename packages/template/{vite-typescript => vite}/tmpl/vite.renderer.config.ts (100%) delete mode 100644 packages/template/webpack-typescript/package.json delete mode 100644 packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts delete mode 100644 packages/template/webpack-typescript/src/WebpackTypeScriptTemplate.ts delete mode 100644 packages/template/webpack-typescript/tmpl/.oxlintrc.json delete mode 100644 packages/template/webpack-typescript/tmpl/package.json delete mode 100644 packages/template/webpack/tmpl/forge.config.js rename packages/template/{webpack-typescript/tmpl/forge.config.ts => webpack/tmpl/forge.config.mts} (100%) rename packages/template/webpack/tmpl/{ => js}/webpack.main.config.js (61%) rename packages/template/webpack/tmpl/{ => js}/webpack.renderer.config.js (54%) create mode 100644 packages/template/webpack/tmpl/js/webpack.rules.js rename packages/template/{webpack-typescript/tmpl/index.ts => webpack/tmpl/main.ts} (91%) delete mode 100644 packages/template/webpack/tmpl/preload.js rename packages/template/{vite-typescript => webpack}/tmpl/preload.ts (100%) delete mode 100644 packages/template/webpack/tmpl/renderer.js rename packages/template/{webpack-typescript => webpack}/tmpl/renderer.ts (86%) rename packages/template/{webpack-typescript => webpack}/tmpl/tsconfig.json (100%) rename packages/template/{webpack-typescript => webpack}/tmpl/webpack.main.config.ts (94%) rename packages/template/{webpack-typescript => webpack}/tmpl/webpack.plugins.ts (100%) rename packages/template/{webpack-typescript => webpack}/tmpl/webpack.renderer.config.ts (100%) delete mode 100644 packages/template/webpack/tmpl/webpack.rules.js rename packages/template/{webpack-typescript => webpack}/tmpl/webpack.rules.ts (100%) diff --git a/packages/external/create-electron-app/src/create-electron-app.ts b/packages/external/create-electron-app/src/create-electron-app.ts index b3cb52cc36..ed3fdc4381 100644 --- a/packages/external/create-electron-app/src/create-electron-app.ts +++ b/packages/external/create-electron-app/src/create-electron-app.ts @@ -41,6 +41,7 @@ const initCommand = program '--package-manager [name]', 'Set a specific package manager to use for your Forge project. Supported package managers are `npm`, `pnpm`, and `yarn`. You can also specify an exact version to use (e.g. `yarn@1.22.22`).', ) + .option('--typescript', 'Use TypeScript in the template.') .action(async (dir) => { const options = initCommand.opts(); const tasks = new Listr( @@ -52,6 +53,7 @@ const initCommand = program initOpts.copyCIFiles = Boolean(options.copyCiFiles); initOpts.force = Boolean(options.force); initOpts.skipGit = Boolean(options.skipGit); + initOpts.typescript = Boolean(options.typescript); initOpts.dir = resolveWorkingDir(dir, false); initOpts.electronVersion = options.electronVersion ?? 'latest'; initOpts.packageManager = options.packageManager ?? 'npm@latest'; @@ -120,21 +122,19 @@ const initCommand = program }, ); - let language: string | undefined; - if (bundler !== 'base') { - language = await prompt.run>( + initOpts.typescript = await prompt.run>( select, { message: 'Select a programming language', choices: [ { name: 'JavaScript', - value: undefined, + value: false, }, { name: 'TypeScript', - value: 'typescript', + value: true, }, ], }, @@ -142,7 +142,7 @@ const initCommand = program } initOpts.packageManager = packageManager; - initOpts.template = `${bundler}${language ? `-${language}` : ''}`; + initOpts.template = bundler; // TODO: add prompt for passing in an exact version as well initOpts.electronVersion = await prompt.run>( diff --git a/packages/external/create-electron-app/src/init-scripts/find-template.ts b/packages/external/create-electron-app/src/init-scripts/find-template.ts index 29e4d8df7b..4d95548a96 100644 --- a/packages/external/create-electron-app/src/init-scripts/find-template.ts +++ b/packages/external/create-electron-app/src/init-scripts/find-template.ts @@ -44,6 +44,12 @@ export const findTemplate = async ( } } if (!foundTemplate) { + const tsMatch = template.match(/^(.+)-typescript$/); + if (tsMatch) { + throw new Error( + `The "${template}" template no longer exists. Use "--template ${tsMatch[1]}" instead and select TypeScript when prompted.`, + ); + } throw new Error(`Failed to locate custom template: "${template}".`); } else { d(`found template module at: ${foundTemplate.path}`); diff --git a/packages/external/create-electron-app/src/init.ts b/packages/external/create-electron-app/src/init.ts index 8cdd1e9fdf..b3d8759185 100644 --- a/packages/external/create-electron-app/src/init.ts +++ b/packages/external/create-electron-app/src/init.ts @@ -59,6 +59,10 @@ export interface InitOptions { * Force a package manager to use (npm|yarn|pnpm). */ packageManager?: string; + /** + * Whether to use TypeScript in the template. + */ + typescript?: boolean; } async function validateTemplate( @@ -95,6 +99,7 @@ export async function init({ skipGit = false, electronVersion = 'latest', packageManager, + typescript = false, }: InitOptions): Promise { d(`Initializing in: ${dir}`); @@ -173,6 +178,7 @@ export async function init({ const tasks = await templateModule.initializeTemplate(dir, { copyCIFiles, force, + typescript, }); if (tasks) { return task.newListr(tasks, { concurrent: false }); diff --git a/packages/template/base/package.json b/packages/template/base/package.json index 791f58b156..5013553ac5 100644 --- a/packages/template/base/package.json +++ b/packages/template/base/package.json @@ -15,6 +15,7 @@ "@electron-forge/core-utils": "workspace:*", "@electron-forge/shared-types": "workspace:*", "@malept/cross-spawn-promise": "^2.0.0", + "oxfmt": "^0.41.0", "debug": "^4.3.1", "fs-extra": "^10.0.0", "semver": "^7.2.1", diff --git a/packages/template/base/src/BaseTemplate.ts b/packages/template/base/src/BaseTemplate.ts index d94a4783bc..767d76cb28 100644 --- a/packages/template/base/src/BaseTemplate.ts +++ b/packages/template/base/src/BaseTemplate.ts @@ -1,3 +1,4 @@ +import { stripTypeScriptTypes } from 'node:module'; import path from 'node:path'; import { resolvePackageManager } from '@electron-forge/core-utils'; @@ -8,6 +9,7 @@ import { } from '@electron-forge/shared-types'; import debug from 'debug'; import fs from 'fs-extra'; +import { format } from 'oxfmt'; import semver from 'semver'; import determineAuthor from './determine-author.js'; @@ -187,6 +189,16 @@ export class BaseTemplate implements ForgeTemplate { }); } + async stripAndRename(srcPath: string, destPath: string): Promise { + const source = await fs.readFile(srcPath, 'utf8'); + const stripped = stripTypeScriptTypes(source, { mode: 'transform' }); + const formatted = await format(destPath, stripped); + await fs.writeFile(destPath, formatted.code); + if (srcPath !== destPath) { + await fs.remove(srcPath); + } + } + async updateFileByLine( inputPath: string, lineHandler: (line: string) => string | null, diff --git a/packages/template/vite-typescript/.eslintignore b/packages/template/vite-typescript/.eslintignore deleted file mode 100644 index 14e485a5bc..0000000000 --- a/packages/template/vite-typescript/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -tmpl diff --git a/packages/template/vite-typescript/package.json b/packages/template/vite-typescript/package.json deleted file mode 100644 index 9d90367de7..0000000000 --- a/packages/template/vite-typescript/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@electron-forge/template-vite-typescript", - "version": "8.0.0-alpha.7", - "type": "module", - "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", - "exports": "./dist/ViteTypeScriptTemplate.js", - "typings": "dist/ViteTypeScriptTemplate.d.ts", - "engines": { - "node": ">= 22.12.0" - }, - "dependencies": { - "@electron-forge/shared-types": "workspace:*", - "@electron-forge/template-base": "workspace:*", - "fs-extra": "^10.0.0" - }, - "devDependencies": { - "@electron-forge/core-utils": "workspace:*", - "@electron-forge/test-utils": "workspace:*", - "fast-glob": "^3.2.7", - "vitest": "catalog:" - }, - "files": [ - "dist", - "src", - "tmpl" - ] -} diff --git a/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts b/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts deleted file mode 100644 index 714d7fcb5d..0000000000 --- a/packages/template/vite-typescript/spec/ViteTypeScriptTemplate.slow.verdaccio.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; - -import { - PACKAGE_MANAGERS, - spawnPackageManager, -} from '@electron-forge/core-utils'; -import * as testUtils from '@electron-forge/test-utils'; -import glob from 'fast-glob'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; - -// eslint-disable-next-line n/no-missing-import -import { api } from '../../../api/core/dist/api'; -import { init } from '../../../external/create-electron-app/src/init'; - -describe('ViteTypeScriptTemplate', () => { - let dir: string; - - beforeAll(async () => { - dir = await testUtils.ensureTestDirIsNonexistent(); - await init({ - dir, - template: path.resolve(import.meta.dirname, '..'), - interactive: false, - electronVersion: '38.2.2', - }); - }); - - afterAll(async () => { - await spawnPackageManager(PACKAGE_MANAGERS['yarn'], ['unlink', '--all']); - if (os.platform() !== 'win32') { - // Windows platform `fs.remove(dir)` logic using `npm run test:clear`. - await fs.promises.rm(dir, { force: true, recursive: true }); - } - }); - - describe('template files are copied to project', () => { - it.each([ - 'package.json', - 'tsconfig.json', - '.oxlintrc.json', - 'forge.config.ts', - 'vite.main.config.ts', - 'vite.preload.config.ts', - 'vite.renderer.config.ts', - path.join('src', 'main.ts'), - path.join('src', 'renderer.ts'), - path.join('src', 'preload.ts'), - ])(`%s should exist`, async (filename) => { - expect(fs.existsSync(path.join(dir, filename))).toBe(true); - }); - - it('should ensure js source files from base template are removed', async () => { - const jsFiles = await glob(path.join(dir, 'src', '**', '*.js')); - expect(jsFiles.length).toEqual(0); - }); - - it('should contain `private:true` in package.json', async () => { - const packageJSONString = await fs.promises.readFile( - path.join(dir, 'package.json'), - 'utf-8', - ); - const packageJSON = JSON.parse(packageJSONString); - expect(packageJSON).toHaveProperty('private', true); - }); - - it('should contain electron-forge scripts in package.json', async () => { - const packageJSON = JSON.parse( - await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), - ); - expect(packageJSON.scripts.start).toBe('electron-forge start'); - expect(packageJSON.scripts.package).toBe('electron-forge package'); - expect(packageJSON.scripts.make).toBe('electron-forge make'); - expect(packageJSON.scripts.publish).toBe('electron-forge publish'); - }); - }); - - describe('lint', () => { - it('should initially pass the linting process', async () => { - delete process.env.TS_NODE_PROJECT; - await testUtils.expectLintToPass(dir); - }); - }); - - describe('typecheck', () => { - it('should initially pass the typechecking process', async () => { - await testUtils.expectTypecheckToPass(dir); - }); - }); - - describe('package', () => { - let cwd: string; - - beforeAll(async () => { - delete process.env.TS_NODE_PROJECT; - // Vite resolves plugins via cwd - cwd = process.cwd(); - process.chdir(dir); - }); - - afterAll(() => { - process.chdir(cwd); - }); - - it('should pass', async () => { - await api.package({ - dir, - interactive: false, - }); - }); - }); -}); diff --git a/packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts b/packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts deleted file mode 100644 index 90df268793..0000000000 --- a/packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts +++ /dev/null @@ -1,89 +0,0 @@ -import path from 'node: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(import.meta.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.preload.config.ts'); - await this.copyTemplateFile(directory, 'vite.renderer.config.ts'); - - // Copy tsconfig with a small set of presets - await this.copyTemplateFile(directory, 'tsconfig.json'); - - await this.writeLintConfig(directory); - - // 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', - ); - - // 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'), - { overwrite: options.force }, - ); - await this.updateFileByLine( - path.join(directory, 'index.html'), - (line) => { - if (line.includes('link rel="stylesheet"')) return null; - 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'; - await fs.writeJson(packageJSONPath, packageJSON, { - spaces: 2, - }); - }, - }, - ]; - } -} - -export default new ViteTypeScriptTemplate(); diff --git a/packages/template/vite-typescript/tmpl/.oxlintrc.json b/packages/template/vite-typescript/tmpl/.oxlintrc.json deleted file mode 100644 index 1d845d7f69..0000000000 --- a/packages/template/vite-typescript/tmpl/.oxlintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "categories": { - "correctness": "warn" - } -} diff --git a/packages/template/vite-typescript/tmpl/package.json b/packages/template/vite-typescript/tmpl/package.json deleted file mode 100644 index 04cf900c7b..0000000000 --- a/packages/template/vite-typescript/tmpl/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "main": ".vite/build/main.js", - "scripts": { - "lint": "oxlint && oxfmt --check", - "lint:fix": "oxlint --fix && oxfmt --write", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@electron-forge/plugin-vite": "ELECTRON_FORGE/VERSION", - "@types/electron-squirrel-startup": "^1.0.2", - "oxfmt": "^0.41.0", - "oxlint": "^1.0.0", - "typescript": "^5.9.2", - "vite": "^8.0.0" - } -} diff --git a/packages/template/vite/spec/ViteTemplate.spec.ts b/packages/template/vite/spec/ViteTemplate.spec.ts index b2bf29ea95..4194b7468f 100644 --- a/packages/template/vite/spec/ViteTemplate.spec.ts +++ b/packages/template/vite/spec/ViteTemplate.spec.ts @@ -8,74 +8,218 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import template from '../src/ViteTemplate'; describe('ViteTemplate', () => { - let dir: string; + describe('with typescript: false (default)', () => { + let dir: string; - beforeAll(async () => { - dir = await testUtils.ensureTestDirIsNonexistent(); - }); + beforeAll(async () => { + dir = await testUtils.ensureTestDirIsNonexistent(); + }); - afterAll(async () => { - await fs.promises.rm(dir, { recursive: true }); - }); + afterAll(async () => { + await fs.promises.rm(dir, { recursive: true }); + }); - it('should succeed in initializing the vite template', async () => { - const tasks = await template.initializeTemplate(dir, {}); - const runner = new Listr(tasks, { - concurrent: false, - exitOnError: false, - fallbackRendererCondition: - Boolean(process.env.DEBUG) || Boolean(process.env.CI), + it('should succeed in initializing the template', async () => { + const tasks = await template.initializeTemplate(dir, {}); + const runner = new Listr(tasks, { + concurrent: false, + exitOnError: false, + fallbackRendererCondition: + Boolean(process.env.DEBUG) || Boolean(process.env.CI), + }); + await runner.run(); + expect(runner.errors).toHaveLength(0); }); - await runner.run(); - expect(runner.errors).toHaveLength(0); - }); - describe('template files are copied to project', () => { - const expectedFiles = [ - 'package.json', - 'forge.config.js', - 'vite.main.config.mjs', - 'vite.preload.config.mjs', - 'vite.renderer.config.mjs', - path.join('src', 'renderer.js'), - path.join('src', 'preload.js'), - ]; - it.each(expectedFiles)(`%s should exist`, async (filename) => { - const file = path.join(dir, filename); - expect(fs.existsSync(file)).toBe(true); + describe('template files are copied to project', () => { + const expectedFiles = [ + 'package.json', + 'forge.config.mjs', + 'vite.main.config.mjs', + 'vite.preload.config.mjs', + 'vite.renderer.config.mjs', + path.join('src', 'main.js'), + path.join('src', 'renderer.js'), + path.join('src', 'preload.js'), + ]; + it.each(expectedFiles)(`%s should exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(true); + }); + + const unexpectedFiles = [ + 'forge.config.mts', + 'tsconfig.json', + 'vite.main.config.ts', + 'vite.preload.config.ts', + 'vite.renderer.config.ts', + path.join('src', 'main.ts'), + path.join('src', 'renderer.ts'), + path.join('src', 'preload.ts'), + ]; + it.each(unexpectedFiles)(`%s should not exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(false); + }); }); - }); - it('should move and rewrite the main process file', async () => { - expect(fs.existsSync(path.join(dir, 'src', 'index.js'))).toBe(false); - expect(fs.existsSync(path.join(dir, 'src', 'main.js'))).toBe(true); - const mainFile = ( - await fs.promises.readFile(path.join(dir, 'src', 'main.js')) - ).toString(); - expect(mainFile).toMatch(/MAIN_WINDOW_VITE_DEV_SERVER_URL/); - expect(mainFile).toMatch( - /\.\.\/renderer\/\${MAIN_WINDOW_VITE_NAME}\/index\.html/, - ); - }); + it('should move and rewrite the main process file', async () => { + expect(fs.existsSync(path.join(dir, 'src', 'index.js'))).toBe(false); + expect(fs.existsSync(path.join(dir, 'src', 'main.js'))).toBe(true); + const mainFile = ( + await fs.promises.readFile(path.join(dir, 'src', 'main.js')) + ).toString(); + expect(mainFile).toMatch(/MAIN_WINDOW_VITE_DEV_SERVER_URL/); + expect(mainFile).toMatch( + /\.\.\/renderer\/\${MAIN_WINDOW_VITE_NAME}\/index\.html/, + ); + }); - it('should remove the stylesheet link from the HTML file', async () => { - expect( - (await fs.promises.readFile(path.join(dir, 'index.html'))).toString(), - ).not.toMatch(/link rel="stylesheet"/); - }); + it('should produce valid JavaScript without type annotations', async () => { + const mainFile = ( + await fs.promises.readFile(path.join(dir, 'src', 'main.js')) + ).toString(); + expect(mainFile).not.toMatch(/:\s*(string|number|boolean|void)\b/); + expect(mainFile).not.toMatch(/\binterface\b/); + }); - it('should inject script into the HTML file', async () => { - expect( - (await fs.promises.readFile(path.join(dir, 'index.html'))).toString(), - ).toMatch(/src="\/src\/renderer\.js"/); + it('should remove the stylesheet link from the HTML file', async () => { + expect( + (await fs.promises.readFile(path.join(dir, 'index.html'))).toString(), + ).not.toMatch(/link rel="stylesheet"/); + }); + + it('should inject script with .js extension into the HTML file', async () => { + expect( + (await fs.promises.readFile(path.join(dir, 'index.html'))).toString(), + ).toMatch(/src="\/src\/renderer\.js"/); + }); + + it('should reference .js/.mjs paths in forge.config.mjs', async () => { + const config = ( + await fs.promises.readFile(path.join(dir, 'forge.config.mjs')) + ).toString(); + expect(config).toMatch(/src\/main\.js/); + expect(config).toMatch(/src\/preload\.js/); + expect(config).toMatch(/vite\.main\.config\.mjs/); + expect(config).toMatch(/vite\.preload\.config\.mjs/); + expect(config).toMatch(/vite\.renderer\.config\.mjs/); + expect(config).not.toMatch(/\.ts/); + }); + + it('should not include typecheck script in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON.scripts.typecheck).toBeUndefined(); + }); + + it('should contain `private:true` in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON).toHaveProperty('private', true); + }); }); - it('should contain `private:true` in package.json', async () => { - const packageJSONString = await fs.promises.readFile( - path.join(dir, 'package.json'), - 'utf-8', - ); - const packageJSON = JSON.parse(packageJSONString); - expect(packageJSON).toHaveProperty('private', true); + describe('with typescript: true', () => { + let dir: string; + + beforeAll(async () => { + dir = await testUtils.ensureTestDirIsNonexistent(); + }); + + afterAll(async () => { + await fs.promises.rm(dir, { recursive: true }); + }); + + it('should succeed in initializing the template', async () => { + const tasks = await template.initializeTemplate(dir, { + typescript: true, + }); + const runner = new Listr(tasks, { + concurrent: false, + exitOnError: false, + fallbackRendererCondition: + Boolean(process.env.DEBUG) || Boolean(process.env.CI), + }); + await runner.run(); + expect(runner.errors).toHaveLength(0); + }); + + describe('template files are copied to project', () => { + const expectedFiles = [ + 'package.json', + 'forge.config.mts', + 'tsconfig.json', + 'vite.main.config.ts', + 'vite.preload.config.ts', + 'vite.renderer.config.ts', + path.join('src', 'main.ts'), + path.join('src', 'renderer.ts'), + path.join('src', 'preload.ts'), + ]; + it.each(expectedFiles)(`%s should exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(true); + }); + + const unexpectedFiles = [ + 'forge.config.mjs', + 'vite.main.config.mjs', + 'vite.preload.config.mjs', + 'vite.renderer.config.mjs', + path.join('src', 'main.js'), + path.join('src', 'renderer.js'), + path.join('src', 'preload.js'), + ]; + it.each(unexpectedFiles)(`%s should not exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(false); + }); + }); + + it('should move and rewrite the main process file', async () => { + expect(fs.existsSync(path.join(dir, 'src', 'index.js'))).toBe(false); + expect(fs.existsSync(path.join(dir, 'src', 'main.ts'))).toBe(true); + const mainFile = ( + await fs.promises.readFile(path.join(dir, 'src', 'main.ts')) + ).toString(); + expect(mainFile).toMatch(/MAIN_WINDOW_VITE_DEV_SERVER_URL/); + expect(mainFile).toMatch( + /\.\.\/renderer\/\${MAIN_WINDOW_VITE_NAME}\/index\.html/, + ); + }); + + it('should remove the stylesheet link from the HTML file', async () => { + expect( + (await fs.promises.readFile(path.join(dir, 'index.html'))).toString(), + ).not.toMatch(/link rel="stylesheet"/); + }); + + it('should inject script with .ts extension into the HTML file', async () => { + expect( + (await fs.promises.readFile(path.join(dir, 'index.html'))).toString(), + ).toMatch(/src="\/src\/renderer\.ts"/); + }); + + it('should reference .ts paths in forge.config.mts', async () => { + const config = ( + await fs.promises.readFile(path.join(dir, 'forge.config.mts')) + ).toString(); + expect(config).toMatch(/src\/main\.ts/); + expect(config).toMatch(/src\/preload\.ts/); + expect(config).toMatch(/vite\.main\.config\.ts/); + }); + + it('should include typecheck script in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON.scripts.typecheck).toBeDefined(); + }); + + it('should contain `private:true` in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON).toHaveProperty('private', true); + }); }); }); diff --git a/packages/template/vite/src/ViteTemplate.ts b/packages/template/vite/src/ViteTemplate.ts index 2e2034f86c..d2343f75a4 100644 --- a/packages/template/vite/src/ViteTemplate.ts +++ b/packages/template/vite/src/ViteTemplate.ts @@ -7,57 +7,133 @@ import { import { BaseTemplate } from '@electron-forge/template-base'; import fs from 'fs-extra'; +const TS_ONLY_DEV_DEPS = new Set([ + '@types/electron-squirrel-startup', + 'typescript', +]); + +const TS_ONLY_SCRIPTS = new Set(['typecheck']); + class ViteTemplate extends BaseTemplate { public templateDir = path.resolve(import.meta.dirname, '..', 'tmpl'); + public override get devDependencies(): string[] { + const all = super.devDependencies; + if (this._typescript) return all; + return all.filter((dep) => { + const name = dep.replace(/@[^@]*$/, ''); + return !TS_ONLY_DEV_DEPS.has(name); + }); + } + + private _typescript = false; + public async initializeTemplate( directory: string, options: InitTemplateOptions, ): Promise { + const typescript = options.typescript ?? false; + this._typescript = typescript; const superTasks = await super.initializeTemplate(directory, options); + return [ ...superTasks, { title: 'Setting up Forge configuration', task: async () => { - await this.copyTemplateFile(directory, 'forge.config.js'); + // Remove the base template's forge.config.js + await fs.remove(path.resolve(directory, 'forge.config.js')); + + if (typescript) { + await this.copyTemplateFile(directory, 'forge.config.mts'); + } else { + await this.copyTemplateFile(directory, 'forge.config.mts'); + await this.stripAndRename( + path.resolve(directory, 'forge.config.mts'), + path.resolve(directory, 'forge.config.mjs'), + ); + // Patch entry/config paths from .ts to .js/.mjs + await this.updateFileByLine( + path.resolve(directory, 'forge.config.mjs'), + (line) => + line + .replace(/src\/main\.ts/g, 'src/main.js') + .replace(/src\/preload\.ts/g, 'src/preload.js') + .replace(/vite\.main\.config\.ts/g, 'vite.main.config.mjs') + .replace( + /vite\.preload\.config\.ts/g, + 'vite.preload.config.mjs', + ) + .replace( + /vite\.renderer\.config\.ts/g, + 'vite.renderer.config.mjs', + ), + ); + } }, }, { - title: 'Setting up Vite configuration', + title: `Setting up ${typescript ? 'TypeScript' : 'Vite'} configuration`, task: async () => { - await this.copyTemplateFile(directory, 'vite.main.config.mjs'); - await this.copyTemplateFile(directory, 'vite.preload.config.mjs'); - await this.copyTemplateFile(directory, 'vite.renderer.config.mjs'); + // Copy Vite config files + if (typescript) { + await this.copyTemplateFile(directory, 'vite.main.config.ts'); + await this.copyTemplateFile(directory, 'vite.preload.config.ts'); + await this.copyTemplateFile(directory, 'vite.renderer.config.ts'); + } else { + for (const name of [ + 'vite.main.config', + 'vite.preload.config', + 'vite.renderer.config', + ]) { + await this.copyTemplateFile(directory, `${name}.ts`); + await this.stripAndRename( + path.resolve(directory, `${name}.ts`), + path.resolve(directory, `${name}.mjs`), + ); + } + } + + // Copy tsconfig for TypeScript only + if (typescript) { + await this.copyTemplateFile(directory, 'tsconfig.json'); + } await this.writeLintConfig(directory); - await this.copyTemplateFile( - path.join(directory, 'src'), - 'renderer.js', - ); - await this.copyTemplateFile( - path.join(directory, 'src'), - 'preload.js', - ); - await this.copyTemplateFile(path.join(directory, 'src'), 'index.js'); - await this.updateFileByLine( - path.resolve(directory, 'src', 'index.js'), - (line) => { - if (line.includes('mainWindow.loadFile')) - return ` if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { - mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); - } else { - mainWindow.loadFile(path.join(import.meta.dirname, \`../renderer/\${MAIN_WINDOW_VITE_NAME}/index.html\`)); - }`; - return line; - }, - path.resolve(directory, 'src', 'main.js'), - ); + // Remove base template's JS source files + await fs.remove(path.resolve(directory, 'src', 'index.js')); + await fs.remove(path.resolve(directory, 'src', 'preload.js')); + + // Copy source files + if (typescript) { + 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'), + 'preload.ts', + ); + } else { + // Copy TS source files, strip types, rename to .js + for (const name of ['main', 'renderer', 'preload']) { + await this.copyTemplateFile( + path.join(directory, 'src'), + `${name}.ts`, + ); + await this.stripAndRename( + path.resolve(directory, 'src', `${name}.ts`), + path.resolve(directory, 'src', `${name}.js`), + ); + } + } - // TODO: Compatible with any path entry. - // Vite uses index.html under the root path as the entry point. - fs.moveSync( + const ext = typescript ? 'ts' : 'js'; + + // Move index.html to root (Vite uses root index.html as entry) + await fs.move( path.join(directory, 'src', 'index.html'), path.join(directory, 'index.html'), { overwrite: options.force }, @@ -67,10 +143,20 @@ class ViteTemplate extends BaseTemplate { (line) => { if (line.includes('link rel="stylesheet"')) return null; if (line.includes('')) - return ' \n '; + return ` \n `; return line; }, ); + + // Remove TS-only scripts from package.json for JS variant + if (!typescript) { + const packageJSONPath = path.resolve(directory, 'package.json'); + const packageJSON = await fs.readJson(packageJSONPath); + for (const script of TS_ONLY_SCRIPTS) { + delete packageJSON.scripts[script]; + } + await fs.writeJson(packageJSONPath, packageJSON, { spaces: 2 }); + } }, }, ]; diff --git a/packages/template/vite/tmpl/forge.config.js b/packages/template/vite/tmpl/forge.config.js deleted file mode 100644 index f8d183977a..0000000000 --- a/packages/template/vite/tmpl/forge.config.js +++ /dev/null @@ -1,66 +0,0 @@ -const { FusesPlugin } = require('@electron-forge/plugin-fuses'); -const { FuseV1Options, FuseVersion } = require('@electron/fuses'); - -module.exports = { - packagerConfig: { - asar: true, - }, - rebuildConfig: {}, - makers: [ - { - name: '@electron-forge/maker-squirrel', - config: {}, - }, - { - name: '@electron-forge/maker-zip', - platforms: ['darwin'], - }, - { - name: '@electron-forge/maker-deb', - config: {}, - }, - { - name: '@electron-forge/maker-rpm', - config: {}, - }, - ], - plugins: [ - { - name: '@electron-forge/plugin-vite', - config: { - // `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.js', - config: 'vite.main.config.mjs', - target: 'main', - }, - { - entry: 'src/preload.js', - config: 'vite.preload.config.mjs', - target: 'preload', - }, - ], - renderer: [ - { - name: 'main_window', - config: 'vite.renderer.config.mjs', - }, - ], - }, - }, - // Fuses are used to enable/disable various Electron functionality - // at package time, before code signing the application - new FusesPlugin({ - version: FuseVersion.V1, - [FuseV1Options.RunAsNode]: false, - [FuseV1Options.EnableCookieEncryption]: true, - [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, - [FuseV1Options.EnableNodeCliInspectArguments]: false, - [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, - [FuseV1Options.OnlyLoadAppFromAsar]: true, - }), - ], -}; diff --git a/packages/template/vite-typescript/tmpl/forge.config.ts b/packages/template/vite/tmpl/forge.config.mts similarity index 100% rename from packages/template/vite-typescript/tmpl/forge.config.ts rename to packages/template/vite/tmpl/forge.config.mts diff --git a/packages/template/vite/tmpl/index.js b/packages/template/vite/tmpl/index.js deleted file mode 100644 index 121c5c9f80..0000000000 --- a/packages/template/vite/tmpl/index.js +++ /dev/null @@ -1,52 +0,0 @@ -import { app, BrowserWindow } from 'electron'; -import path from 'node:path'; -import started from 'electron-squirrel-startup'; - -// Handle creating/removing shortcuts on Windows when installing/uninstalling. -if (started) { - 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. - mainWindow.loadFile(path.join(__dirname, '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.whenReady().then(() => { - createWindow(); - - // 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. - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - 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(); - } -}); - -// 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/main.ts b/packages/template/vite/tmpl/main.ts similarity index 90% rename from packages/template/vite-typescript/tmpl/main.ts rename to packages/template/vite/tmpl/main.ts index f4f001e6e5..89dccf9c94 100644 --- a/packages/template/vite-typescript/tmpl/main.ts +++ b/packages/template/vite/tmpl/main.ts @@ -33,7 +33,17 @@ const createWindow = () => { // 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); +app.whenReady().then(() => { + createWindow(); + + // 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. + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + 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 @@ -44,13 +54,5 @@ app.on('window-all-closed', () => { } }); -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/tmpl/package.json b/packages/template/vite/tmpl/package.json index 9b0ddedd37..04cf900c7b 100644 --- a/packages/template/vite/tmpl/package.json +++ b/packages/template/vite/tmpl/package.json @@ -2,12 +2,15 @@ "main": ".vite/build/main.js", "scripts": { "lint": "oxlint && oxfmt --check", - "lint:fix": "oxlint --fix && oxfmt --write" + "lint:fix": "oxlint --fix && oxfmt --write", + "typecheck": "tsc --noEmit" }, "devDependencies": { "@electron-forge/plugin-vite": "ELECTRON_FORGE/VERSION", + "@types/electron-squirrel-startup": "^1.0.2", "oxfmt": "^0.41.0", "oxlint": "^1.0.0", + "typescript": "^5.9.2", "vite": "^8.0.0" } } diff --git a/packages/template/vite/tmpl/preload.js b/packages/template/vite/tmpl/preload.js deleted file mode 100644 index 7a844867f9..0000000000 --- a/packages/template/vite/tmpl/preload.js +++ /dev/null @@ -1,3 +0,0 @@ -//oxlint-disable eslint-plugin-unicorn/no-empty-file -// 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/webpack-typescript/tmpl/preload.ts b/packages/template/vite/tmpl/preload.ts similarity index 74% rename from packages/template/webpack-typescript/tmpl/preload.ts rename to packages/template/vite/tmpl/preload.ts index 7a844867f9..f760e37317 100644 --- a/packages/template/webpack-typescript/tmpl/preload.ts +++ b/packages/template/vite/tmpl/preload.ts @@ -1,3 +1,3 @@ -//oxlint-disable eslint-plugin-unicorn/no-empty-file +// oxlint-disable eslint-plugin-unicorn/no-empty-file // 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/tmpl/renderer.js b/packages/template/vite/tmpl/renderer.js deleted file mode 100644 index 757e88f5f0..0000000000 --- a/packages/template/vite/tmpl/renderer.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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/process-model - * - * 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.js` 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.js", included via Vite', -); diff --git a/packages/template/vite-typescript/tmpl/renderer.ts b/packages/template/vite/tmpl/renderer.ts similarity index 100% rename from packages/template/vite-typescript/tmpl/renderer.ts rename to packages/template/vite/tmpl/renderer.ts diff --git a/packages/template/vite-typescript/tmpl/tsconfig.json b/packages/template/vite/tmpl/tsconfig.json similarity index 100% rename from packages/template/vite-typescript/tmpl/tsconfig.json rename to packages/template/vite/tmpl/tsconfig.json diff --git a/packages/template/vite/tmpl/vite.main.config.mjs b/packages/template/vite/tmpl/vite.main.config.mjs deleted file mode 100644 index 690be5b1a9..0000000000 --- a/packages/template/vite/tmpl/vite.main.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import { defineConfig } from 'vite'; - -// https://vitejs.dev/config -export default defineConfig({}); diff --git a/packages/template/vite-typescript/tmpl/vite.main.config.ts b/packages/template/vite/tmpl/vite.main.config.ts similarity index 100% rename from packages/template/vite-typescript/tmpl/vite.main.config.ts rename to packages/template/vite/tmpl/vite.main.config.ts diff --git a/packages/template/vite/tmpl/vite.preload.config.mjs b/packages/template/vite/tmpl/vite.preload.config.mjs deleted file mode 100644 index 690be5b1a9..0000000000 --- a/packages/template/vite/tmpl/vite.preload.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import { defineConfig } from 'vite'; - -// https://vitejs.dev/config -export default defineConfig({}); diff --git a/packages/template/vite-typescript/tmpl/vite.preload.config.ts b/packages/template/vite/tmpl/vite.preload.config.ts similarity index 100% rename from packages/template/vite-typescript/tmpl/vite.preload.config.ts rename to packages/template/vite/tmpl/vite.preload.config.ts diff --git a/packages/template/vite/tmpl/vite.renderer.config.mjs b/packages/template/vite/tmpl/vite.renderer.config.mjs deleted file mode 100644 index 690be5b1a9..0000000000 --- a/packages/template/vite/tmpl/vite.renderer.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -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/tmpl/vite.renderer.config.ts similarity index 100% rename from packages/template/vite-typescript/tmpl/vite.renderer.config.ts rename to packages/template/vite/tmpl/vite.renderer.config.ts diff --git a/packages/template/webpack-typescript/package.json b/packages/template/webpack-typescript/package.json deleted file mode 100644 index 73f0353b00..0000000000 --- a/packages/template/webpack-typescript/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@electron-forge/template-webpack-typescript", - "version": "8.0.0-alpha.7", - "type": "module", - "description": "Webpack-TypeScript template for Electron Forge", - "repository": "https://github.com/electron/forge", - "author": "Shelley Vohr ", - "license": "MIT", - "exports": "./dist/WebpackTypeScriptTemplate.js", - "typings": "dist/WebpackTypeScriptTemplate.d.ts", - "engines": { - "node": ">= 22.12.0" - }, - "dependencies": { - "@electron-forge/shared-types": "workspace:*", - "@electron-forge/template-base": "workspace:*", - "fs-extra": "^10.0.0", - "typescript": "5.9.3", - "webpack": "^5.69.1" - }, - "devDependencies": { - "@electron-forge/core-utils": "workspace:*", - "@electron-forge/maker-deb": "workspace:*", - "@electron-forge/maker-rpm": "workspace:*", - "@electron-forge/maker-squirrel": "workspace:*", - "@electron-forge/maker-zip": "workspace:*", - "@electron-forge/plugin-webpack": "workspace:*", - "@electron-forge/test-utils": "workspace:*", - "fast-glob": "^3.2.7", - "fork-ts-checker-webpack-plugin": "^7.2.13", - "listr2": "^7.0.2", - "typescript": "5.9.3", - "vitest": "catalog:", - "webpack": "^5.69.1" - }, - "files": [ - "dist", - "src", - "tmpl" - ] -} diff --git a/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts b/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts deleted file mode 100644 index 0527744f33..0000000000 --- a/packages/template/webpack-typescript/spec/WebpackTypeScript.slow.verdaccio.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -import { - PACKAGE_MANAGERS, - spawnPackageManager, -} from '@electron-forge/core-utils'; -import * as testUtils from '@electron-forge/test-utils'; -import glob from 'fast-glob'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; - -// eslint-disable-next-line n/no-missing-import -import { api } from '../../../api/core/dist/api'; -import { init } from '../../../external/create-electron-app/src/init'; - -describe('WebpackTypeScriptTemplate', () => { - let dir: string; - - beforeAll(async () => { - dir = await testUtils.ensureTestDirIsNonexistent(); - await init({ - dir, - template: path.join(import.meta.dirname, '..'), - interactive: false, - electronVersion: '38.2.2', - }); - }); - - describe('template files are copied to project', () => { - it.each([ - 'tsconfig.json', - '.oxlintrc.json', - 'forge.config.ts', - 'webpack.main.config.ts', - 'webpack.renderer.config.ts', - 'webpack.rules.ts', - 'webpack.plugins.ts', - path.join('src', 'index.ts'), - path.join('src', 'renderer.ts'), - path.join('src', 'preload.ts'), - ])(`%s should exist`, async (filename) => { - expect(fs.existsSync(path.join(dir, filename))).toBe(true); - }); - }); - - it('should ensure js source files from base template are removed', async () => { - const jsFiles = await glob(path.join(dir, 'src', '**', '*.js')); - expect(jsFiles.length).toEqual(0); - }); - - it('should contain `private:true` in package.json', async () => { - const packageJSONString = await fs.promises.readFile( - path.join(dir, 'package.json'), - 'utf-8', - ); - const packageJSON = JSON.parse(packageJSONString); - expect(packageJSON).toHaveProperty('private', true); - }); - - it('should contain electron-forge scripts in package.json', async () => { - const packageJSON = JSON.parse( - await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), - ); - expect(packageJSON.scripts.start).toBe('electron-forge start'); - expect(packageJSON.scripts.package).toBe('electron-forge package'); - expect(packageJSON.scripts.make).toBe('electron-forge make'); - expect(packageJSON.scripts.publish).toBe('electron-forge publish'); - }); - - describe('lint', () => { - it('should initially pass the linting process', async () => { - delete process.env.TS_NODE_PROJECT; - await testUtils.expectLintToPass(dir); - }); - }); - - describe('typecheck', () => { - it('should initially pass the typechecking process', async () => { - await testUtils.expectTypecheckToPass(dir); - }); - }); - - describe('package', () => { - let cwd: string; - - beforeAll(async () => { - delete process.env.TS_NODE_PROJECT; - // Webpack resolves plugins via cwd - cwd = process.cwd(); - process.chdir(dir); - }); - - afterAll(() => { - process.chdir(cwd); - }); - - it('should pass', async () => { - await api.package({ - dir, - interactive: false, - }); - }); - }); - - afterAll(async () => { - await spawnPackageManager(PACKAGE_MANAGERS['yarn'], ['unlink', '--all']); - await fs.promises.rm(dir, { recursive: true, force: true }); - }); -}); diff --git a/packages/template/webpack-typescript/src/WebpackTypeScriptTemplate.ts b/packages/template/webpack-typescript/src/WebpackTypeScriptTemplate.ts deleted file mode 100644 index 1c5423a812..0000000000 --- a/packages/template/webpack-typescript/src/WebpackTypeScriptTemplate.ts +++ /dev/null @@ -1,81 +0,0 @@ -import path from 'node:path'; - -import { - ForgeListrTaskDefinition, - InitTemplateOptions, -} from '@electron-forge/shared-types'; -import { BaseTemplate } from '@electron-forge/template-base'; -import fs from 'fs-extra'; - -class WebpackTypeScriptTemplate extends BaseTemplate { - public templateDir = path.resolve(import.meta.dirname, '..', 'tmpl'); - - 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 Webpack files - await this.copyTemplateFile(directory, 'webpack.main.config.ts'); - await this.copyTemplateFile(directory, 'webpack.renderer.config.ts'); - await this.copyTemplateFile(directory, 'webpack.rules.ts'); - await this.copyTemplateFile(directory, 'webpack.plugins.ts'); - - await this.updateFileByLine( - path.resolve(directory, 'src', 'index.html'), - (line) => { - if (line.includes('link rel="stylesheet"')) return null; - return line; - }, - ); - - // Copy tsconfig with a small set of presets - await this.copyTemplateFile(directory, 'tsconfig.json'); - - await this.writeLintConfig(directory); - - // Remove index.js and replace with index.ts - await fs.remove(filePath('index.js')); - await this.copyTemplateFile(path.join(directory, 'src'), 'index.ts'); - - await this.copyTemplateFile( - path.join(directory, 'src'), - 'renderer.ts', - ); - - // Remove preload.js and replace with preload.ts - await fs.remove(filePath('preload.js')); - await this.copyTemplateFile( - path.join(directory, 'src'), - 'preload.ts', - ); - - // update package.json - const packageJSONPath = path.resolve(directory, 'package.json'); - const packageJSON = await fs.readJson(packageJSONPath); - packageJSON.main = '.webpack/main'; - await fs.writeJson(packageJSONPath, packageJSON, { - spaces: 2, - }); - }, - }, - ]; - } -} - -export default new WebpackTypeScriptTemplate(); diff --git a/packages/template/webpack-typescript/tmpl/.oxlintrc.json b/packages/template/webpack-typescript/tmpl/.oxlintrc.json deleted file mode 100644 index 1d845d7f69..0000000000 --- a/packages/template/webpack-typescript/tmpl/.oxlintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "categories": { - "correctness": "warn" - } -} diff --git a/packages/template/webpack-typescript/tmpl/package.json b/packages/template/webpack-typescript/tmpl/package.json deleted file mode 100644 index 2601aea3c4..0000000000 --- a/packages/template/webpack-typescript/tmpl/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "main": ".webpack/main", - "scripts": { - "lint": "oxlint && oxfmt --check", - "lint:fix": "oxlint --fix && oxfmt --write", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@electron-forge/plugin-webpack": "ELECTRON_FORGE/VERSION", - "@vercel/webpack-asset-relocator-loader": "1.7.3", - "css-loader": "^6.0.0", - "fork-ts-checker-webpack-plugin": "^7.2.13", - "node-loader": "^2.0.0", - "oxfmt": "^0.41.0", - "oxlint": "^1.0.0", - "style-loader": "^3.0.0", - "ts-loader": "^9.2.2", - "typescript": "^5.9.2" - } -} diff --git a/packages/template/webpack/spec/WebpackTemplate.spec.ts b/packages/template/webpack/spec/WebpackTemplate.spec.ts index c5bbc91ec5..7247b43e43 100644 --- a/packages/template/webpack/spec/WebpackTemplate.spec.ts +++ b/packages/template/webpack/spec/WebpackTemplate.spec.ts @@ -8,66 +8,248 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import template from '../src/WebpackTemplate'; describe('WebpackTemplate', () => { - let dir: string; + describe('with typescript: false (default)', () => { + let dir: string; - beforeAll(async () => { - dir = await testUtils.ensureTestDirIsNonexistent(); - }); + beforeAll(async () => { + dir = await testUtils.ensureTestDirIsNonexistent(); + }); - afterAll(async () => { - await fs.promises.rm(dir, { recursive: true }); - }); + afterAll(async () => { + await fs.promises.rm(dir, { recursive: true }); + }); - it('should succeed in initializing the webpack template', async () => { - const tasks = await template.initializeTemplate(dir, {}); - const runner = new Listr(tasks, { - concurrent: false, - exitOnError: false, - fallbackRendererCondition: - Boolean(process.env.DEBUG) || Boolean(process.env.CI), + it('should succeed in initializing the template', async () => { + const tasks = await template.initializeTemplate(dir, {}); + const runner = new Listr(tasks, { + concurrent: false, + exitOnError: false, + fallbackRendererCondition: + Boolean(process.env.DEBUG) || Boolean(process.env.CI), + }); + await runner.run(); + expect(runner.errors).toHaveLength(0); }); - await runner.run(); - expect(runner.errors).toHaveLength(0); - }); - describe('template files are copied to project', () => { - const expectedFiles = [ - 'webpack.main.config.js', - 'webpack.renderer.config.js', - 'webpack.rules.js', - path.join('src', 'renderer.js'), - path.join('src', 'preload.js'), - ]; - it.each(expectedFiles)(`%s should exist`, async (filename) => { - const file = path.join(dir, filename); - expect(fs.existsSync(file)).toBe(true); + describe('template files are copied to project', () => { + const expectedFiles = [ + 'package.json', + 'forge.config.mjs', + 'webpack.main.config.js', + 'webpack.renderer.config.js', + 'webpack.rules.js', + path.join('src', 'main.js'), + path.join('src', 'renderer.js'), + path.join('src', 'preload.js'), + ]; + it.each(expectedFiles)(`%s should exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(true); + }); + + const unexpectedFiles = [ + 'forge.config.mts', + 'tsconfig.json', + 'webpack.main.config.ts', + 'webpack.renderer.config.ts', + 'webpack.rules.ts', + 'webpack.plugins.ts', + 'webpack.plugins.js', + path.join('src', 'main.ts'), + path.join('src', 'renderer.ts'), + path.join('src', 'preload.ts'), + ]; + it.each(unexpectedFiles)(`%s should not exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(false); + }); }); - }); - it('should move and rewrite the main process file', async () => { - expect(fs.existsSync(path.join(dir, 'src', 'index.js'))).toBe(false); - expect(fs.existsSync(path.join(dir, 'src', 'main.js'))).toBe(true); - const mainFile = ( - await fs.promises.readFile(path.join(dir, 'src', 'main.js')) - ).toString(); - expect(mainFile).toMatch(/MAIN_WINDOW_WEBPACK_ENTRY/); - expect(mainFile).toMatch(/MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY/); - }); + it('should move and rewrite the main process file', async () => { + expect(fs.existsSync(path.join(dir, 'src', 'index.js'))).toBe(false); + expect(fs.existsSync(path.join(dir, 'src', 'main.js'))).toBe(true); + const mainFile = ( + await fs.promises.readFile(path.join(dir, 'src', 'main.js')) + ).toString(); + expect(mainFile).toMatch(/MAIN_WINDOW_WEBPACK_ENTRY/); + expect(mainFile).toMatch(/MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY/); + }); - it('should remove the stylesheet link from the HTML file', async () => { - expect( - ( - await fs.promises.readFile(path.join(dir, 'src', 'index.html')) - ).toString(), - ).not.toMatch(/link rel="stylesheet"/); + it('should produce valid JavaScript without type annotations', async () => { + const mainFile = ( + await fs.promises.readFile(path.join(dir, 'src', 'main.js')) + ).toString(); + expect(mainFile).not.toMatch(/:\s*(string|number|boolean|void)\b/); + expect(mainFile).not.toMatch(/\binterface\b/); + expect(mainFile).not.toMatch(/\bdeclare\s+const\b/); + }); + + it('should not include ts-loader rule in webpack.rules.js', async () => { + const rules = ( + await fs.promises.readFile(path.join(dir, 'webpack.rules.js')) + ).toString(); + expect(rules).not.toMatch(/ts-loader/); + expect(rules).not.toMatch(/\.tsx\?\$/); + }); + + it('should not include plugins or resolve.extensions in webpack configs', async () => { + for (const name of [ + 'webpack.main.config.js', + 'webpack.renderer.config.js', + ]) { + const config = ( + await fs.promises.readFile(path.join(dir, name)) + ).toString(); + expect(config).not.toMatch(/webpack\.plugins/); + expect(config).not.toMatch(/resolve:/); + expect(config).not.toMatch(/extensions:/); + } + }); + + it('should use string paths in forge.config.mjs for JS variant', async () => { + const config = ( + await fs.promises.readFile(path.join(dir, 'forge.config.mjs')) + ).toString(); + expect(config).toMatch(/webpack\.main\.config\.js/); + expect(config).toMatch(/webpack\.renderer\.config\.js/); + expect(config).toMatch(/src\/renderer\.js/); + expect(config).toMatch(/src\/preload\.js/); + }); + + it('should remove the stylesheet link from the HTML file', async () => { + expect( + ( + await fs.promises.readFile(path.join(dir, 'src', 'index.html')) + ).toString(), + ).not.toMatch(/link rel="stylesheet"/); + }); + + it('should not include typecheck script in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON.scripts.typecheck).toBeUndefined(); + }); + + it('should contain `private:true` in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON).toHaveProperty('private', true); + }); }); - it('should contain `private:true` in package.json', async () => { - const packageJSONString = await fs.promises.readFile( - path.join(dir, 'package.json'), - 'utf-8', - ); - const packageJSON = JSON.parse(packageJSONString); - expect(packageJSON).toHaveProperty('private', true); + describe('with typescript: true', () => { + let dir: string; + + beforeAll(async () => { + dir = await testUtils.ensureTestDirIsNonexistent(); + }); + + afterAll(async () => { + await fs.promises.rm(dir, { recursive: true }); + }); + + it('should succeed in initializing the template', async () => { + const tasks = await template.initializeTemplate(dir, { + typescript: true, + }); + const runner = new Listr(tasks, { + concurrent: false, + exitOnError: false, + fallbackRendererCondition: + Boolean(process.env.DEBUG) || Boolean(process.env.CI), + }); + await runner.run(); + expect(runner.errors).toHaveLength(0); + }); + + describe('template files are copied to project', () => { + const expectedFiles = [ + 'package.json', + 'forge.config.mts', + 'tsconfig.json', + 'webpack.main.config.ts', + 'webpack.renderer.config.ts', + 'webpack.rules.ts', + 'webpack.plugins.ts', + path.join('src', 'main.ts'), + path.join('src', 'renderer.ts'), + path.join('src', 'preload.ts'), + ]; + it.each(expectedFiles)(`%s should exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(true); + }); + + const unexpectedFiles = [ + 'forge.config.mjs', + 'webpack.main.config.js', + 'webpack.renderer.config.js', + 'webpack.rules.js', + 'webpack.plugins.js', + path.join('src', 'main.js'), + path.join('src', 'renderer.js'), + path.join('src', 'preload.js'), + ]; + it.each(unexpectedFiles)(`%s should not exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(false); + }); + }); + + it('should move and rewrite the main process file', async () => { + expect(fs.existsSync(path.join(dir, 'src', 'index.js'))).toBe(false); + expect(fs.existsSync(path.join(dir, 'src', 'main.ts'))).toBe(true); + const mainFile = ( + await fs.promises.readFile(path.join(dir, 'src', 'main.ts')) + ).toString(); + expect(mainFile).toMatch(/MAIN_WINDOW_WEBPACK_ENTRY/); + expect(mainFile).toMatch(/MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY/); + }); + + it('should include ts-loader rule in webpack.rules.ts', async () => { + const rules = ( + await fs.promises.readFile(path.join(dir, 'webpack.rules.ts')) + ).toString(); + expect(rules).toMatch(/ts-loader/); + }); + + it('should include webpack.plugins.ts with fork-ts-checker', async () => { + const plugins = ( + await fs.promises.readFile(path.join(dir, 'webpack.plugins.ts')) + ).toString(); + expect(plugins).toMatch(/ForkTsCheckerWebpackPlugin/); + }); + + it('should include resolve.extensions in webpack configs', async () => { + for (const name of [ + 'webpack.main.config.ts', + 'webpack.renderer.config.ts', + ]) { + const config = ( + await fs.promises.readFile(path.join(dir, name)) + ).toString(); + expect(config).toMatch(/extensions:/); + } + }); + + it('should remove the stylesheet link from the HTML file', async () => { + expect( + ( + await fs.promises.readFile(path.join(dir, 'src', 'index.html')) + ).toString(), + ).not.toMatch(/link rel="stylesheet"/); + }); + + it('should include typecheck script in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON.scripts.typecheck).toBeDefined(); + }); + + it('should contain `private:true` in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON).toHaveProperty('private', true); + }); }); }); diff --git a/packages/template/webpack/src/WebpackTemplate.ts b/packages/template/webpack/src/WebpackTemplate.ts index 98ae909fdc..5297c47acd 100644 --- a/packages/template/webpack/src/WebpackTemplate.ts +++ b/packages/template/webpack/src/WebpackTemplate.ts @@ -5,52 +5,136 @@ import { InitTemplateOptions, } from '@electron-forge/shared-types'; import { BaseTemplate } from '@electron-forge/template-base'; +import fs from 'fs-extra'; + +const TS_ONLY_DEV_DEPS = new Set([ + 'fork-ts-checker-webpack-plugin', + 'ts-loader', + 'typescript', +]); + +const TS_ONLY_SCRIPTS = new Set(['typecheck']); class WebpackTemplate extends BaseTemplate { public templateDir = path.resolve(import.meta.dirname, '..', 'tmpl'); + public override get devDependencies(): string[] { + const all = super.devDependencies; + if (this._typescript) return all; + return all.filter((dep) => { + const name = dep.replace(/@[^@]*$/, ''); + return !TS_ONLY_DEV_DEPS.has(name); + }); + } + + private _typescript = false; + public async initializeTemplate( directory: string, options: InitTemplateOptions, ): Promise { + const typescript = options.typescript ?? false; + this._typescript = typescript; const superTasks = await super.initializeTemplate(directory, options); + return [ ...superTasks, { title: 'Setting up Forge configuration', task: async () => { - await this.copyTemplateFile(directory, 'forge.config.js'); + await fs.remove(path.resolve(directory, 'forge.config.js')); + + if (typescript) { + await this.copyTemplateFile(directory, 'forge.config.mts'); + } else { + await this.copyTemplateFile(directory, 'forge.config.mts'); + await this.stripAndRename( + path.resolve(directory, 'forge.config.mts'), + path.resolve(directory, 'forge.config.mjs'), + ); + // For JS, replace module imports with string-based config paths + // and patch file references from .ts to .js + await this.updateFileByLine( + path.resolve(directory, 'forge.config.mjs'), + (line) => { + // Remove webpack config imports (JS uses string paths instead) + if (line.includes("from './webpack.")) return null; + // Replace object reference with string path + if (line.includes('mainConfig,')) + return line.replace( + 'mainConfig,', + "'./webpack.main.config.js',", + ); + if (/config:\s*rendererConfig,/.test(line)) + return line.replace( + 'rendererConfig,', + "'./webpack.renderer.config.js',", + ); + return line + .replace(/src\/renderer\.ts/g, 'src/renderer.js') + .replace(/src\/preload\.ts/g, 'src/preload.js'); + }, + ); + } }, }, { - title: 'Setting up webpack configuration', + title: `Setting up ${typescript ? 'TypeScript and webpack' : 'webpack'} configuration`, task: async () => { - await this.copyTemplateFile(directory, 'webpack.main.config.js'); - await this.copyTemplateFile(directory, 'webpack.renderer.config.js'); - await this.copyTemplateFile(directory, 'webpack.rules.js'); + if (typescript) { + // Copy all webpack config files as-is + await this.copyTemplateFile(directory, 'webpack.main.config.ts'); + await this.copyTemplateFile( + directory, + 'webpack.renderer.config.ts', + ); + await this.copyTemplateFile(directory, 'webpack.rules.ts'); + await this.copyTemplateFile(directory, 'webpack.plugins.ts'); + await this.copyTemplateFile(directory, 'tsconfig.json'); + } else { + for (const name of [ + 'webpack.main.config.js', + 'webpack.renderer.config.js', + 'webpack.rules.js', + ]) { + await this.copy( + path.join(this.templateDir, 'js', name), + path.resolve(directory, name), + ); + } + } await this.writeLintConfig(directory); - await this.copyTemplateFile( - path.join(directory, 'src'), - 'renderer.js', - ); - await this.copyTemplateFile( - path.join(directory, 'src'), - 'preload.js', - ); - await this.updateFileByLine( - path.resolve(directory, 'src', 'index.js'), - (line) => { - if (line.includes('mainWindow.loadFile')) - return ' mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);'; - if (line.includes('preload: ')) - return ' preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,'; - return line; - }, - path.resolve(directory, 'src', 'main.js'), - ); + // Remove base template's JS source files + await fs.remove(path.resolve(directory, 'src', 'index.js')); + await fs.remove(path.resolve(directory, 'src', 'preload.js')); + // Copy and process source files + if (typescript) { + 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'), + 'preload.ts', + ); + } else { + for (const name of ['main', 'renderer', 'preload']) { + await this.copyTemplateFile( + path.join(directory, 'src'), + `${name}.ts`, + ); + await this.stripAndRename( + path.resolve(directory, 'src', `${name}.ts`), + path.resolve(directory, 'src', `${name}.js`), + ); + } + } + + // Remove CSS link from index.html await this.updateFileByLine( path.resolve(directory, 'src', 'index.html'), (line) => { @@ -58,6 +142,16 @@ class WebpackTemplate extends BaseTemplate { return line; }, ); + + // Remove TS-only scripts from package.json for JS variant + if (!typescript) { + const packageJSONPath = path.resolve(directory, 'package.json'); + const packageJSON = await fs.readJson(packageJSONPath); + for (const script of TS_ONLY_SCRIPTS) { + delete packageJSON.scripts[script]; + } + await fs.writeJson(packageJSONPath, packageJSON, { spaces: 2 }); + } }, }, ]; diff --git a/packages/template/webpack/tmpl/forge.config.js b/packages/template/webpack/tmpl/forge.config.js deleted file mode 100644 index d6aa49c127..0000000000 --- a/packages/template/webpack/tmpl/forge.config.js +++ /dev/null @@ -1,63 +0,0 @@ -const { FusesPlugin } = require('@electron-forge/plugin-fuses'); -const { FuseV1Options, FuseVersion } = require('@electron/fuses'); - -module.exports = { - packagerConfig: { - asar: true, - }, - rebuildConfig: {}, - makers: [ - { - name: '@electron-forge/maker-squirrel', - config: {}, - }, - { - name: '@electron-forge/maker-zip', - platforms: ['darwin'], - }, - { - name: '@electron-forge/maker-deb', - config: {}, - }, - { - name: '@electron-forge/maker-rpm', - config: {}, - }, - ], - plugins: [ - { - name: '@electron-forge/plugin-auto-unpack-natives', - config: {}, - }, - { - name: '@electron-forge/plugin-webpack', - config: { - mainConfig: './webpack.main.config.js', - renderer: { - config: './webpack.renderer.config.js', - entryPoints: [ - { - html: './src/index.html', - js: './src/renderer.js', - name: 'main_window', - preload: { - js: './src/preload.js', - }, - }, - ], - }, - }, - }, - // Fuses are used to enable/disable various Electron functionality - // at package time, before code signing the application - new FusesPlugin({ - version: FuseVersion.V1, - [FuseV1Options.RunAsNode]: false, - [FuseV1Options.EnableCookieEncryption]: true, - [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, - [FuseV1Options.EnableNodeCliInspectArguments]: false, - [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, - [FuseV1Options.OnlyLoadAppFromAsar]: true, - }), - ], -}; diff --git a/packages/template/webpack-typescript/tmpl/forge.config.ts b/packages/template/webpack/tmpl/forge.config.mts similarity index 100% rename from packages/template/webpack-typescript/tmpl/forge.config.ts rename to packages/template/webpack/tmpl/forge.config.mts diff --git a/packages/template/webpack/tmpl/webpack.main.config.js b/packages/template/webpack/tmpl/js/webpack.main.config.js similarity index 61% rename from packages/template/webpack/tmpl/webpack.main.config.js rename to packages/template/webpack/tmpl/js/webpack.main.config.js index d23d0e363a..5366a111d4 100644 --- a/packages/template/webpack/tmpl/webpack.main.config.js +++ b/packages/template/webpack/tmpl/js/webpack.main.config.js @@ -1,11 +1,12 @@ -module.exports = { +import { rules } from './webpack.rules'; + +export const mainConfig = { /** * This is the main entry point for your application, it's the first file * that runs in the main process. */ entry: './src/main.js', - // Put your normal webpack config below here module: { - rules: require('./webpack.rules'), + rules, }, }; diff --git a/packages/template/webpack/tmpl/webpack.renderer.config.js b/packages/template/webpack/tmpl/js/webpack.renderer.config.js similarity index 54% rename from packages/template/webpack/tmpl/webpack.renderer.config.js rename to packages/template/webpack/tmpl/js/webpack.renderer.config.js index e5f6a64c57..6701473271 100644 --- a/packages/template/webpack/tmpl/webpack.renderer.config.js +++ b/packages/template/webpack/tmpl/js/webpack.renderer.config.js @@ -1,12 +1,11 @@ -const rules = require('./webpack.rules'); +import { rules } from './webpack.rules'; rules.push({ test: /\.css$/, use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], }); -module.exports = { - // Put your normal webpack config below here +export const rendererConfig = { module: { rules, }, diff --git a/packages/template/webpack/tmpl/js/webpack.rules.js b/packages/template/webpack/tmpl/js/webpack.rules.js new file mode 100644 index 0000000000..4c7e516045 --- /dev/null +++ b/packages/template/webpack/tmpl/js/webpack.rules.js @@ -0,0 +1,16 @@ +export const rules = [ + { + test: /native_modules[/\\].+\.node$/, + use: 'node-loader', + }, + { + test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, + parser: { amd: false }, + use: { + loader: '@vercel/webpack-asset-relocator-loader', + options: { + outputAssetBase: 'native_modules', + }, + }, + }, +]; diff --git a/packages/template/webpack-typescript/tmpl/index.ts b/packages/template/webpack/tmpl/main.ts similarity index 91% rename from packages/template/webpack-typescript/tmpl/index.ts rename to packages/template/webpack/tmpl/main.ts index 7123c571bf..f0e15df4fe 100644 --- a/packages/template/webpack-typescript/tmpl/index.ts +++ b/packages/template/webpack/tmpl/main.ts @@ -31,7 +31,17 @@ const createWindow = (): void => { // 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); +app.whenReady().then(() => { + createWindow(); + + // 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. + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + 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 @@ -42,13 +52,5 @@ app.on('window-all-closed', () => { } }); -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/webpack/tmpl/package.json b/packages/template/webpack/tmpl/package.json index 1ec5122187..2601aea3c4 100644 --- a/packages/template/webpack/tmpl/package.json +++ b/packages/template/webpack/tmpl/package.json @@ -2,15 +2,19 @@ "main": ".webpack/main", "scripts": { "lint": "oxlint && oxfmt --check", - "lint:fix": "oxlint --fix && oxfmt --write" + "lint:fix": "oxlint --fix && oxfmt --write", + "typecheck": "tsc --noEmit" }, "devDependencies": { "@electron-forge/plugin-webpack": "ELECTRON_FORGE/VERSION", "@vercel/webpack-asset-relocator-loader": "1.7.3", "css-loader": "^6.0.0", + "fork-ts-checker-webpack-plugin": "^7.2.13", "node-loader": "^2.0.0", "oxfmt": "^0.41.0", "oxlint": "^1.0.0", - "style-loader": "^3.0.0" + "style-loader": "^3.0.0", + "ts-loader": "^9.2.2", + "typescript": "^5.9.2" } } diff --git a/packages/template/webpack/tmpl/preload.js b/packages/template/webpack/tmpl/preload.js deleted file mode 100644 index 7a844867f9..0000000000 --- a/packages/template/webpack/tmpl/preload.js +++ /dev/null @@ -1,3 +0,0 @@ -//oxlint-disable eslint-plugin-unicorn/no-empty-file -// 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/preload.ts b/packages/template/webpack/tmpl/preload.ts similarity index 100% rename from packages/template/vite-typescript/tmpl/preload.ts rename to packages/template/webpack/tmpl/preload.ts diff --git a/packages/template/webpack/tmpl/renderer.js b/packages/template/webpack/tmpl/renderer.js deleted file mode 100644 index 80985d62fa..0000000000 --- a/packages/template/webpack/tmpl/renderer.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * This file will automatically be loaded by webpack 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/process-model - * - * 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.js` 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.js", included via webpack', -); diff --git a/packages/template/webpack-typescript/tmpl/renderer.ts b/packages/template/webpack/tmpl/renderer.ts similarity index 86% rename from packages/template/webpack-typescript/tmpl/renderer.ts rename to packages/template/webpack/tmpl/renderer.ts index c518b8092f..e609cab7c5 100644 --- a/packages/template/webpack-typescript/tmpl/renderer.ts +++ b/packages/template/webpack/tmpl/renderer.ts @@ -11,7 +11,7 @@ * * https://electronjs.org/docs/tutorial/security * - * To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration` + * To enable Node.js integration in this file, open up `main.ts` and enable the `nodeIntegration` * flag: * * ``` @@ -29,5 +29,5 @@ import './index.css'; console.log( - '👋 This message is being logged by "renderer.js", included via webpack', + '👋 This message is being logged by "renderer.ts", included via webpack', ); diff --git a/packages/template/webpack-typescript/tmpl/tsconfig.json b/packages/template/webpack/tmpl/tsconfig.json similarity index 100% rename from packages/template/webpack-typescript/tmpl/tsconfig.json rename to packages/template/webpack/tmpl/tsconfig.json diff --git a/packages/template/webpack-typescript/tmpl/webpack.main.config.ts b/packages/template/webpack/tmpl/webpack.main.config.ts similarity index 94% rename from packages/template/webpack-typescript/tmpl/webpack.main.config.ts rename to packages/template/webpack/tmpl/webpack.main.config.ts index 5ed911f841..5e801b5e45 100644 --- a/packages/template/webpack-typescript/tmpl/webpack.main.config.ts +++ b/packages/template/webpack/tmpl/webpack.main.config.ts @@ -8,7 +8,7 @@ export const mainConfig: Configuration = { * This is the main entry point for your application, it's the first file * that runs in the main process. */ - entry: './src/index.ts', + entry: './src/main.ts', // Put your normal webpack config below here module: { rules, diff --git a/packages/template/webpack-typescript/tmpl/webpack.plugins.ts b/packages/template/webpack/tmpl/webpack.plugins.ts similarity index 100% rename from packages/template/webpack-typescript/tmpl/webpack.plugins.ts rename to packages/template/webpack/tmpl/webpack.plugins.ts diff --git a/packages/template/webpack-typescript/tmpl/webpack.renderer.config.ts b/packages/template/webpack/tmpl/webpack.renderer.config.ts similarity index 100% rename from packages/template/webpack-typescript/tmpl/webpack.renderer.config.ts rename to packages/template/webpack/tmpl/webpack.renderer.config.ts diff --git a/packages/template/webpack/tmpl/webpack.rules.js b/packages/template/webpack/tmpl/webpack.rules.js deleted file mode 100644 index 23cb22a35f..0000000000 --- a/packages/template/webpack/tmpl/webpack.rules.js +++ /dev/null @@ -1,35 +0,0 @@ -module.exports = [ - // Add support for native node modules - { - // We're specifying native_modules in the test because the asset relocator loader generates a - // "fake" .node file which is really a cjs file. - test: /native_modules[/\\].+\.node$/, - use: 'node-loader', - }, - { - test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, - parser: { amd: false }, - use: { - loader: '@vercel/webpack-asset-relocator-loader', - options: { - outputAssetBase: 'native_modules', - }, - }, - }, - // Put your webpack loader rules in this array. This is where you would put - // your ts-loader configuration for instance: - /** - * Typescript Example: - * - * { - * test: /\.tsx?$/, - * exclude: /(node_modules|.webpack)/, - * loaders: [{ - * loader: 'ts-loader', - * options: { - * transpileOnly: true - * } - * }] - * } - */ -]; diff --git a/packages/template/webpack-typescript/tmpl/webpack.rules.ts b/packages/template/webpack/tmpl/webpack.rules.ts similarity index 100% rename from packages/template/webpack-typescript/tmpl/webpack.rules.ts rename to packages/template/webpack/tmpl/webpack.rules.ts diff --git a/packages/utils/types/src/index.ts b/packages/utils/types/src/index.ts index 63836a1435..5c65bfb780 100644 --- a/packages/utils/types/src/index.ts +++ b/packages/utils/types/src/index.ts @@ -281,6 +281,7 @@ export type StartResult = export interface InitTemplateOptions { copyCIFiles?: boolean; force?: boolean; + typescript?: boolean; } // eslint-disable-next-line @typescript-eslint/no-explicit-any From 59c6f65a1e6a645e55d4df756b449adea6a06957 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Apr 2026 12:34:25 -0700 Subject: [PATCH 02/22] hmm --- .../external/create-electron-app/package.json | 2 - packages/template/base/package.json | 2 +- yarn.lock | 40 +------------------ 3 files changed, 3 insertions(+), 41 deletions(-) diff --git a/packages/external/create-electron-app/package.json b/packages/external/create-electron-app/package.json index 57d5c95ac3..10f59cc318 100644 --- a/packages/external/create-electron-app/package.json +++ b/packages/external/create-electron-app/package.json @@ -16,9 +16,7 @@ "@electron-forge/shared-types": "workspace:*", "@electron-forge/template-base": "workspace:*", "@electron-forge/template-vite": "workspace:*", - "@electron-forge/template-vite-typescript": "workspace:*", "@electron-forge/template-webpack": "workspace:*", - "@electron-forge/template-webpack-typescript": "workspace:*", "@inquirer/prompts": "^6.0.1", "@listr2/prompt-adapter-inquirer": "^2.0.22", "@malept/cross-spawn-promise": "^2.0.0", diff --git a/packages/template/base/package.json b/packages/template/base/package.json index 5013553ac5..df589b80a3 100644 --- a/packages/template/base/package.json +++ b/packages/template/base/package.json @@ -15,9 +15,9 @@ "@electron-forge/core-utils": "workspace:*", "@electron-forge/shared-types": "workspace:*", "@malept/cross-spawn-promise": "^2.0.0", - "oxfmt": "^0.41.0", "debug": "^4.3.1", "fs-extra": "^10.0.0", + "oxfmt": "^0.41.0", "semver": "^7.2.1", "username": "^5.1.0" }, diff --git a/yarn.lock b/yarn.lock index 513e905f68..d98624c8e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1060,7 +1060,7 @@ __metadata: languageName: unknown linkType: soft -"@electron-forge/plugin-webpack@workspace:*, @electron-forge/plugin-webpack@workspace:packages/plugin/webpack": +"@electron-forge/plugin-webpack@workspace:packages/plugin/webpack": version: 0.0.0-use.local resolution: "@electron-forge/plugin-webpack@workspace:packages/plugin/webpack" dependencies: @@ -1216,6 +1216,7 @@ __metadata: "@malept/cross-spawn-promise": "npm:^2.0.0" debug: "npm:^4.3.1" fs-extra: "npm:^10.0.0" + oxfmt: "npm:^0.41.0" semver: "npm:^7.2.1" username: "npm:^5.1.0" vitest: "catalog:" @@ -1228,19 +1229,6 @@ __metadata: languageName: node linkType: soft -"@electron-forge/template-vite-typescript@workspace:*, @electron-forge/template-vite-typescript@workspace:packages/template/vite-typescript": - version: 0.0.0-use.local - resolution: "@electron-forge/template-vite-typescript@workspace:packages/template/vite-typescript" - dependencies: - "@electron-forge/core-utils": "workspace:*" - "@electron-forge/shared-types": "workspace:*" - "@electron-forge/template-base": "workspace:*" - "@electron-forge/test-utils": "workspace:*" - fs-extra: "npm:^10.0.0" - vitest: "catalog:" - languageName: unknown - linkType: soft - "@electron-forge/template-vite@workspace:*, @electron-forge/template-vite@workspace:packages/template/vite": version: 0.0.0-use.local resolution: "@electron-forge/template-vite@workspace:packages/template/vite" @@ -1254,28 +1242,6 @@ __metadata: languageName: unknown linkType: soft -"@electron-forge/template-webpack-typescript@workspace:*, @electron-forge/template-webpack-typescript@workspace:packages/template/webpack-typescript": - version: 0.0.0-use.local - resolution: "@electron-forge/template-webpack-typescript@workspace:packages/template/webpack-typescript" - dependencies: - "@electron-forge/core-utils": "workspace:*" - "@electron-forge/maker-deb": "workspace:*" - "@electron-forge/maker-rpm": "workspace:*" - "@electron-forge/maker-squirrel": "workspace:*" - "@electron-forge/maker-zip": "workspace:*" - "@electron-forge/plugin-webpack": "workspace:*" - "@electron-forge/shared-types": "workspace:*" - "@electron-forge/template-base": "workspace:*" - "@electron-forge/test-utils": "workspace:*" - fork-ts-checker-webpack-plugin: "npm:^7.2.13" - fs-extra: "npm:^10.0.0" - listr2: "npm:^7.0.2" - typescript: "npm:5.9.3" - vitest: "catalog:" - webpack: "npm:^5.69.1" - languageName: unknown - linkType: soft - "@electron-forge/template-webpack@workspace:*, @electron-forge/template-webpack@workspace:packages/template/webpack": version: 0.0.0-use.local resolution: "@electron-forge/template-webpack@workspace:packages/template/webpack" @@ -7194,9 +7160,7 @@ __metadata: "@electron-forge/shared-types": "workspace:*" "@electron-forge/template-base": "workspace:*" "@electron-forge/template-vite": "workspace:*" - "@electron-forge/template-vite-typescript": "workspace:*" "@electron-forge/template-webpack": "workspace:*" - "@electron-forge/template-webpack-typescript": "workspace:*" "@electron-forge/test-utils": "workspace:*" "@inquirer/prompts": "npm:^6.0.1" "@listr2/prompt-adapter-inquirer": "npm:^2.0.22" From 7bf5ea70168bf55e0034888992a2bef7ab7bd60c Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Apr 2026 12:51:24 -0700 Subject: [PATCH 03/22] add slow tests back --- .../spec/ViteTemplate.slow.verdaccio.spec.ts | 203 ++++++++++++++++++ .../WebpackTemplate.slow.verdaccio.spec.ts | 202 +++++++++++++++++ 2 files changed, 405 insertions(+) create mode 100644 packages/template/vite/spec/ViteTemplate.slow.verdaccio.spec.ts create mode 100644 packages/template/webpack/spec/WebpackTemplate.slow.verdaccio.spec.ts diff --git a/packages/template/vite/spec/ViteTemplate.slow.verdaccio.spec.ts b/packages/template/vite/spec/ViteTemplate.slow.verdaccio.spec.ts new file mode 100644 index 0000000000..61bcb676b3 --- /dev/null +++ b/packages/template/vite/spec/ViteTemplate.slow.verdaccio.spec.ts @@ -0,0 +1,203 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { + PACKAGE_MANAGERS, + spawnPackageManager, +} from '@electron-forge/core-utils'; +import * as testUtils from '@electron-forge/test-utils'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +// eslint-disable-next-line n/no-missing-import +import { api } from '../../../api/core/dist/api'; +import { init } from '../../../external/create-electron-app/src/init'; + +describe('ViteTemplate (TypeScript)', () => { + let dir: string; + + beforeAll(async () => { + dir = await testUtils.ensureTestDirIsNonexistent(); + await init({ + dir, + template: 'vite', + typescript: true, + interactive: false, + electronVersion: '38.2.2', + }); + }); + + afterAll(async () => { + await spawnPackageManager(PACKAGE_MANAGERS['yarn'], ['unlink', '--all']); + if (os.platform() !== 'win32') { + await fs.promises.rm(dir, { force: true, recursive: true }); + } + }); + + describe('template files are copied to project', () => { + it.each([ + 'package.json', + 'tsconfig.json', + '.oxlintrc.json', + 'forge.config.mts', + 'vite.main.config.ts', + 'vite.preload.config.ts', + 'vite.renderer.config.ts', + path.join('src', 'main.ts'), + path.join('src', 'renderer.ts'), + path.join('src', 'preload.ts'), + ])(`%s should exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(true); + }); + + it('should ensure js source files from base template are removed', async () => { + const jsFiles = await Array.fromAsync( + fs.promises.glob(path.join(dir, 'src', '**', '*.js')), + ); + expect(jsFiles.length).toEqual(0); + }); + + it('should contain `private:true` in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON).toHaveProperty('private', true); + }); + + it('should contain electron-forge scripts in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON.scripts.start).toBe('electron-forge start'); + expect(packageJSON.scripts.package).toBe('electron-forge package'); + expect(packageJSON.scripts.make).toBe('electron-forge make'); + expect(packageJSON.scripts.publish).toBe('electron-forge publish'); + }); + }); + + describe('lint', () => { + it('should initially pass the linting process', async () => { + delete process.env.TS_NODE_PROJECT; + await testUtils.expectLintToPass(dir); + }); + }); + + describe('typecheck', () => { + it('should initially pass the typechecking process', async () => { + await testUtils.expectTypecheckToPass(dir); + }); + }); + + describe('package', () => { + let cwd: string; + + beforeAll(async () => { + delete process.env.TS_NODE_PROJECT; + cwd = process.cwd(); + process.chdir(dir); + }); + + afterAll(() => { + process.chdir(cwd); + }); + + it('should pass', async () => { + await api.package({ + dir, + interactive: false, + }); + }); + }); +}); + +describe('ViteTemplate (JavaScript)', () => { + let dir: string; + + beforeAll(async () => { + dir = await testUtils.ensureTestDirIsNonexistent(); + await init({ + dir, + template: 'vite', + typescript: false, + interactive: false, + electronVersion: '38.2.2', + }); + }); + + afterAll(async () => { + await spawnPackageManager(PACKAGE_MANAGERS['yarn'], ['unlink', '--all']); + if (os.platform() !== 'win32') { + await fs.promises.rm(dir, { force: true, recursive: true }); + } + }); + + describe('template files are copied to project', () => { + it.each([ + 'package.json', + '.oxlintrc.json', + 'forge.config.mjs', + 'vite.main.config.mjs', + 'vite.preload.config.mjs', + 'vite.renderer.config.mjs', + path.join('src', 'main.js'), + path.join('src', 'renderer.js'), + path.join('src', 'preload.js'), + ])(`%s should exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(true); + }); + + it('should ensure ts source files are not present', async () => { + const tsFiles = await Array.fromAsync( + fs.promises.glob(path.join(dir, 'src', '**', '*.ts')), + ); + expect(tsFiles.length).toEqual(0); + }); + + it('should not have tsconfig.json', async () => { + expect(fs.existsSync(path.join(dir, 'tsconfig.json'))).toBe(false); + }); + + it('should contain `private:true` in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON).toHaveProperty('private', true); + }); + + it('should contain electron-forge scripts in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON.scripts.start).toBe('electron-forge start'); + expect(packageJSON.scripts.package).toBe('electron-forge package'); + expect(packageJSON.scripts.make).toBe('electron-forge make'); + expect(packageJSON.scripts.publish).toBe('electron-forge publish'); + }); + }); + + describe('lint', () => { + it('should initially pass the linting process', async () => { + await testUtils.expectLintToPass(dir); + }); + }); + + describe('package', () => { + let cwd: string; + + beforeAll(async () => { + cwd = process.cwd(); + process.chdir(dir); + }); + + afterAll(() => { + process.chdir(cwd); + }); + + it('should pass', async () => { + await api.package({ + dir, + interactive: false, + }); + }); + }); +}); diff --git a/packages/template/webpack/spec/WebpackTemplate.slow.verdaccio.spec.ts b/packages/template/webpack/spec/WebpackTemplate.slow.verdaccio.spec.ts new file mode 100644 index 0000000000..c7e449de33 --- /dev/null +++ b/packages/template/webpack/spec/WebpackTemplate.slow.verdaccio.spec.ts @@ -0,0 +1,202 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { + PACKAGE_MANAGERS, + spawnPackageManager, +} from '@electron-forge/core-utils'; +import * as testUtils from '@electron-forge/test-utils'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +// eslint-disable-next-line n/no-missing-import +import { api } from '../../../api/core/dist/api'; +import { init } from '../../../external/create-electron-app/src/init'; + +describe('WebpackTemplate (TypeScript)', () => { + let dir: string; + + beforeAll(async () => { + dir = await testUtils.ensureTestDirIsNonexistent(); + await init({ + dir, + template: 'webpack', + typescript: true, + interactive: false, + electronVersion: '38.2.2', + }); + }); + + afterAll(async () => { + await spawnPackageManager(PACKAGE_MANAGERS['yarn'], ['unlink', '--all']); + await fs.promises.rm(dir, { recursive: true, force: true }); + }); + + describe('template files are copied to project', () => { + it.each([ + 'tsconfig.json', + '.oxlintrc.json', + 'forge.config.mts', + 'webpack.main.config.ts', + 'webpack.renderer.config.ts', + 'webpack.rules.ts', + 'webpack.plugins.ts', + path.join('src', 'main.ts'), + path.join('src', 'renderer.ts'), + path.join('src', 'preload.ts'), + ])(`%s should exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(true); + }); + + it('should ensure js source files from base template are removed', async () => { + const jsFiles = await Array.fromAsync( + fs.promises.glob(path.join(dir, 'src', '**', '*.js')), + ); + expect(jsFiles.length).toEqual(0); + }); + + it('should contain `private:true` in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON).toHaveProperty('private', true); + }); + + it('should contain electron-forge scripts in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON.scripts.start).toBe('electron-forge start'); + expect(packageJSON.scripts.package).toBe('electron-forge package'); + expect(packageJSON.scripts.make).toBe('electron-forge make'); + expect(packageJSON.scripts.publish).toBe('electron-forge publish'); + }); + }); + + describe('lint', () => { + it('should initially pass the linting process', async () => { + delete process.env.TS_NODE_PROJECT; + await testUtils.expectLintToPass(dir); + }); + }); + + describe('typecheck', () => { + it('should initially pass the typechecking process', async () => { + await testUtils.expectTypecheckToPass(dir); + }); + }); + + describe('package', () => { + let cwd: string; + + beforeAll(async () => { + delete process.env.TS_NODE_PROJECT; + cwd = process.cwd(); + process.chdir(dir); + }); + + afterAll(() => { + process.chdir(cwd); + }); + + it('should pass', async () => { + await api.package({ + dir, + interactive: false, + }); + }); + }); +}); + +describe('WebpackTemplate (JavaScript)', () => { + let dir: string; + + beforeAll(async () => { + dir = await testUtils.ensureTestDirIsNonexistent(); + await init({ + dir, + template: 'webpack', + typescript: false, + interactive: false, + electronVersion: '38.2.2', + }); + }); + + afterAll(async () => { + await spawnPackageManager(PACKAGE_MANAGERS['yarn'], ['unlink', '--all']); + await fs.promises.rm(dir, { recursive: true, force: true }); + }); + + describe('template files are copied to project', () => { + it.each([ + '.oxlintrc.json', + 'forge.config.mjs', + 'webpack.main.config.js', + 'webpack.renderer.config.js', + 'webpack.rules.js', + path.join('src', 'main.js'), + path.join('src', 'renderer.js'), + path.join('src', 'preload.js'), + ])(`%s should exist`, async (filename) => { + expect(fs.existsSync(path.join(dir, filename))).toBe(true); + }); + + it('should ensure ts source files are not present', async () => { + const tsFiles = await Array.fromAsync( + fs.promises.glob(path.join(dir, 'src', '**', '*.ts')), + ); + expect(tsFiles.length).toEqual(0); + }); + + it('should not have tsconfig.json', async () => { + expect(fs.existsSync(path.join(dir, 'tsconfig.json'))).toBe(false); + }); + + it('should not have webpack.plugins.ts or webpack.plugins.js', async () => { + expect(fs.existsSync(path.join(dir, 'webpack.plugins.ts'))).toBe(false); + expect(fs.existsSync(path.join(dir, 'webpack.plugins.js'))).toBe(false); + }); + + it('should contain `private:true` in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON).toHaveProperty('private', true); + }); + + it('should contain electron-forge scripts in package.json', async () => { + const packageJSON = JSON.parse( + await fs.promises.readFile(path.join(dir, 'package.json'), 'utf-8'), + ); + expect(packageJSON.scripts.start).toBe('electron-forge start'); + expect(packageJSON.scripts.package).toBe('electron-forge package'); + expect(packageJSON.scripts.make).toBe('electron-forge make'); + expect(packageJSON.scripts.publish).toBe('electron-forge publish'); + }); + }); + + describe('lint', () => { + it('should initially pass the linting process', async () => { + await testUtils.expectLintToPass(dir); + }); + }); + + describe('package', () => { + let cwd: string; + + beforeAll(async () => { + cwd = process.cwd(); + process.chdir(dir); + }); + + afterAll(() => { + process.chdir(cwd); + }); + + it('should pass', async () => { + await api.package({ + dir, + interactive: false, + }); + }); + }); +}); From 984f19207151ed835f2bbb2279845be35e143e94 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Apr 2026 15:11:13 -0700 Subject: [PATCH 04/22] 22.13.0 minimum --- .nvmrc | 2 +- package.json | 2 +- packages/api/cli/package.json | 2 +- packages/api/core/package.json | 2 +- packages/external/create-electron-app/package.json | 2 +- packages/maker/appx/package.json | 2 +- packages/maker/base/package.json | 2 +- packages/maker/deb/package.json | 2 +- packages/maker/dmg/package.json | 2 +- packages/maker/flatpak/package.json | 2 +- packages/maker/msix/package.json | 2 +- packages/maker/pkg/package.json | 2 +- packages/maker/rpm/package.json | 2 +- packages/maker/snap/package.json | 2 +- packages/maker/squirrel/package.json | 2 +- packages/maker/wix/package.json | 2 +- packages/maker/zip/package.json | 2 +- packages/plugin/auto-unpack-natives/package.json | 2 +- packages/plugin/base/package.json | 2 +- packages/plugin/fuses/package.json | 2 +- packages/plugin/local-electron/package.json | 2 +- packages/plugin/vite/package.json | 2 +- packages/plugin/webpack/package.json | 2 +- packages/publisher/base-static/package.json | 2 +- packages/publisher/base/package.json | 2 +- packages/publisher/bitbucket/package.json | 2 +- packages/publisher/electron-release-server/package.json | 2 +- packages/publisher/gcs/package.json | 2 +- packages/publisher/github/package.json | 2 +- packages/publisher/nucleus/package.json | 2 +- packages/publisher/s3/package.json | 2 +- packages/publisher/snapcraft/package.json | 2 +- packages/template/base/package.json | 2 +- packages/template/vite/package.json | 2 +- packages/template/webpack/package.json | 2 +- packages/utils/core-utils/package.json | 2 +- packages/utils/test-utils/package.json | 2 +- packages/utils/tracer/package.json | 2 +- packages/utils/types/package.json | 2 +- packages/utils/web-multi-logger/package.json | 2 +- tools/doc-plugin/package.json | 2 +- 41 files changed, 41 insertions(+), 41 deletions(-) diff --git a/.nvmrc b/.nvmrc index 35d2d08ea1..fb0a135541 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.12 +22.13 diff --git a/package.json b/package.json index d796f378ac..0fbfa3d02f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "MIT", "type": "module", "engines": { - "node": ">= 22.12.0" + "node": ">= " }, "resolutions": { "ajv@8.17.1": "^8.18.0", diff --git a/packages/api/cli/package.json b/packages/api/cli/package.json index 047da57644..bbb8dcee63 100644 --- a/packages/api/cli/package.json +++ b/packages/api/cli/package.json @@ -29,7 +29,7 @@ "semver": "^7.2.1" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "funding": [ { diff --git a/packages/api/core/package.json b/packages/api/core/package.json index 14c907aa32..c6f0b5228b 100644 --- a/packages/api/core/package.json +++ b/packages/api/core/package.json @@ -42,7 +42,7 @@ "log-symbols": "^4.0.0" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "funding": [ { diff --git a/packages/external/create-electron-app/package.json b/packages/external/create-electron-app/package.json index 10f59cc318..0864085186 100644 --- a/packages/external/create-electron-app/package.json +++ b/packages/external/create-electron-app/package.json @@ -9,7 +9,7 @@ "author": "Samuel Attard", "license": "MIT", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/core-utils": "workspace:*", diff --git a/packages/maker/appx/package.json b/packages/maker/appx/package.json index d51f3b556a..b7760f2394 100644 --- a/packages/maker/appx/package.json +++ b/packages/maker/appx/package.json @@ -12,7 +12,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/core-utils": "workspace:*", diff --git a/packages/maker/base/package.json b/packages/maker/base/package.json index 8d72f4d2fd..33e335d53c 100644 --- a/packages/maker/base/package.json +++ b/packages/maker/base/package.json @@ -12,7 +12,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/shared-types": "workspace:*", diff --git a/packages/maker/deb/package.json b/packages/maker/deb/package.json index 0eb8b91fd2..c26b07c1dd 100644 --- a/packages/maker/deb/package.json +++ b/packages/maker/deb/package.json @@ -12,7 +12,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/maker-base": "workspace:*", diff --git a/packages/maker/dmg/package.json b/packages/maker/dmg/package.json index d9f352b73c..e38433cb39 100644 --- a/packages/maker/dmg/package.json +++ b/packages/maker/dmg/package.json @@ -12,7 +12,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/maker-base": "workspace:*", diff --git a/packages/maker/flatpak/package.json b/packages/maker/flatpak/package.json index 334b064f84..c2916bda56 100644 --- a/packages/maker/flatpak/package.json +++ b/packages/maker/flatpak/package.json @@ -12,7 +12,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/maker-base": "workspace:*", diff --git a/packages/maker/msix/package.json b/packages/maker/msix/package.json index cbe4e0b509..836c4abc94 100644 --- a/packages/maker/msix/package.json +++ b/packages/maker/msix/package.json @@ -12,7 +12,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/core-utils": "workspace:*", diff --git a/packages/maker/pkg/package.json b/packages/maker/pkg/package.json index 8500277f6a..6f54bfaf8f 100644 --- a/packages/maker/pkg/package.json +++ b/packages/maker/pkg/package.json @@ -11,7 +11,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/maker-base": "workspace:*", diff --git a/packages/maker/rpm/package.json b/packages/maker/rpm/package.json index 47dfc751b2..54cd71eaad 100644 --- a/packages/maker/rpm/package.json +++ b/packages/maker/rpm/package.json @@ -12,7 +12,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/maker-base": "workspace:*", diff --git a/packages/maker/snap/package.json b/packages/maker/snap/package.json index 99d608f013..0d0959c36a 100644 --- a/packages/maker/snap/package.json +++ b/packages/maker/snap/package.json @@ -11,7 +11,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/maker-base": "workspace:*", diff --git a/packages/maker/squirrel/package.json b/packages/maker/squirrel/package.json index 872005f114..5993a2b256 100644 --- a/packages/maker/squirrel/package.json +++ b/packages/maker/squirrel/package.json @@ -9,7 +9,7 @@ "exports": "./dist/MakerSquirrel.js", "typings": "dist/MakerSquirrel.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/maker-base": "workspace:*", diff --git a/packages/maker/wix/package.json b/packages/maker/wix/package.json index abc0c4fbd6..0c43ec9b65 100644 --- a/packages/maker/wix/package.json +++ b/packages/maker/wix/package.json @@ -9,7 +9,7 @@ "exports": "./dist/MakerWix.js", "typings": "dist/MakerWix.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/core-utils": "workspace:*", diff --git a/packages/maker/zip/package.json b/packages/maker/zip/package.json index 023f6f54ad..1f92fbe9c3 100644 --- a/packages/maker/zip/package.json +++ b/packages/maker/zip/package.json @@ -12,7 +12,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/maker-base": "workspace:*", diff --git a/packages/plugin/auto-unpack-natives/package.json b/packages/plugin/auto-unpack-natives/package.json index 5e28de0b23..976e7988cc 100644 --- a/packages/plugin/auto-unpack-natives/package.json +++ b/packages/plugin/auto-unpack-natives/package.json @@ -9,7 +9,7 @@ "exports": "./dist/AutoUnpackNativesPlugin.js", "typings": "dist/AutoUnpackNativesPlugin.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/plugin-base": "workspace:*", diff --git a/packages/plugin/base/package.json b/packages/plugin/base/package.json index 034c283b6d..dbac972b0b 100644 --- a/packages/plugin/base/package.json +++ b/packages/plugin/base/package.json @@ -9,7 +9,7 @@ "exports": "./dist/Plugin.js", "typings": "dist/Plugin.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/shared-types": "workspace:*" diff --git a/packages/plugin/fuses/package.json b/packages/plugin/fuses/package.json index 5892fa3791..96ba708613 100644 --- a/packages/plugin/fuses/package.json +++ b/packages/plugin/fuses/package.json @@ -23,7 +23,7 @@ "@electron/fuses": "^2.0.0" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/plugin-base": "workspace:*", diff --git a/packages/plugin/local-electron/package.json b/packages/plugin/local-electron/package.json index ec5d7d6baf..ff74fc3aea 100644 --- a/packages/plugin/local-electron/package.json +++ b/packages/plugin/local-electron/package.json @@ -9,7 +9,7 @@ "exports": "./dist/LocalElectronPlugin.js", "typings": "dist/LocalElectronPlugin.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/plugin-base": "workspace:*", diff --git a/packages/plugin/vite/package.json b/packages/plugin/vite/package.json index 90571250c7..f49779c081 100644 --- a/packages/plugin/vite/package.json +++ b/packages/plugin/vite/package.json @@ -33,7 +33,7 @@ "xvfb-maybe": "^0.2.1" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "publishConfig": { "access": "public" diff --git a/packages/plugin/webpack/package.json b/packages/plugin/webpack/package.json index 4c0f87aef7..879e7d1607 100644 --- a/packages/plugin/webpack/package.json +++ b/packages/plugin/webpack/package.json @@ -17,7 +17,7 @@ "xvfb-maybe": "^0.2.1" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/core-utils": "workspace:*", diff --git a/packages/publisher/base-static/package.json b/packages/publisher/base-static/package.json index e5ba609084..4cf91c09c5 100644 --- a/packages/publisher/base-static/package.json +++ b/packages/publisher/base-static/package.json @@ -16,7 +16,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "publishConfig": { "access": "public" diff --git a/packages/publisher/base/package.json b/packages/publisher/base/package.json index 260b470cb7..f347a5014a 100644 --- a/packages/publisher/base/package.json +++ b/packages/publisher/base/package.json @@ -15,7 +15,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "publishConfig": { "access": "public" diff --git a/packages/publisher/bitbucket/package.json b/packages/publisher/bitbucket/package.json index 299a1e1b18..5263fa5bc9 100644 --- a/packages/publisher/bitbucket/package.json +++ b/packages/publisher/bitbucket/package.json @@ -9,7 +9,7 @@ "exports": "./dist/PublisherBitbucket.js", "typings": "dist/PublisherBitbucket.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/publisher-base": "workspace:*", diff --git a/packages/publisher/electron-release-server/package.json b/packages/publisher/electron-release-server/package.json index 6abc53a930..a1fe5ee8bd 100644 --- a/packages/publisher/electron-release-server/package.json +++ b/packages/publisher/electron-release-server/package.json @@ -13,7 +13,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/publisher-base": "workspace:*", diff --git a/packages/publisher/gcs/package.json b/packages/publisher/gcs/package.json index bb6bd73183..52582a03ac 100644 --- a/packages/publisher/gcs/package.json +++ b/packages/publisher/gcs/package.json @@ -9,7 +9,7 @@ "exports": "./dist/PublisherGCS.js", "typings": "dist/PublisherGCS.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/publisher-static": "workspace:*", diff --git a/packages/publisher/github/package.json b/packages/publisher/github/package.json index 5dad5f9fc6..40ff9fdc11 100644 --- a/packages/publisher/github/package.json +++ b/packages/publisher/github/package.json @@ -12,7 +12,7 @@ "vitest": "catalog:" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/publisher-base": "workspace:*", diff --git a/packages/publisher/nucleus/package.json b/packages/publisher/nucleus/package.json index b1df219316..c06c1ce7e5 100644 --- a/packages/publisher/nucleus/package.json +++ b/packages/publisher/nucleus/package.json @@ -9,7 +9,7 @@ "exports": "./dist/PublisherNucleus.js", "typings": "dist/PublisherNucleus.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/publisher-base": "workspace:*", diff --git a/packages/publisher/s3/package.json b/packages/publisher/s3/package.json index 686ff006a2..c2fbdf0825 100644 --- a/packages/publisher/s3/package.json +++ b/packages/publisher/s3/package.json @@ -9,7 +9,7 @@ "exports": "./dist/PublisherS3.js", "typings": "dist/PublisherS3.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@aws-sdk/client-s3": "^3.654.0", diff --git a/packages/publisher/snapcraft/package.json b/packages/publisher/snapcraft/package.json index f10afdb2b1..c074645106 100644 --- a/packages/publisher/snapcraft/package.json +++ b/packages/publisher/snapcraft/package.json @@ -9,7 +9,7 @@ "exports": "./dist/PublisherSnapcraft.js", "typings": "dist/PublisherSnapcraft.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/publisher-base": "workspace:*", diff --git a/packages/template/base/package.json b/packages/template/base/package.json index df589b80a3..3db80966b5 100644 --- a/packages/template/base/package.json +++ b/packages/template/base/package.json @@ -9,7 +9,7 @@ "exports": "./dist/BaseTemplate.js", "typings": "dist/BaseTemplate.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/core-utils": "workspace:*", diff --git a/packages/template/vite/package.json b/packages/template/vite/package.json index dd2befdc1b..078eb4a247 100644 --- a/packages/template/vite/package.json +++ b/packages/template/vite/package.json @@ -13,7 +13,7 @@ "exports": "./dist/ViteTemplate.js", "typings": "dist/ViteTemplate.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/shared-types": "workspace:*", diff --git a/packages/template/webpack/package.json b/packages/template/webpack/package.json index faaac8301c..af2b377924 100644 --- a/packages/template/webpack/package.json +++ b/packages/template/webpack/package.json @@ -9,7 +9,7 @@ "exports": "./dist/WebpackTemplate.js", "typings": "dist/WebpackTemplate.d.ts", "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "dependencies": { "@electron-forge/shared-types": "workspace:*", diff --git a/packages/utils/core-utils/package.json b/packages/utils/core-utils/package.json index 44854faa44..527785aa82 100644 --- a/packages/utils/core-utils/package.json +++ b/packages/utils/core-utils/package.json @@ -19,7 +19,7 @@ "semver": "^7.2.1" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "devDependencies": { "@electron-forge/test-utils": "workspace:*", diff --git a/packages/utils/test-utils/package.json b/packages/utils/test-utils/package.json index 2d8104d710..9f998a506c 100644 --- a/packages/utils/test-utils/package.json +++ b/packages/utils/test-utils/package.json @@ -13,7 +13,7 @@ "fs-extra": "^10.0.0" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "publishConfig": { "access": "public" diff --git a/packages/utils/tracer/package.json b/packages/utils/tracer/package.json index 42c01a597c..bb791453b4 100644 --- a/packages/utils/tracer/package.json +++ b/packages/utils/tracer/package.json @@ -12,7 +12,7 @@ "chrome-trace-event": "^1.0.3" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "publishConfig": { "access": "public" diff --git a/packages/utils/types/package.json b/packages/utils/types/package.json index c0d895a078..8f7f96c322 100644 --- a/packages/utils/types/package.json +++ b/packages/utils/types/package.json @@ -15,7 +15,7 @@ "listr2": "^7.0.2" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "publishConfig": { "access": "public" diff --git a/packages/utils/web-multi-logger/package.json b/packages/utils/web-multi-logger/package.json index f444812aed..f0d8b2ffe3 100644 --- a/packages/utils/web-multi-logger/package.json +++ b/packages/utils/web-multi-logger/package.json @@ -16,7 +16,7 @@ "xterm-addon-search": "^0.8.0" }, "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "publishConfig": { "access": "public" diff --git a/tools/doc-plugin/package.json b/tools/doc-plugin/package.json index 79adb54aa9..f005633e2c 100644 --- a/tools/doc-plugin/package.json +++ b/tools/doc-plugin/package.json @@ -9,7 +9,7 @@ "custom-sidebar" ], "engines": { - "node": ">= 22.12.0" + "node": ">= 22.13.0" }, "devDependencies": { "typescript": "5.9.3" From bf8f781db0cd3d40ebaa6df5206623be78e9d06a Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Apr 2026 15:15:44 -0700 Subject: [PATCH 05/22] add missing deps --- packages/template/vite/package.json | 1 + packages/template/webpack/package.json | 1 + yarn.lock | 2 ++ 3 files changed, 4 insertions(+) diff --git a/packages/template/vite/package.json b/packages/template/vite/package.json index 078eb4a247..f9210894d4 100644 --- a/packages/template/vite/package.json +++ b/packages/template/vite/package.json @@ -21,6 +21,7 @@ "fs-extra": "^10.0.0" }, "devDependencies": { + "@electron-forge/core-utils": "workspace:*", "@electron-forge/test-utils": "workspace:*", "listr2": "^7.0.2", "vitest": "catalog:" diff --git a/packages/template/webpack/package.json b/packages/template/webpack/package.json index af2b377924..6350897773 100644 --- a/packages/template/webpack/package.json +++ b/packages/template/webpack/package.json @@ -16,6 +16,7 @@ "@electron-forge/template-base": "workspace:*" }, "devDependencies": { + "@electron-forge/core-utils": "workspace:*", "@electron-forge/test-utils": "workspace:*", "listr2": "^7.0.2", "vitest": "catalog:" diff --git a/yarn.lock b/yarn.lock index d98624c8e4..37495b1fd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1233,6 +1233,7 @@ __metadata: version: 0.0.0-use.local resolution: "@electron-forge/template-vite@workspace:packages/template/vite" dependencies: + "@electron-forge/core-utils": "workspace:*" "@electron-forge/shared-types": "workspace:*" "@electron-forge/template-base": "workspace:*" "@electron-forge/test-utils": "workspace:*" @@ -1246,6 +1247,7 @@ __metadata: version: 0.0.0-use.local resolution: "@electron-forge/template-webpack@workspace:packages/template/webpack" dependencies: + "@electron-forge/core-utils": "workspace:*" "@electron-forge/shared-types": "workspace:*" "@electron-forge/template-base": "workspace:*" "@electron-forge/test-utils": "workspace:*" From 49ec177135d2e241d9d4824614627fce3b32ed73 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Apr 2026 16:01:11 -0700 Subject: [PATCH 06/22] use project oxfmt --- packages/template/base/src/BaseTemplate.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/template/base/src/BaseTemplate.ts b/packages/template/base/src/BaseTemplate.ts index 767d76cb28..96d4ca6e9e 100644 --- a/packages/template/base/src/BaseTemplate.ts +++ b/packages/template/base/src/BaseTemplate.ts @@ -20,6 +20,9 @@ const currentForgeVersion = fs.readJSONSync( const d = debug('electron-forge:template:base'); const tmplDir = path.resolve(import.meta.dirname, '../tmpl'); +const oxfmtConfig = fs.readJSONSync( + path.resolve(import.meta.dirname, '../../../../.oxfmtrc.json'), +); export class BaseTemplate implements ForgeTemplate { public templateDir = tmplDir; @@ -127,13 +130,12 @@ export class BaseTemplate implements ForgeTemplate { async writeLintConfig(directory: string): Promise { await this.copyTemplateFile(directory, '.oxlintrc.json'); - const oxfmtrc = await fs.readJson( - path.resolve(import.meta.dirname, '../../../../.oxfmtrc.json'), + const { ignorePatterns: _, ...projectConfig } = oxfmtConfig; + await fs.writeJson( + path.resolve(directory, '.oxfmtrc.json'), + projectConfig, + { spaces: 2 }, ); - delete oxfmtrc.ignorePatterns; - await fs.writeJson(path.resolve(directory, '.oxfmtrc.json'), oxfmtrc, { - spaces: 2, - }); } async copyTemplateFile(destDir: string, basename: string): Promise { @@ -192,7 +194,7 @@ export class BaseTemplate implements ForgeTemplate { async stripAndRename(srcPath: string, destPath: string): Promise { const source = await fs.readFile(srcPath, 'utf8'); const stripped = stripTypeScriptTypes(source, { mode: 'transform' }); - const formatted = await format(destPath, stripped); + const formatted = await format(destPath, stripped, oxfmtConfig); await fs.writeFile(destPath, formatted.code); if (srcPath !== destPath) { await fs.remove(srcPath); From b7d71b11f4f6fc77b00d4ed21530da4bf11b6ee3 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Apr 2026 17:43:06 -0700 Subject: [PATCH 07/22] more fixes --- .../WebpackTemplate.slow.verdaccio.spec.ts | 6 +++--- .../webpack/spec/WebpackTemplate.spec.ts | 21 +++++++++++-------- .../template/webpack/src/WebpackTemplate.ts | 10 ++++----- ...main.config.js => webpack.main.config.mjs} | 4 ++-- ....config.js => webpack.renderer.config.mjs} | 4 ++-- .../{webpack.rules.js => webpack.rules.mjs} | 0 vitest.config.mts | 2 +- 7 files changed, 25 insertions(+), 22 deletions(-) rename packages/template/webpack/tmpl/js/{webpack.main.config.js => webpack.main.config.mjs} (72%) rename packages/template/webpack/tmpl/js/{webpack.renderer.config.js => webpack.renderer.config.mjs} (64%) rename packages/template/webpack/tmpl/js/{webpack.rules.js => webpack.rules.mjs} (100%) diff --git a/packages/template/webpack/spec/WebpackTemplate.slow.verdaccio.spec.ts b/packages/template/webpack/spec/WebpackTemplate.slow.verdaccio.spec.ts index c7e449de33..460d321def 100644 --- a/packages/template/webpack/spec/WebpackTemplate.slow.verdaccio.spec.ts +++ b/packages/template/webpack/spec/WebpackTemplate.slow.verdaccio.spec.ts @@ -130,9 +130,9 @@ describe('WebpackTemplate (JavaScript)', () => { it.each([ '.oxlintrc.json', 'forge.config.mjs', - 'webpack.main.config.js', - 'webpack.renderer.config.js', - 'webpack.rules.js', + 'webpack.main.config.mjs', + 'webpack.renderer.config.mjs', + 'webpack.rules.mjs', path.join('src', 'main.js'), path.join('src', 'renderer.js'), path.join('src', 'preload.js'), diff --git a/packages/template/webpack/spec/WebpackTemplate.spec.ts b/packages/template/webpack/spec/WebpackTemplate.spec.ts index 7247b43e43..a42c8c449c 100644 --- a/packages/template/webpack/spec/WebpackTemplate.spec.ts +++ b/packages/template/webpack/spec/WebpackTemplate.spec.ts @@ -35,9 +35,9 @@ describe('WebpackTemplate', () => { const expectedFiles = [ 'package.json', 'forge.config.mjs', - 'webpack.main.config.js', - 'webpack.renderer.config.js', - 'webpack.rules.js', + 'webpack.main.config.mjs', + 'webpack.renderer.config.mjs', + 'webpack.rules.mjs', path.join('src', 'main.js'), path.join('src', 'renderer.js'), path.join('src', 'preload.js'), @@ -82,9 +82,9 @@ describe('WebpackTemplate', () => { expect(mainFile).not.toMatch(/\bdeclare\s+const\b/); }); - it('should not include ts-loader rule in webpack.rules.js', async () => { + it('should not include ts-loader rule in webpack.rules.mjs', async () => { const rules = ( - await fs.promises.readFile(path.join(dir, 'webpack.rules.js')) + await fs.promises.readFile(path.join(dir, 'webpack.rules.mjs')) ).toString(); expect(rules).not.toMatch(/ts-loader/); expect(rules).not.toMatch(/\.tsx\?\$/); @@ -92,8 +92,8 @@ describe('WebpackTemplate', () => { it('should not include plugins or resolve.extensions in webpack configs', async () => { for (const name of [ - 'webpack.main.config.js', - 'webpack.renderer.config.js', + 'webpack.main.config.mjs', + 'webpack.renderer.config.mjs', ]) { const config = ( await fs.promises.readFile(path.join(dir, name)) @@ -108,8 +108,8 @@ describe('WebpackTemplate', () => { const config = ( await fs.promises.readFile(path.join(dir, 'forge.config.mjs')) ).toString(); - expect(config).toMatch(/webpack\.main\.config\.js/); - expect(config).toMatch(/webpack\.renderer\.config\.js/); + expect(config).toMatch(/webpack\.main\.config\.mjs/); + expect(config).toMatch(/webpack\.renderer\.config\.mjs/); expect(config).toMatch(/src\/renderer\.js/); expect(config).toMatch(/src\/preload\.js/); }); @@ -182,8 +182,11 @@ describe('WebpackTemplate', () => { const unexpectedFiles = [ 'forge.config.mjs', 'webpack.main.config.js', + 'webpack.main.config.mjs', 'webpack.renderer.config.js', + 'webpack.renderer.config.mjs', 'webpack.rules.js', + 'webpack.rules.mjs', 'webpack.plugins.js', path.join('src', 'main.js'), path.join('src', 'renderer.js'), diff --git a/packages/template/webpack/src/WebpackTemplate.ts b/packages/template/webpack/src/WebpackTemplate.ts index 5297c47acd..54754d0852 100644 --- a/packages/template/webpack/src/WebpackTemplate.ts +++ b/packages/template/webpack/src/WebpackTemplate.ts @@ -63,12 +63,12 @@ class WebpackTemplate extends BaseTemplate { if (line.includes('mainConfig,')) return line.replace( 'mainConfig,', - "'./webpack.main.config.js',", + "mainConfig: './webpack.main.config.mjs',", ); if (/config:\s*rendererConfig,/.test(line)) return line.replace( 'rendererConfig,', - "'./webpack.renderer.config.js',", + "'./webpack.renderer.config.mjs',", ); return line .replace(/src\/renderer\.ts/g, 'src/renderer.js') @@ -93,9 +93,9 @@ class WebpackTemplate extends BaseTemplate { await this.copyTemplateFile(directory, 'tsconfig.json'); } else { for (const name of [ - 'webpack.main.config.js', - 'webpack.renderer.config.js', - 'webpack.rules.js', + 'webpack.main.config.mjs', + 'webpack.renderer.config.mjs', + 'webpack.rules.mjs', ]) { await this.copy( path.join(this.templateDir, 'js', name), diff --git a/packages/template/webpack/tmpl/js/webpack.main.config.js b/packages/template/webpack/tmpl/js/webpack.main.config.mjs similarity index 72% rename from packages/template/webpack/tmpl/js/webpack.main.config.js rename to packages/template/webpack/tmpl/js/webpack.main.config.mjs index 5366a111d4..593b7a4486 100644 --- a/packages/template/webpack/tmpl/js/webpack.main.config.js +++ b/packages/template/webpack/tmpl/js/webpack.main.config.mjs @@ -1,6 +1,6 @@ -import { rules } from './webpack.rules'; +import { rules } from './webpack.rules.mjs'; -export const mainConfig = { +export default { /** * This is the main entry point for your application, it's the first file * that runs in the main process. diff --git a/packages/template/webpack/tmpl/js/webpack.renderer.config.js b/packages/template/webpack/tmpl/js/webpack.renderer.config.mjs similarity index 64% rename from packages/template/webpack/tmpl/js/webpack.renderer.config.js rename to packages/template/webpack/tmpl/js/webpack.renderer.config.mjs index 6701473271..5e17fae970 100644 --- a/packages/template/webpack/tmpl/js/webpack.renderer.config.js +++ b/packages/template/webpack/tmpl/js/webpack.renderer.config.mjs @@ -1,11 +1,11 @@ -import { rules } from './webpack.rules'; +import { rules } from './webpack.rules.mjs'; rules.push({ test: /\.css$/, use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], }); -export const rendererConfig = { +export default { module: { rules, }, diff --git a/packages/template/webpack/tmpl/js/webpack.rules.js b/packages/template/webpack/tmpl/js/webpack.rules.mjs similarity index 100% rename from packages/template/webpack/tmpl/js/webpack.rules.js rename to packages/template/webpack/tmpl/js/webpack.rules.mjs diff --git a/vitest.config.mts b/vitest.config.mts index fdc6feca3b..ed14d6d23a 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { clearMocks: true, - exclude: ['**/.links/**', '**/node_modules/**'], + exclude: ['**/.links/**', '**/node_modules/**', '**/.claude/**'], fileParallelism: false, projects: [ { From e66f3db0887d0df3ad6a19600b51cc9b2ebb3443 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Apr 2026 23:57:14 -0700 Subject: [PATCH 08/22] remove knip references to old template --- knip.json | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/knip.json b/knip.json index dbf3844a3a..4809966dcf 100644 --- a/knip.json +++ b/knip.json @@ -142,24 +142,10 @@ "entry": ["spec/**/*.spec.ts"], "project": ["src/**/*.ts!", "spec/**/*.ts"] }, - "packages/template/vite-typescript": { - "entry": ["spec/**/*.spec.ts"], - "project": ["src/**/*.ts!", "spec/**/*.ts"] - }, "packages/template/webpack": { "entry": ["spec/**/*.spec.ts"], "project": ["src/**/*.ts!", "spec/**/*.ts"] }, - "packages/template/webpack-typescript": { - "entry": ["spec/**/*.spec.ts"], - "project": ["src/**/*.ts!", "spec/**/*.ts"], - "ignoreDependencies": [ - "@electron-forge/maker-.*", - "@electron-forge/plugin-webpack", - "webpack", - "listr2" - ] - }, "packages/utils/core-utils": { "entry": ["src/remote-rebuild.ts!", "spec/**/*.spec.ts"], "project": ["src/**/*.ts!", "spec/**/*.ts"] From 869ecab2cfc972720cd9d8db5619aa20026df2f7 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Apr 2026 23:57:26 -0700 Subject: [PATCH 09/22] add missing deps --- packages/template/webpack/package.json | 3 ++- yarn.lock | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/template/webpack/package.json b/packages/template/webpack/package.json index 6350897773..14b74d1491 100644 --- a/packages/template/webpack/package.json +++ b/packages/template/webpack/package.json @@ -13,7 +13,8 @@ }, "dependencies": { "@electron-forge/shared-types": "workspace:*", - "@electron-forge/template-base": "workspace:*" + "@electron-forge/template-base": "workspace:*", + "fs-extra": "^10.0.0" }, "devDependencies": { "@electron-forge/core-utils": "workspace:*", diff --git a/yarn.lock b/yarn.lock index 37495b1fd9..9d01e24d46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1251,6 +1251,7 @@ __metadata: "@electron-forge/shared-types": "workspace:*" "@electron-forge/template-base": "workspace:*" "@electron-forge/test-utils": "workspace:*" + fs-extra: "npm:^10.0.0" listr2: "npm:^7.0.2" vitest: "catalog:" languageName: unknown From b7d24fd098dcd2f7e257da80adf5cb3b9bdd9948 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Apr 2026 23:57:34 -0700 Subject: [PATCH 10/22] tests/warnings --- .../spec/fast/init-scripts/find-template.spec.ts | 14 ++++++++++++++ packages/external/create-electron-app/src/init.ts | 6 ++++++ packages/template/vite/src/ViteTemplate.ts | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts b/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts index c4412bb4d5..02d555066f 100644 --- a/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts +++ b/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts @@ -48,4 +48,18 @@ describe('findTemplate', () => { 'Failed to locate custom template: "non-existent-template".', ); }); + + describe('deprecated -typescript templates', () => { + it('should provide a helpful error for vite-typescript', async () => { + await expect(findTemplate('vite-typescript')).rejects.toThrowError( + /no longer exists.*--template vite/, + ); + }); + + it('should provide a helpful error for webpack-typescript', async () => { + await expect(findTemplate('webpack-typescript')).rejects.toThrowError( + /no longer exists.*--template webpack/, + ); + }); + }); }); diff --git a/packages/external/create-electron-app/src/init.ts b/packages/external/create-electron-app/src/init.ts index b3d8759185..fe66b7df0e 100644 --- a/packages/external/create-electron-app/src/init.ts +++ b/packages/external/create-electron-app/src/init.ts @@ -103,6 +103,12 @@ export async function init({ }: InitOptions): Promise { d(`Initializing in: ${dir}`); + if (typescript && template === 'base') { + throw new Error( + 'The "base" template does not support TypeScript. Use "--template vite" or "--template webpack" with "--typescript".', + ); + } + const runner = new Listr<{ templateModule: ForgeTemplate; pm: PMDetails; diff --git a/packages/template/vite/src/ViteTemplate.ts b/packages/template/vite/src/ViteTemplate.ts index d2343f75a4..b9991985d7 100644 --- a/packages/template/vite/src/ViteTemplate.ts +++ b/packages/template/vite/src/ViteTemplate.ts @@ -73,7 +73,7 @@ class ViteTemplate extends BaseTemplate { }, }, { - title: `Setting up ${typescript ? 'TypeScript' : 'Vite'} configuration`, + title: `Setting up ${typescript ? 'TypeScript and Vite' : 'Vite'} configuration`, task: async () => { // Copy Vite config files if (typescript) { From 488fc8168095d835a9e40a18906b70fbccedea6c Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 21 May 2026 16:22:33 -0700 Subject: [PATCH 11/22] huh --- .../src/init-scripts/find-template.ts | 2 +- .../template/base/spec/BaseTemplate.spec.ts | 7 +- .../template/vite-typescript/package.json | 32 ------ .../src/ViteTypeScriptTemplate.ts | 97 ------------------- .../vite-typescript/tmpl/package.json | 16 --- packages/template/vite/package.json | 1 - packages/template/vite/src/ViteTemplate.ts | 4 + .../template/webpack-typescript/package.json | 37 ------- .../src/WebpackTypeScriptTemplate.ts | 89 ----------------- .../webpack-typescript/tmpl/package.json | 20 ---- packages/template/webpack/package.json | 5 +- .../template/webpack/src/WebpackTemplate.ts | 23 +++-- yarn.lock | 35 +------ 13 files changed, 26 insertions(+), 342 deletions(-) delete mode 100644 packages/template/vite-typescript/package.json delete mode 100644 packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts delete mode 100644 packages/template/vite-typescript/tmpl/package.json delete mode 100644 packages/template/webpack-typescript/package.json delete mode 100644 packages/template/webpack-typescript/src/WebpackTypeScriptTemplate.ts delete mode 100644 packages/template/webpack-typescript/tmpl/package.json diff --git a/packages/external/create-electron-app/src/init-scripts/find-template.ts b/packages/external/create-electron-app/src/init-scripts/find-template.ts index 4d95548a96..5e6e571f5d 100644 --- a/packages/external/create-electron-app/src/init-scripts/find-template.ts +++ b/packages/external/create-electron-app/src/init-scripts/find-template.ts @@ -47,7 +47,7 @@ export const findTemplate = async ( const tsMatch = template.match(/^(.+)-typescript$/); if (tsMatch) { throw new Error( - `The "${template}" template no longer exists. Use "--template ${tsMatch[1]}" instead and select TypeScript when prompted.`, + `The "${template}" template no longer exists. Use "--template ${tsMatch[1]} --typescript" instead.`, ); } throw new Error(`Failed to locate custom template: "${template}".`); diff --git a/packages/template/base/spec/BaseTemplate.spec.ts b/packages/template/base/spec/BaseTemplate.spec.ts index 8cc5797a66..9b7593256d 100644 --- a/packages/template/base/spec/BaseTemplate.spec.ts +++ b/packages/template/base/spec/BaseTemplate.spec.ts @@ -29,12 +29,7 @@ describe('BaseTemplate', () => { }); it('.oxlintrc.json should exist in each template that uses writeLintConfig', () => { - const templatesWithLintConfig = [ - 'vite', - 'vite-typescript', - 'webpack', - 'webpack-typescript', - ]; + const templatesWithLintConfig = ['vite', 'webpack']; for (const template of templatesWithLintConfig) { const oxlintrcPath = path.resolve( import.meta.dirname, diff --git a/packages/template/vite-typescript/package.json b/packages/template/vite-typescript/package.json deleted file mode 100644 index c67e05f364..0000000000 --- a/packages/template/vite-typescript/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@electron-forge/template-vite-typescript", - "version": "8.0.0-alpha.9", - "type": "module", - "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", - "exports": "./dist/ViteTypeScriptTemplate.js", - "typings": "dist/ViteTypeScriptTemplate.d.ts", - "engines": { - "node": ">= 22.12.0" - }, - "dependencies": { - "@electron-forge/core-utils": "workspace:*", - "@electron-forge/shared-types": "workspace:*", - "@electron-forge/template-base": "workspace:*" - }, - "devDependencies": { - "@electron-forge/test-utils": "workspace:*", - "vitest": "catalog:" - }, - "files": [ - "dist", - "src", - "tmpl" - ] -} diff --git a/packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts b/packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts deleted file mode 100644 index 34e68b1cc2..0000000000 --- a/packages/template/vite-typescript/src/ViteTypeScriptTemplate.ts +++ /dev/null @@ -1,97 +0,0 @@ -import fs from 'node:fs/promises'; -import path from 'node:path'; - -import { move, readJson, writeJson } from '@electron-forge/core-utils'; -import { - ForgeListrTaskDefinition, - InitTemplateOptions, -} from '@electron-forge/shared-types'; -import { BaseTemplate } from '@electron-forge/template-base'; - -class ViteTypeScriptTemplate extends BaseTemplate { - public templateDir = path.resolve(import.meta.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.rm(path.resolve(directory, 'forge.config.js'), { - force: true, - }); - }, - }, - { - 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.preload.config.ts'); - await this.copyTemplateFile(directory, 'vite.renderer.config.ts'); - - // Copy tsconfig with a small set of presets - await this.copyTemplateFile(directory, 'tsconfig.json'); - - await this.writeLintConfig(directory); - - await this.copyTemplateFile( - path.join(directory, 'src'), - 'declarations.d.ts', - ); - - // Remove index.js and replace with main.ts - await fs.rm(filePath('index.js'), { force: true }); - await this.copyTemplateFile(path.join(directory, 'src'), 'main.ts'); - - await this.copyTemplateFile( - path.join(directory, 'src'), - 'renderer.ts', - ); - - // Remove preload.js and replace with preload.ts - await fs.rm(filePath('preload.js'), { force: true }); - 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 move( - filePath('index.html'), - path.join(directory, 'index.html'), - { overwrite: options.force }, - ); - await this.updateFileByLine( - path.join(directory, 'index.html'), - (line) => { - if (line.includes('link rel="stylesheet"')) return null; - if (line.includes('')) - return ' \n '; - return line; - }, - ); - - // update package.json - const packageJSONPath = path.resolve(directory, 'package.json'); - const packageJSON = await readJson(packageJSONPath); - packageJSON.main = '.vite/build/main.js'; - await writeJson(packageJSONPath, packageJSON, { - spaces: 2, - }); - }, - }, - ]; - } -} - -export default new ViteTypeScriptTemplate(); diff --git a/packages/template/vite-typescript/tmpl/package.json b/packages/template/vite-typescript/tmpl/package.json deleted file mode 100644 index 9ea4159d04..0000000000 --- a/packages/template/vite-typescript/tmpl/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "main": ".vite/build/main.js", - "scripts": { - "lint": "oxlint && oxfmt --check", - "lint:fix": "oxlint --fix && oxfmt --write", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@electron-forge/plugin-vite": "ELECTRON_FORGE/VERSION", - "@types/electron-squirrel-startup": "^1.0.2", - "oxfmt": "^0.41.0", - "oxlint": "^1.0.0", - "typescript": "^6.0.0", - "vite": "^8.0.0" - } -} diff --git a/packages/template/vite/package.json b/packages/template/vite/package.json index 6de26e7b49..5c1d191e7c 100644 --- a/packages/template/vite/package.json +++ b/packages/template/vite/package.json @@ -21,7 +21,6 @@ "@electron-forge/template-base": "workspace:*" }, "devDependencies": { - "@electron-forge/core-utils": "workspace:*", "@electron-forge/test-utils": "workspace:*", "listr2": "^7.0.2", "vitest": "catalog:" diff --git a/packages/template/vite/src/ViteTemplate.ts b/packages/template/vite/src/ViteTemplate.ts index b3190f2ed4..58bc573609 100644 --- a/packages/template/vite/src/ViteTemplate.ts +++ b/packages/template/vite/src/ViteTemplate.ts @@ -114,6 +114,10 @@ class ViteTemplate extends BaseTemplate { // Copy source files if (typescript) { + await this.copyTemplateFile( + path.join(directory, 'src'), + 'declarations.d.ts', + ); await this.copyTemplateFile(path.join(directory, 'src'), 'main.ts'); await this.copyTemplateFile( path.join(directory, 'src'), diff --git a/packages/template/webpack-typescript/package.json b/packages/template/webpack-typescript/package.json deleted file mode 100644 index 24e5169241..0000000000 --- a/packages/template/webpack-typescript/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@electron-forge/template-webpack-typescript", - "version": "8.0.0-alpha.9", - "type": "module", - "description": "Webpack-TypeScript template for Electron Forge", - "repository": "https://github.com/electron/forge", - "author": "Shelley Vohr ", - "license": "MIT", - "exports": "./dist/WebpackTypeScriptTemplate.js", - "typings": "dist/WebpackTypeScriptTemplate.d.ts", - "engines": { - "node": ">= 22.12.0" - }, - "dependencies": { - "@electron-forge/core-utils": "workspace:*", - "@electron-forge/shared-types": "workspace:*", - "@electron-forge/template-base": "workspace:*", - "webpack": "^5.69.1" - }, - "devDependencies": { - "@electron-forge/maker-deb": "workspace:*", - "@electron-forge/maker-rpm": "workspace:*", - "@electron-forge/maker-squirrel": "workspace:*", - "@electron-forge/maker-zip": "workspace:*", - "@electron-forge/plugin-webpack": "workspace:*", - "@electron-forge/test-utils": "workspace:*", - "fork-ts-checker-webpack-plugin": "^7.2.13", - "listr2": "^7.0.2", - "vitest": "catalog:", - "webpack": "^5.69.1" - }, - "files": [ - "dist", - "src", - "tmpl" - ] -} diff --git a/packages/template/webpack-typescript/src/WebpackTypeScriptTemplate.ts b/packages/template/webpack-typescript/src/WebpackTypeScriptTemplate.ts deleted file mode 100644 index e7b59954b1..0000000000 --- a/packages/template/webpack-typescript/src/WebpackTypeScriptTemplate.ts +++ /dev/null @@ -1,89 +0,0 @@ -import fs from 'node:fs/promises'; -import path from 'node:path'; - -import { readJson, writeJson } from '@electron-forge/core-utils'; -import { - ForgeListrTaskDefinition, - InitTemplateOptions, -} from '@electron-forge/shared-types'; -import { BaseTemplate } from '@electron-forge/template-base'; - -class WebpackTypeScriptTemplate extends BaseTemplate { - public templateDir = path.resolve(import.meta.dirname, '..', 'tmpl'); - - 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.rm(path.resolve(directory, 'forge.config.js'), { - force: true, - }); - }, - }, - { - title: 'Preparing TypeScript files and configuration', - task: async () => { - const filePath = (fileName: string) => - path.join(directory, 'src', fileName); - - // Copy Webpack files - await this.copyTemplateFile(directory, 'webpack.main.config.ts'); - await this.copyTemplateFile(directory, 'webpack.renderer.config.ts'); - await this.copyTemplateFile(directory, 'webpack.rules.ts'); - await this.copyTemplateFile(directory, 'webpack.plugins.ts'); - - await this.updateFileByLine( - path.resolve(directory, 'src', 'index.html'), - (line) => { - if (line.includes('link rel="stylesheet"')) return null; - return line; - }, - ); - - // Copy tsconfig with a small set of presets - await this.copyTemplateFile(directory, 'tsconfig.json'); - - await this.writeLintConfig(directory); - - await this.copyTemplateFile( - path.join(directory, 'src'), - 'declarations.d.ts', - ); - - // Remove index.js and replace with index.ts - await fs.rm(filePath('index.js'), { force: true }); - await this.copyTemplateFile(path.join(directory, 'src'), 'index.ts'); - - await this.copyTemplateFile( - path.join(directory, 'src'), - 'renderer.ts', - ); - - // Remove preload.js and replace with preload.ts - await fs.rm(filePath('preload.js'), { force: true }); - await this.copyTemplateFile( - path.join(directory, 'src'), - 'preload.ts', - ); - - // update package.json - const packageJSONPath = path.resolve(directory, 'package.json'); - const packageJSON = await readJson(packageJSONPath); - packageJSON.main = '.webpack/main'; - await writeJson(packageJSONPath, packageJSON, { - spaces: 2, - }); - }, - }, - ]; - } -} - -export default new WebpackTypeScriptTemplate(); diff --git a/packages/template/webpack-typescript/tmpl/package.json b/packages/template/webpack-typescript/tmpl/package.json deleted file mode 100644 index 58ec83aca5..0000000000 --- a/packages/template/webpack-typescript/tmpl/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "main": ".webpack/main", - "scripts": { - "lint": "oxlint && oxfmt --check", - "lint:fix": "oxlint --fix && oxfmt --write", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@electron-forge/plugin-webpack": "ELECTRON_FORGE/VERSION", - "@vercel/webpack-asset-relocator-loader": "1.7.3", - "css-loader": "^6.0.0", - "fork-ts-checker-webpack-plugin": "^7.2.13", - "node-loader": "^2.0.0", - "oxfmt": "^0.41.0", - "oxlint": "^1.0.0", - "style-loader": "^3.0.0", - "ts-loader": "^9.2.2", - "typescript": "^6.0.0" - } -} diff --git a/packages/template/webpack/package.json b/packages/template/webpack/package.json index d593818dee..25f5975e72 100644 --- a/packages/template/webpack/package.json +++ b/packages/template/webpack/package.json @@ -12,12 +12,11 @@ "node": ">= 22.13.0" }, "dependencies": { + "@electron-forge/core-utils": "workspace:*", "@electron-forge/shared-types": "workspace:*", - "@electron-forge/template-base": "workspace:*", - "fs-extra": "^10.0.0" + "@electron-forge/template-base": "workspace:*" }, "devDependencies": { - "@electron-forge/core-utils": "workspace:*", "@electron-forge/test-utils": "workspace:*", "listr2": "^7.0.2", "vitest": "catalog:" diff --git a/packages/template/webpack/src/WebpackTemplate.ts b/packages/template/webpack/src/WebpackTemplate.ts index 54754d0852..25a2f8ae76 100644 --- a/packages/template/webpack/src/WebpackTemplate.ts +++ b/packages/template/webpack/src/WebpackTemplate.ts @@ -1,11 +1,12 @@ +import fs from 'node:fs/promises'; import path from 'node:path'; +import { readJson, writeJson } from '@electron-forge/core-utils'; import { ForgeListrTaskDefinition, InitTemplateOptions, } from '@electron-forge/shared-types'; import { BaseTemplate } from '@electron-forge/template-base'; -import fs from 'fs-extra'; const TS_ONLY_DEV_DEPS = new Set([ 'fork-ts-checker-webpack-plugin', @@ -42,7 +43,9 @@ class WebpackTemplate extends BaseTemplate { { title: 'Setting up Forge configuration', task: async () => { - await fs.remove(path.resolve(directory, 'forge.config.js')); + await fs.rm(path.resolve(directory, 'forge.config.js'), { + force: true, + }); if (typescript) { await this.copyTemplateFile(directory, 'forge.config.mts'); @@ -107,11 +110,19 @@ class WebpackTemplate extends BaseTemplate { await this.writeLintConfig(directory); // Remove base template's JS source files - await fs.remove(path.resolve(directory, 'src', 'index.js')); - await fs.remove(path.resolve(directory, 'src', 'preload.js')); + await fs.rm(path.resolve(directory, 'src', 'index.js'), { + force: true, + }); + await fs.rm(path.resolve(directory, 'src', 'preload.js'), { + force: true, + }); // Copy and process source files if (typescript) { + await this.copyTemplateFile( + path.join(directory, 'src'), + 'declarations.d.ts', + ); await this.copyTemplateFile(path.join(directory, 'src'), 'main.ts'); await this.copyTemplateFile( path.join(directory, 'src'), @@ -146,11 +157,11 @@ class WebpackTemplate extends BaseTemplate { // Remove TS-only scripts from package.json for JS variant if (!typescript) { const packageJSONPath = path.resolve(directory, 'package.json'); - const packageJSON = await fs.readJson(packageJSONPath); + const packageJSON = await readJson(packageJSONPath); for (const script of TS_ONLY_SCRIPTS) { delete packageJSON.scripts[script]; } - await fs.writeJson(packageJSONPath, packageJSON, { spaces: 2 }); + await writeJson(packageJSONPath, packageJSON, { spaces: 2 }); } }, }, diff --git a/yarn.lock b/yarn.lock index adfd059813..b5cf186867 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1046,7 +1046,7 @@ __metadata: languageName: unknown linkType: soft -"@electron-forge/plugin-webpack@workspace:*, @electron-forge/plugin-webpack@workspace:packages/plugin/webpack": +"@electron-forge/plugin-webpack@workspace:packages/plugin/webpack": version: 0.0.0-use.local resolution: "@electron-forge/plugin-webpack@workspace:packages/plugin/webpack" dependencies: @@ -1212,18 +1212,6 @@ __metadata: languageName: node linkType: soft -"@electron-forge/template-vite-typescript@workspace:packages/template/vite-typescript": - version: 0.0.0-use.local - resolution: "@electron-forge/template-vite-typescript@workspace:packages/template/vite-typescript" - dependencies: - "@electron-forge/core-utils": "workspace:*" - "@electron-forge/shared-types": "workspace:*" - "@electron-forge/template-base": "workspace:*" - "@electron-forge/test-utils": "workspace:*" - vitest: "catalog:" - languageName: unknown - linkType: soft - "@electron-forge/template-vite@workspace:*, @electron-forge/template-vite@workspace:packages/template/vite": version: 0.0.0-use.local resolution: "@electron-forge/template-vite@workspace:packages/template/vite" @@ -1237,26 +1225,6 @@ __metadata: languageName: unknown linkType: soft -"@electron-forge/template-webpack-typescript@workspace:packages/template/webpack-typescript": - version: 0.0.0-use.local - resolution: "@electron-forge/template-webpack-typescript@workspace:packages/template/webpack-typescript" - dependencies: - "@electron-forge/core-utils": "workspace:*" - "@electron-forge/maker-deb": "workspace:*" - "@electron-forge/maker-rpm": "workspace:*" - "@electron-forge/maker-squirrel": "workspace:*" - "@electron-forge/maker-zip": "workspace:*" - "@electron-forge/plugin-webpack": "workspace:*" - "@electron-forge/shared-types": "workspace:*" - "@electron-forge/template-base": "workspace:*" - "@electron-forge/test-utils": "workspace:*" - fork-ts-checker-webpack-plugin: "npm:^7.2.13" - listr2: "npm:^7.0.2" - vitest: "catalog:" - webpack: "npm:^5.69.1" - languageName: unknown - linkType: soft - "@electron-forge/template-webpack@workspace:*, @electron-forge/template-webpack@workspace:packages/template/webpack": version: 0.0.0-use.local resolution: "@electron-forge/template-webpack@workspace:packages/template/webpack" @@ -1265,7 +1233,6 @@ __metadata: "@electron-forge/shared-types": "workspace:*" "@electron-forge/template-base": "workspace:*" "@electron-forge/test-utils": "workspace:*" - fs-extra: "npm:^10.0.0" listr2: "npm:^7.0.2" vitest: "catalog:" languageName: unknown From 63ac87ba06cb589314239c5f01154fc9a33739a3 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 21 May 2026 16:26:45 -0700 Subject: [PATCH 12/22] another removal --- .../spec/fast/init-scripts/find-template.spec.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts b/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts index 02d555066f..c4412bb4d5 100644 --- a/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts +++ b/packages/external/create-electron-app/spec/fast/init-scripts/find-template.spec.ts @@ -48,18 +48,4 @@ describe('findTemplate', () => { 'Failed to locate custom template: "non-existent-template".', ); }); - - describe('deprecated -typescript templates', () => { - it('should provide a helpful error for vite-typescript', async () => { - await expect(findTemplate('vite-typescript')).rejects.toThrowError( - /no longer exists.*--template vite/, - ); - }); - - it('should provide a helpful error for webpack-typescript', async () => { - await expect(findTemplate('webpack-typescript')).rejects.toThrowError( - /no longer exists.*--template webpack/, - ); - }); - }); }); From 11d544ef4287fb5ad416473c02b1552fa867f65e Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 21 May 2026 17:05:30 -0700 Subject: [PATCH 13/22] more fixes --- packages/template/vite/spec/ViteTemplate.spec.ts | 2 ++ packages/template/webpack/spec/WebpackTemplate.spec.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/template/vite/spec/ViteTemplate.spec.ts b/packages/template/vite/spec/ViteTemplate.spec.ts index 4194b7468f..a6683685f6 100644 --- a/packages/template/vite/spec/ViteTemplate.spec.ts +++ b/packages/template/vite/spec/ViteTemplate.spec.ts @@ -52,6 +52,7 @@ describe('ViteTemplate', () => { 'vite.main.config.ts', 'vite.preload.config.ts', 'vite.renderer.config.ts', + path.join('src', 'declarations.d.ts'), path.join('src', 'main.ts'), path.join('src', 'renderer.ts'), path.join('src', 'preload.ts'), @@ -153,6 +154,7 @@ describe('ViteTemplate', () => { 'vite.main.config.ts', 'vite.preload.config.ts', 'vite.renderer.config.ts', + path.join('src', 'declarations.d.ts'), path.join('src', 'main.ts'), path.join('src', 'renderer.ts'), path.join('src', 'preload.ts'), diff --git a/packages/template/webpack/spec/WebpackTemplate.spec.ts b/packages/template/webpack/spec/WebpackTemplate.spec.ts index a42c8c449c..91229cb78e 100644 --- a/packages/template/webpack/spec/WebpackTemplate.spec.ts +++ b/packages/template/webpack/spec/WebpackTemplate.spec.ts @@ -54,6 +54,7 @@ describe('WebpackTemplate', () => { 'webpack.rules.ts', 'webpack.plugins.ts', 'webpack.plugins.js', + path.join('src', 'declarations.d.ts'), path.join('src', 'main.ts'), path.join('src', 'renderer.ts'), path.join('src', 'preload.ts'), @@ -171,6 +172,7 @@ describe('WebpackTemplate', () => { 'webpack.renderer.config.ts', 'webpack.rules.ts', 'webpack.plugins.ts', + path.join('src', 'declarations.d.ts'), path.join('src', 'main.ts'), path.join('src', 'renderer.ts'), path.join('src', 'preload.ts'), From d487bdf83986d8fb9fa71becbb6067b5be74b405 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 22 May 2026 15:41:39 -0700 Subject: [PATCH 14/22] Update package.json Co-authored-by: Erik Moura --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4efd0c05dd..f7345d8612 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "MIT", "type": "module", "engines": { - "node": ">= " + "node": ">= 22.13.0" }, "resolutions": { "ajv@8.17.1": "^8.18.0", From a7e02f38e39991ce4a1b33f459e3a99c88c1e67c Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 22 May 2026 15:41:54 -0700 Subject: [PATCH 15/22] remove fs-extra --- packages/template/base/package.json | 1 - yarn.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/template/base/package.json b/packages/template/base/package.json index 684b244c08..05bd1e124f 100644 --- a/packages/template/base/package.json +++ b/packages/template/base/package.json @@ -16,7 +16,6 @@ "@electron-forge/shared-types": "workspace:*", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", - "fs-extra": "^10.0.0", "graceful-fs": "^4.2.11", "oxfmt": "^0.41.0", "semver": "^7.2.1", diff --git a/yarn.lock b/yarn.lock index b5cf186867..4fc172f098 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1197,7 +1197,6 @@ __metadata: "@electron-forge/shared-types": "workspace:*" "@malept/cross-spawn-promise": "npm:^2.0.0" debug: "npm:^4.3.1" - fs-extra: "npm:^10.0.0" graceful-fs: "npm:^4.2.11" oxfmt: "npm:^0.41.0" semver: "npm:^7.2.1" From 57dd0ab56f00cf60e77af729b2f525c0d6f96e63 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 22 May 2026 15:48:26 -0700 Subject: [PATCH 16/22] use utils :) --- packages/template/base/src/BaseTemplate.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/template/base/src/BaseTemplate.ts b/packages/template/base/src/BaseTemplate.ts index df0b91df5a..7af8175893 100644 --- a/packages/template/base/src/BaseTemplate.ts +++ b/packages/template/base/src/BaseTemplate.ts @@ -14,7 +14,6 @@ import { InitTemplateOptions, } from '@electron-forge/shared-types'; import debug from 'debug'; -import fsExtra from 'fs-extra'; import gracefulFs from 'graceful-fs'; import { format } from 'oxfmt'; import semver from 'semver'; @@ -27,7 +26,7 @@ const currentForgeVersion = readJsonSync( const d = debug('electron-forge:template:base'); const tmplDir = path.resolve(import.meta.dirname, '../tmpl'); -const oxfmtConfig = fsExtra.readJSONSync( +const oxfmtConfig = readJsonSync( path.resolve(import.meta.dirname, '../../../../.oxfmtrc.json'), ); @@ -138,11 +137,9 @@ export class BaseTemplate implements ForgeTemplate { async writeLintConfig(directory: string): Promise { await this.copyTemplateFile(directory, '.oxlintrc.json'); const { ignorePatterns: _, ...projectConfig } = oxfmtConfig; - await fsExtra.writeJson( - path.resolve(directory, '.oxfmtrc.json'), - projectConfig, - { spaces: 2 }, - ); + await writeJson(path.resolve(directory, '.oxfmtrc.json'), projectConfig, { + spaces: 2, + }); } async copyTemplateFile(destDir: string, basename: string): Promise { From 69bc0cb888b0af0d8be087576716ba378554f214 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 26 May 2026 13:07:58 -0700 Subject: [PATCH 17/22] revert vitest exclude for now --- vitest.config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitest.config.mts b/vitest.config.mts index ed14d6d23a..fdc6feca3b 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { clearMocks: true, - exclude: ['**/.links/**', '**/node_modules/**', '**/.claude/**'], + exclude: ['**/.links/**', '**/node_modules/**'], fileParallelism: false, projects: [ { From 41bb614cb0b9976ab5acf522b40ea63493c264c9 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 26 May 2026 13:08:28 -0700 Subject: [PATCH 18/22] address comments --- .../src/create-electron-app.ts | 2 +- packages/template/base/package.json | 1 - packages/template/base/src/BaseTemplate.ts | 22 +++++++++---------- .../template/webpack/src/WebpackTemplate.ts | 3 +++ yarn.lock | 1 - 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/external/create-electron-app/src/create-electron-app.ts b/packages/external/create-electron-app/src/create-electron-app.ts index 8e8270015a..15665dd98a 100644 --- a/packages/external/create-electron-app/src/create-electron-app.ts +++ b/packages/external/create-electron-app/src/create-electron-app.ts @@ -122,7 +122,7 @@ const initCommand = program }, ); - if (bundler !== 'base') { + if (bundler !== 'base' && !options.typescript) { initOpts.typescript = await prompt.run>( select, { diff --git a/packages/template/base/package.json b/packages/template/base/package.json index 05bd1e124f..f86180f26c 100644 --- a/packages/template/base/package.json +++ b/packages/template/base/package.json @@ -16,7 +16,6 @@ "@electron-forge/shared-types": "workspace:*", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", - "graceful-fs": "^4.2.11", "oxfmt": "^0.41.0", "semver": "^7.2.1", "username": "^5.1.0" diff --git a/packages/template/base/src/BaseTemplate.ts b/packages/template/base/src/BaseTemplate.ts index 7af8175893..95969de4b6 100644 --- a/packages/template/base/src/BaseTemplate.ts +++ b/packages/template/base/src/BaseTemplate.ts @@ -1,4 +1,5 @@ import fs from 'node:fs/promises'; +import { existsSync } from 'node:fs'; import { stripTypeScriptTypes } from 'node:module'; import path from 'node:path'; @@ -14,7 +15,6 @@ import { InitTemplateOptions, } from '@electron-forge/shared-types'; import debug from 'debug'; -import gracefulFs from 'graceful-fs'; import { format } from 'oxfmt'; import semver from 'semver'; @@ -26,9 +26,6 @@ const currentForgeVersion = readJsonSync( const d = debug('electron-forge:template:base'); const tmplDir = path.resolve(import.meta.dirname, '../tmpl'); -const oxfmtConfig = readJsonSync( - path.resolve(import.meta.dirname, '../../../../.oxfmtrc.json'), -); export class BaseTemplate implements ForgeTemplate { public templateDir = tmplDir; @@ -37,7 +34,7 @@ export class BaseTemplate implements ForgeTemplate { get dependencies(): string[] { const packageJSONPath = path.join(this.templateDir, 'package.json'); - if (gracefulFs.existsSync(packageJSONPath)) { + if (existsSync(packageJSONPath)) { const deps = readJsonSync(packageJSONPath).dependencies; if (deps) { return Object.entries(deps).map(([packageName, version]) => { @@ -54,7 +51,7 @@ export class BaseTemplate implements ForgeTemplate { get devDependencies(): string[] { const packageJSONPath = path.join(this.templateDir, 'package.json'); - if (gracefulFs.existsSync(packageJSONPath)) { + if (existsSync(packageJSONPath)) { const packageDevDeps = readJsonSync(packageJSONPath).devDependencies; if (packageDevDeps) { return Object.entries(packageDevDeps).map(([packageName, version]) => { @@ -136,10 +133,10 @@ export class BaseTemplate implements ForgeTemplate { async writeLintConfig(directory: string): Promise { await this.copyTemplateFile(directory, '.oxlintrc.json'); - const { ignorePatterns: _, ...projectConfig } = oxfmtConfig; - await writeJson(path.resolve(directory, '.oxfmtrc.json'), projectConfig, { - spaces: 2, - }); + await this.copy( + path.join(tmplDir, '.oxfmtrc.json'), + path.resolve(directory, '.oxfmtrc.json'), + ); } async copyTemplateFile(destDir: string, basename: string): Promise { @@ -160,7 +157,7 @@ export class BaseTemplate implements ForgeTemplate { this.templateDir, 'package.json', ); - if (gracefulFs.existsSync(templatePackageJSONPath)) { + if (existsSync(templatePackageJSONPath)) { const templatePackageJSON = await readJson(templatePackageJSONPath); const { dependencies, devDependencies, scripts, ...rest } = templatePackageJSON; @@ -197,7 +194,8 @@ export class BaseTemplate implements ForgeTemplate { async stripAndRename(srcPath: string, destPath: string): Promise { const source = await fs.readFile(srcPath, 'utf8'); - const stripped = stripTypeScriptTypes(source, { mode: 'transform' }); + const stripped = stripTypeScriptTypes(source, { mode: 'strip' }); + const oxfmtConfig = readJsonSync(path.join(tmplDir, '.oxfmtrc.json')); const formatted = await format(destPath, stripped, oxfmtConfig); await fs.writeFile(destPath, formatted.code); if (srcPath !== destPath) { diff --git a/packages/template/webpack/src/WebpackTemplate.ts b/packages/template/webpack/src/WebpackTemplate.ts index 25a2f8ae76..a3b0038497 100644 --- a/packages/template/webpack/src/WebpackTemplate.ts +++ b/packages/template/webpack/src/WebpackTemplate.ts @@ -95,6 +95,9 @@ class WebpackTemplate extends BaseTemplate { await this.copyTemplateFile(directory, 'webpack.plugins.ts'); await this.copyTemplateFile(directory, 'tsconfig.json'); } else { + // JS webpack configs are hand-maintained in tmpl/js/ because + // they differ structurally from the TS variants (no ts-loader, + // no fork-ts-checker-webpack-plugin, different rule sets). for (const name of [ 'webpack.main.config.mjs', 'webpack.renderer.config.mjs', diff --git a/yarn.lock b/yarn.lock index 4fc172f098..03d771eafa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1197,7 +1197,6 @@ __metadata: "@electron-forge/shared-types": "workspace:*" "@malept/cross-spawn-promise": "npm:^2.0.0" debug: "npm:^4.3.1" - graceful-fs: "npm:^4.2.11" oxfmt: "npm:^0.41.0" semver: "npm:^7.2.1" username: "npm:^5.1.0" From 84c6689eff0eb3986e2e384f831135f820c02d92 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Wed, 27 May 2026 22:48:34 -0700 Subject: [PATCH 19/22] format file too --- packages/template/base/src/BaseTemplate.ts | 11 ++++- packages/template/vite/src/ViteTemplate.ts | 31 +++++++------- .../template/webpack/src/WebpackTemplate.ts | 40 +++++++++---------- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/packages/template/base/src/BaseTemplate.ts b/packages/template/base/src/BaseTemplate.ts index 95969de4b6..c3fe6eea76 100644 --- a/packages/template/base/src/BaseTemplate.ts +++ b/packages/template/base/src/BaseTemplate.ts @@ -203,20 +203,29 @@ export class BaseTemplate implements ForgeTemplate { } } + async formatFile(filePath: string): Promise { + const source = await fs.readFile(filePath, 'utf8'); + const oxfmtConfig = readJsonSync(path.join(tmplDir, '.oxfmtrc.json')); + const formatted = await format(filePath, source, oxfmtConfig); + await fs.writeFile(filePath, formatted.code); + } + async updateFileByLine( inputPath: string, lineHandler: (line: string) => string | null, outputPath?: string | undefined, ): Promise { + const destPath = outputPath || inputPath; const fileContents = (await fs.readFile(inputPath, 'utf8')) .split('\n') .map(lineHandler) .filter((line): line is string => line !== null) .join('\n'); - await fs.writeFile(outputPath || inputPath, fileContents); + await fs.writeFile(destPath, fileContents); if (outputPath !== undefined) { await fs.rm(inputPath, { recursive: true, force: true }); } + await this.formatFile(destPath); } } diff --git a/packages/template/vite/src/ViteTemplate.ts b/packages/template/vite/src/ViteTemplate.ts index 58bc573609..42cd1cc94e 100644 --- a/packages/template/vite/src/ViteTemplate.ts +++ b/packages/template/vite/src/ViteTemplate.ts @@ -51,26 +51,25 @@ class ViteTemplate extends BaseTemplate { await this.copyTemplateFile(directory, 'forge.config.mts'); } else { await this.copyTemplateFile(directory, 'forge.config.mts'); + const forgeConfigPath = path.resolve(directory, 'forge.config.mjs'); await this.stripAndRename( path.resolve(directory, 'forge.config.mts'), - path.resolve(directory, 'forge.config.mjs'), + forgeConfigPath, ); // Patch entry/config paths from .ts to .js/.mjs - await this.updateFileByLine( - path.resolve(directory, 'forge.config.mjs'), - (line) => - line - .replace(/src\/main\.ts/g, 'src/main.js') - .replace(/src\/preload\.ts/g, 'src/preload.js') - .replace(/vite\.main\.config\.ts/g, 'vite.main.config.mjs') - .replace( - /vite\.preload\.config\.ts/g, - 'vite.preload.config.mjs', - ) - .replace( - /vite\.renderer\.config\.ts/g, - 'vite.renderer.config.mjs', - ), + await this.updateFileByLine(forgeConfigPath, (line) => + line + .replace(/src\/main\.ts/g, 'src/main.js') + .replace(/src\/preload\.ts/g, 'src/preload.js') + .replace(/vite\.main\.config\.ts/g, 'vite.main.config.mjs') + .replace( + /vite\.preload\.config\.ts/g, + 'vite.preload.config.mjs', + ) + .replace( + /vite\.renderer\.config\.ts/g, + 'vite.renderer.config.mjs', + ), ); } }, diff --git a/packages/template/webpack/src/WebpackTemplate.ts b/packages/template/webpack/src/WebpackTemplate.ts index a3b0038497..688f333dea 100644 --- a/packages/template/webpack/src/WebpackTemplate.ts +++ b/packages/template/webpack/src/WebpackTemplate.ts @@ -57,27 +57,25 @@ class WebpackTemplate extends BaseTemplate { ); // For JS, replace module imports with string-based config paths // and patch file references from .ts to .js - await this.updateFileByLine( - path.resolve(directory, 'forge.config.mjs'), - (line) => { - // Remove webpack config imports (JS uses string paths instead) - if (line.includes("from './webpack.")) return null; - // Replace object reference with string path - if (line.includes('mainConfig,')) - return line.replace( - 'mainConfig,', - "mainConfig: './webpack.main.config.mjs',", - ); - if (/config:\s*rendererConfig,/.test(line)) - return line.replace( - 'rendererConfig,', - "'./webpack.renderer.config.mjs',", - ); - return line - .replace(/src\/renderer\.ts/g, 'src/renderer.js') - .replace(/src\/preload\.ts/g, 'src/preload.js'); - }, - ); + const forgeConfigPath = path.resolve(directory, 'forge.config.mjs'); + await this.updateFileByLine(forgeConfigPath, (line) => { + // Remove webpack config imports (JS uses string paths instead) + if (line.includes("from './webpack.")) return null; + // Replace object reference with string path + if (line.includes('mainConfig,')) + return line.replace( + 'mainConfig,', + "mainConfig: './webpack.main.config.mjs',", + ); + if (/config:\s*rendererConfig,/.test(line)) + return line.replace( + 'rendererConfig,', + "'./webpack.renderer.config.mjs',", + ); + return line + .replace(/src\/renderer\.ts/g, 'src/renderer.js') + .replace(/src\/preload\.ts/g, 'src/preload.js'); + }); } }, }, From 2082b7d9286c0434b5307c3cc00140c2b073426a Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 4 Jun 2026 17:33:49 -0700 Subject: [PATCH 20/22] fixup --- .../src/init-scripts/find-template.ts | 12 +++++++++--- packages/template/vite/tmpl/renderer.ts | 2 +- packages/template/webpack/tmpl/renderer.ts | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/external/create-electron-app/src/init-scripts/find-template.ts b/packages/external/create-electron-app/src/init-scripts/find-template.ts index 5e6e571f5d..f16c8cd59d 100644 --- a/packages/external/create-electron-app/src/init-scripts/find-template.ts +++ b/packages/external/create-electron-app/src/init-scripts/find-template.ts @@ -44,10 +44,16 @@ export const findTemplate = async ( } } if (!foundTemplate) { - const tsMatch = template.match(/^(.+)-typescript$/); - if (tsMatch) { + // Only the bundled `vite`/`webpack` templates were unified behind the + // `--typescript` flag, so only redirect the old short-form names. Skip + // absolute paths and third-party templates, which don't honor the flag. + if ( + !isAbsolutePath && + (template === 'vite-typescript' || template === 'webpack-typescript') + ) { + const bundler = template.replace(/-typescript$/, ''); throw new Error( - `The "${template}" template no longer exists. Use "--template ${tsMatch[1]} --typescript" instead.`, + `The "${template}" template no longer exists. Use "--template ${bundler} --typescript" instead.`, ); } throw new Error(`Failed to locate custom template: "${template}".`); diff --git a/packages/template/vite/tmpl/renderer.ts b/packages/template/vite/tmpl/renderer.ts index 433b9f52e9..05bbd51b0b 100644 --- a/packages/template/vite/tmpl/renderer.ts +++ b/packages/template/vite/tmpl/renderer.ts @@ -15,5 +15,5 @@ import './index.css'; console.log( - '👋 This message is being logged by "renderer.ts", included via Vite', + '👋 This message is being logged by the renderer process, included via Vite', ); diff --git a/packages/template/webpack/tmpl/renderer.ts b/packages/template/webpack/tmpl/renderer.ts index 34cbe78040..8e6c1ecf46 100644 --- a/packages/template/webpack/tmpl/renderer.ts +++ b/packages/template/webpack/tmpl/renderer.ts @@ -15,5 +15,5 @@ import './index.css'; console.log( - '👋 This message is being logged by "renderer.ts", included via webpack', + '👋 This message is being logged by the renderer process, included via webpack', ); From e8819df568a8d2b867f176cb9a283a7b46bf0886 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 4 Jun 2026 23:04:20 -0700 Subject: [PATCH 21/22] remove TS-specific comment from webpack main.ts template The 3-line comment explaining "TypeScript magic constants" survives type stripping and appears in the generated JS output, referencing declarations that no longer exist. Remove it since the declare const lines are self-explanatory for TS users. Co-Authored-By: Claude --- packages/template/webpack/tmpl/main.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/template/webpack/tmpl/main.ts b/packages/template/webpack/tmpl/main.ts index f0e15df4fe..ed09c8b045 100644 --- a/packages/template/webpack/tmpl/main.ts +++ b/packages/template/webpack/tmpl/main.ts @@ -1,7 +1,5 @@ import { app, BrowserWindow } from 'electron'; -// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack -// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on -// whether you're running in development or production). + declare const MAIN_WINDOW_WEBPACK_ENTRY: string; declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string; From 1a9807f5f12e1ebdb9fc3aeca1edc5034d72a60f Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 9 Jun 2026 11:41:56 -0700 Subject: [PATCH 22/22] fix(template): close base+typescript guard bypass and drop orphaned lint directive Move the "base does not support TypeScript" rejection into BaseTemplate.initializeTemplate (gated on the base templateDir) so it fires regardless of how the template was resolved, including the fully-qualified `@electron-forge/template-base` name that bypassed the string compare in init.ts. Switch webpack's main.ts to the ESM `import started from 'electron-squirrel-startup'` pattern that vite already uses. This removes the `// oxlint-disable-next-line @typescript-eslint/no-require-imports` directive that stripTypeScriptTypes preserved verbatim into the generated JS scaffold, where the referenced rule isn't even enabled. Adds @types/electron-squirrel-startup to the webpack TS devDeps (filtered out of the JS variant) so `tsc --noEmit` still passes. Co-Authored-By: Claude --- packages/external/create-electron-app/src/init.ts | 6 ------ packages/template/base/src/BaseTemplate.ts | 14 +++++++++++++- packages/template/webpack/src/WebpackTemplate.ts | 1 + packages/template/webpack/tmpl/main.ts | 4 ++-- packages/template/webpack/tmpl/package.json | 1 + 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/external/create-electron-app/src/init.ts b/packages/external/create-electron-app/src/init.ts index 4e85283357..4ff5fb07bf 100644 --- a/packages/external/create-electron-app/src/init.ts +++ b/packages/external/create-electron-app/src/init.ts @@ -103,12 +103,6 @@ export async function init({ }: InitOptions): Promise { d(`Initializing in: ${dir}`); - if (typescript && template === 'base') { - throw new Error( - 'The "base" template does not support TypeScript. Use "--template vite" or "--template webpack" with "--typescript".', - ); - } - const runner = new Listr<{ templateModule: ForgeTemplate; pm: PMDetails; diff --git a/packages/template/base/src/BaseTemplate.ts b/packages/template/base/src/BaseTemplate.ts index c3fe6eea76..77e931fbfd 100644 --- a/packages/template/base/src/BaseTemplate.ts +++ b/packages/template/base/src/BaseTemplate.ts @@ -68,8 +68,20 @@ export class BaseTemplate implements ForgeTemplate { public async initializeTemplate( directory: string, - { copyCIFiles }: InitTemplateOptions, + { copyCIFiles, typescript }: InitTemplateOptions, ): Promise { + // The base template only ships JavaScript starter files and ignores the + // `typescript` flag. Reject here rather than in `init` so the guard holds + // no matter how the template was resolved (`base`, + // `@electron-forge/template-base`, etc.). The `vite`/`webpack` subclasses + // honor the flag and override `templateDir`, so this only trips for a + // direct base-template scaffold, not their `super.initializeTemplate` calls. + if (typescript && this.templateDir === tmplDir) { + throw new Error( + 'The "base" template does not support TypeScript. Use "--template vite" or "--template webpack" with "--typescript".', + ); + } + return [ { title: 'Copying starter files', diff --git a/packages/template/webpack/src/WebpackTemplate.ts b/packages/template/webpack/src/WebpackTemplate.ts index 688f333dea..52ffb1b719 100644 --- a/packages/template/webpack/src/WebpackTemplate.ts +++ b/packages/template/webpack/src/WebpackTemplate.ts @@ -9,6 +9,7 @@ import { import { BaseTemplate } from '@electron-forge/template-base'; const TS_ONLY_DEV_DEPS = new Set([ + '@types/electron-squirrel-startup', 'fork-ts-checker-webpack-plugin', 'ts-loader', 'typescript', diff --git a/packages/template/webpack/tmpl/main.ts b/packages/template/webpack/tmpl/main.ts index ed09c8b045..2a39e9ff94 100644 --- a/packages/template/webpack/tmpl/main.ts +++ b/packages/template/webpack/tmpl/main.ts @@ -1,11 +1,11 @@ import { app, BrowserWindow } from 'electron'; +import started from 'electron-squirrel-startup'; declare const MAIN_WINDOW_WEBPACK_ENTRY: string; declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string; // Handle creating/removing shortcuts on Windows when installing/uninstalling. -// oxlint-disable-next-line @typescript-eslint/no-require-imports -if (require('electron-squirrel-startup')) { +if (started) { app.quit(); } diff --git a/packages/template/webpack/tmpl/package.json b/packages/template/webpack/tmpl/package.json index 2601aea3c4..7f1639cbaf 100644 --- a/packages/template/webpack/tmpl/package.json +++ b/packages/template/webpack/tmpl/package.json @@ -7,6 +7,7 @@ }, "devDependencies": { "@electron-forge/plugin-webpack": "ELECTRON_FORGE/VERSION", + "@types/electron-squirrel-startup": "^1.0.2", "@vercel/webpack-asset-relocator-loader": "1.7.3", "css-loader": "^6.0.0", "fork-ts-checker-webpack-plugin": "^7.2.13",