From 5c1372a681d9456e63fd4567ceb38087be0177bd Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 20 Apr 2026 12:31:08 -0700 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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) {