diff --git a/.eslintignore b/.eslintignore index f7b1d78263..ac8c1e5dca 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,3 +7,4 @@ packages/*/*/index.ts packages/**/bad.js tmpl packages/api/core/helper/dynamic-import.js +packages/api/core/spec/fixture/api-tester/ diff --git a/package.json b/package.json index f5295a2b55..2a45ee8288 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@yarnpkg/types": "^4.0.1", + "electron": "^39.2.6", "electron-installer-common": "^0.10.2", "eslint": "^8.56.0", "eslint-config-prettier": "^10.0.1", @@ -117,6 +118,7 @@ "minimist": "^1.2.6", "msw": "^2.7.0", "prettier": "^3.6.2", + "ref-napi": "^3.0.3", "typedoc": "0.25.13", "typescript": "~5.4.5", "vitest": "^4.0.14", @@ -152,6 +154,9 @@ "@bitdisaster/exe-icon-extractor": { "built": true }, + "electron": { + "built": true + }, "electron-winstaller": { "built": true }, diff --git a/packages/api/core/spec/fixture/api-tester/forge.config.js b/packages/api/core/spec/fixture/api-tester/forge.config.js new file mode 100644 index 0000000000..71b001e601 --- /dev/null +++ b/packages/api/core/spec/fixture/api-tester/forge.config.js @@ -0,0 +1,18 @@ +module.exports = { + packagerConfig: { + asar: true, + }, + rebuildConfig: {}, + makers: [ + { + name: '@electron-forge/maker-zip', + platforms: ['darwin', 'linux', 'win32'], + }, + ], + plugins: [ + { + name: '@electron-forge/plugin-auto-unpack-natives', + config: {}, + }, + ], +}; diff --git a/packages/api/core/spec/fixture/api-tester/package.json b/packages/api/core/spec/fixture/api-tester/package.json new file mode 100644 index 0000000000..e5a871d55f --- /dev/null +++ b/packages/api/core/spec/fixture/api-tester/package.json @@ -0,0 +1,42 @@ +{ + "name": "api-tester", + "productName": "api-tester", + "version": "1.0.0", + "description": "Test Electron application for Forge API tests", + "main": "src/index.js", + "scripts": { + "start": "electron-forge start", + "package": "electron-forge package", + "make": "electron-forge make" + }, + "keywords": [], + "author": "Test Author", + "license": "MIT", + "homepage": "http://www.electronjs.org/", + "devDependencies": { + "@electron-forge/cli": "file:../../../../../api/cli", + "@electron-forge/core": "file:../../../../../api/core", + "@electron-forge/core-utils": "file:../../../../../utils/core-utils", + "@electron-forge/maker-appx": "file:../../../../../maker/appx", + "@electron-forge/maker-base": "file:../../../../../maker/base", + "@electron-forge/maker-deb": "file:../../../../../maker/deb", + "@electron-forge/maker-dmg": "file:../../../../../maker/dmg", + "@electron-forge/maker-flatpak": "file:../../../../../maker/flatpak", + "@electron-forge/maker-msix": "file:../../../../../maker/msix", + "@electron-forge/maker-pkg": "file:../../../../../maker/pkg", + "@electron-forge/maker-rpm": "file:../../../../../maker/rpm", + "@electron-forge/maker-snap": "file:../../../../../maker/snap", + "@electron-forge/maker-squirrel": "file:../../../../../maker/squirrel", + "@electron-forge/maker-wix": "file:../../../../../maker/wix", + "@electron-forge/maker-zip": "file:../../../../../maker/zip", + "@electron-forge/plugin-auto-unpack-natives": "file:../../../../../plugin/auto-unpack-natives", + "@electron-forge/plugin-base": "file:../../../../../plugin/base", + "@electron-forge/plugin-fuses": "file:../../../../../plugin/fuses", + "@electron-forge/shared-types": "file:../../../../../utils/types", + "@electron/fuses": "file:../../../../../../node_modules/@electron/fuses", + "electron": "file:../../../../../../node_modules/electron" + }, + "config": { + "forge": "./forge.config.js" + } +} diff --git a/packages/api/core/spec/fixture/api-tester/src/index.js b/packages/api/core/spec/fixture/api-tester/src/index.js new file mode 100644 index 0000000000..bac729e29d --- /dev/null +++ b/packages/api/core/spec/fixture/api-tester/src/index.js @@ -0,0 +1,24 @@ +const { app, BrowserWindow } = require('electron'); + +const createWindow = () => { + const mainWindow = new BrowserWindow(); + + // and load the index.html of the app. + mainWindow.loadURL('https://electronjs.org'); +}; + +app.whenReady().then(() => { + createWindow(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); diff --git a/packages/api/core/spec/slow/api.slow.spec.ts b/packages/api/core/spec/slow/api.slow.spec.ts deleted file mode 100644 index 352882e436..0000000000 --- a/packages/api/core/spec/slow/api.slow.spec.ts +++ /dev/null @@ -1,689 +0,0 @@ -import assert from 'node:assert'; -import { execSync } from 'node:child_process'; -import fs from 'node:fs'; -import path from 'node:path'; - -import { - PACKAGE_MANAGERS, - spawnPackageManager, -} from '@electron-forge/core-utils'; -import { createDefaultCertificate } from '@electron-forge/maker-appx'; -import { - ForgeConfig, - IForgeResolvableMaker, -} from '@electron-forge/shared-types'; -import { - ensureTestDirIsNonexistent, - expectLintToPass, -} from '@electron-forge/test-utils'; -import { readMetadata } from 'electron-installer-common'; -import semver from 'semver'; -import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; - -import { api, InitOptions } from '../../src/api/index'; -import { installDependencies } from '../../src/util/install-dependencies'; -import { readRawPackageJson } from '../../src/util/read-package-json'; - -type BeforeInitFunction = () => void; -type PackageJSON = Record & { - config: { - forge: ForgeConfig; - }; - dependencies: Record; -}; - -async function updatePackageJSON( - dir: string, - packageJSONUpdater: (packageJSON: PackageJSON) => Promise, -) { - const packageJSON = await readRawPackageJson(dir); - await packageJSONUpdater(packageJSON); - await fs.promises.writeFile( - path.resolve(dir, 'package.json'), - JSON.stringify(packageJSON), - 'utf-8', - ); -} - -// TODO: move more tests outside of the describe.each block -// if the actual package manager doesn't matter for the test -describe('init params', () => { - let dir: string; - describe('init (with electronVersion)', () => { - beforeEach(async () => { - dir = await ensureTestDirIsNonexistent(); - - return async () => { - await fs.promises.rm(dir, { recursive: true, force: true }); - }; - }); - - it('can define a specific Electron version with a version number', async () => { - await api.init({ - dir, - electronVersion: 'v38.0.0', - }); - const packageJSON = await import(path.resolve(dir, 'package.json')); - expect(packageJSON.devDependencies.electron).toEqual('38.0.0'); - }); - - it('can define a specific Electron nightly version with a version number', async () => { - await api.init({ - dir, - electronVersion: '40.0.0-nightly.20251020', - }); - const packageJSON = await import(path.resolve(dir, 'package.json')); - expect( - semver.valid(packageJSON.devDependencies['electron-nightly']), - ).not.toBeNull(); - expect(packageJSON.devDependencies.electron).not.toBeDefined(); - }); - - it('can define a specific Electron prerelease version with the beta tag', async () => { - await api.init({ - dir, - electronVersion: 'beta', - }); - const packageJSON = await import(path.resolve(dir, 'package.json')); - const prereleaseTag = semver.prerelease( - packageJSON.devDependencies.electron, - ); - expect(prereleaseTag).toEqual( - expect.arrayContaining([expect.stringMatching(/alpha|beta/)]), - ); - }); - - it('can define a specific Electron nightly version with the nightly tag', async () => { - await api.init({ - dir, - electronVersion: 'nightly', - }); - const packageJSON = await import(path.resolve(dir, 'package.json')); - expect( - semver.valid(packageJSON.devDependencies['electron-nightly']), - ).not.toBeNull(); - expect(packageJSON.devDependencies.electron).not.toBeDefined(); - }); - }); -}); - -describe.each([ - PACKAGE_MANAGERS['npm'], - PACKAGE_MANAGERS['yarn'], - PACKAGE_MANAGERS['pnpm'], -])(`init (with $executable)`, (pm) => { - let dir: string; - - beforeAll(async () => { - const originalCorepackStrict = process.env.COREPACK_ENABLE_STRICT; - if (pm.executable === 'pnpm') { - // temporarily disable corepack to enable pnpm to set links - process.env.COREPACK_ENABLE_STRICT = '0'; - - await spawnPackageManager( - pm, - 'config set node-linker hoisted'.split(' '), - ); - } - - return async () => { - // Unlink packages created during tests (syntax varies by package manager) - if (pm.executable === 'yarn') { - // yarn unlink --all removes all linked packages - await spawnPackageManager(pm, ['unlink', '--all']); - } else if (pm.executable === 'pnpm') { - // pnpm doesn't support --all, and requires Corepack bypass - await spawnPackageManager(pm, ['unlink']); - } - delete process.env.NODE_INSTALLER; - if (originalCorepackStrict === undefined) { - delete process.env.COREPACK_ENABLE_STRICT; - } else { - process.env.COREPACK_ENABLE_STRICT = originalCorepackStrict; - } - }; - }); - - const beforeInitTest = ( - params?: Partial, - beforeInit?: BeforeInitFunction, - ) => { - beforeAll(async () => { - dir = await ensureTestDirIsNonexistent(); - if (beforeInit) { - beforeInit(); - } - await api.init({ ...params, dir }); - }); - }; - - describe('init (with skipGit)', () => { - beforeAll(async () => { - dir = await ensureTestDirIsNonexistent(); - - return async () => { - await fs.promises.rm(dir, { recursive: true, force: true }); - }; - }); - - it('should not initialize a git repo if passed the skipGit option', async () => { - await api.init({ - dir, - skipGit: true, - }); - expect(fs.existsSync(path.join(dir, '.git'))).toEqual(false); - }); - }); - - describe('init', () => { - beforeInitTest(); - - afterAll(() => fs.promises.rm(dir, { recursive: true, force: true })); - - it('should fail in initializing an already initialized directory', async () => { - await expect(api.init({ dir })).rejects.toThrow( - `The specified path: "${dir}" is not empty. Please ensure it is empty before initializing a new project`, - ); - }); - - it('should initialize an already initialized directory when forced to', async () => { - await api.init({ - dir, - force: true, - }); - }); - - it('should create a new folder with a npm module inside', async () => { - expect( - fs.existsSync(dir), - 'the target dir should have been created', - ).toEqual(true); - expect(fs.existsSync(path.join(dir, 'package.json'))).toEqual(true); - expect(fs.existsSync(path.join(dir, '.git'))).toEqual(true); - expect( - fs.existsSync(path.resolve(dir, 'node_modules/electron')), - 'electron should exist', - ).toEqual(true); - expect( - fs.existsSync( - path.resolve(dir, 'node_modules/electron-squirrel-startup'), - ), - 'electron-squirrel-startup should exist', - ).toEqual(true); - expect( - fs.existsSync(path.resolve(dir, 'node_modules/@electron-forge/cli')), - '@electron-forge/cli should exist', - ).toEqual(true); - expect(fs.existsSync(path.join(dir, 'forge.config.js'))).toEqual(true); - }); - - describe('lint', () => { - it('should initially pass the linting process', () => - expectLintToPass(dir)); - }); - }); - - describe.skip('init with CI files enabled', () => { - beforeInitTest({ copyCIFiles: true }); - it.todo('should copy over the CI config files correctly'); - }); - - describe('init (with custom templater)', () => { - beforeInitTest({ - template: path.resolve(__dirname, '../fixture/custom_init'), - }); - - it('should add custom dependencies', async () => { - const packageJSON = await import(path.resolve(dir, 'package.json')); - expect(packageJSON.dependencies).toHaveProperty('debug'); - }); - - it('should add custom devDependencies', async () => { - const packageJSON = await import(path.resolve(dir, 'package.json')); - expect(packageJSON.devDependencies).toHaveProperty('lodash'); - }); - - it('should create dot files correctly', async () => { - expect( - fs.existsSync(dir), - 'the target dir should have been created', - ).toEqual(true); - expect(fs.existsSync(path.join(dir, '.bar'))).toEqual(true); - }); - - it('should create deep files correctly', async () => { - expect(fs.existsSync(path.join(dir, 'src/foo.js'))).toBe(true); - expect(fs.existsSync(path.join(dir, 'src/index.html'))).toBe(true); - }); - - describe('lint', () => { - it('should initially pass the linting process', () => - expectLintToPass(dir)); - }); - - afterAll(async () => { - await fs.promises.rm(dir, { recursive: true, force: true }); - execSync('npm unlink -g', { - cwd: path.resolve(__dirname, '../fixture/custom_init'), - }); - }); - }); - - describe('init (with a templater sans required Forge version)', () => { - beforeAll(async () => { - dir = await ensureTestDirIsNonexistent(); - - return async () => { - await fs.promises.rm(dir, { recursive: true, force: true }); - }; - }); - - it('should fail in initializing', async () => { - await expect( - api.init({ - dir, - template: path.resolve( - __dirname, - '../fixture/template-sans-forge-version', - ), - }), - ).rejects.toThrow(/it does not specify its required Forge version\.$/); - }); - }); - - describe('init (with a templater with a non-matching Forge version)', () => { - beforeAll(async () => { - dir = await ensureTestDirIsNonexistent(); - - return async () => { - await fs.promises.rm(dir, { recursive: true, force: true }); - }; - }); - - it('should fail in initializing', async () => { - await expect( - api.init({ - dir, - template: path.resolve( - __dirname, - '../fixture/template-nonmatching-forge-version', - ), - }), - ).rejects.toThrow( - /is not compatible with this version of Electron Forge/, - ); - }); - }); - - describe('init (with a nonexistent templater)', () => { - beforeAll(async () => { - dir = await ensureTestDirIsNonexistent(); - - return async () => { - await fs.promises.rm(dir, { recursive: true, force: true }); - }; - }); - - it('should fail in initializing', async () => { - await expect( - api.init({ - dir, - template: 'does-not-exist', - }), - ).rejects.toThrow('Failed to locate custom template'); - }); - }); - - describe('import', () => { - beforeEach(async () => { - dir = await ensureTestDirIsNonexistent(); - await fs.promises.mkdir(dir); - execSync( - `git clone https://github.com/electron/minimal-repro.git . --quiet`, - { - cwd: dir, - }, - ); - - return async () => { - await fs.promises.rm(dir, { recursive: true, force: true }); - }; - }); - - it('creates forge.config.js and can successfully package the application', async () => { - await updatePackageJSON(dir, async (packageJSON) => { - packageJSON.name = 'Name'; - packageJSON.productName = 'ProductName'; - }); - - await api.import({ dir }); - - expect(fs.existsSync(path.join(dir, 'forge.config.js'))).toEqual(true); - - await api.package({ dir }); - - const outDirContents = fs.readdirSync(path.join(dir, 'out')); - expect(outDirContents).toHaveLength(1); - expect(outDirContents[0]).toEqual( - `ProductName-${process.platform}-${process.arch}`, - ); - }); - }); - - describe('Electron Forge API', () => { - let dir: string; - - describe('after init', () => { - let devCert: string; - - beforeAll(async () => { - dir = path.join( - await ensureTestDirIsNonexistent(), - 'electron-forge-test', - ); - await api.init({ dir }); - - await updatePackageJSON(dir, async (packageJSON) => { - packageJSON.name = 'testapp'; - packageJSON.version = '1.0.0-beta.1'; - packageJSON.productName = 'Test-App'; - packageJSON.config = packageJSON.config || {}; - packageJSON.config.forge = { - ...packageJSON.config.forge, - packagerConfig: { - asar: false, - }, - }; - if (process.platform === 'win32') { - await fs.promises.copyFile( - path.join(__dirname, '..', 'fixture', 'bogus-private-key.pvk'), - path.join(dir, 'default.pvk'), - ); - devCert = await createDefaultCertificate('CN=Test Author', { - certFilePath: dir, - }); - } else if (process.platform === 'linux') { - packageJSON.config.forge.packagerConfig = { - ...packageJSON.config.forge.packagerConfig, - executableName: 'testapp', - }; - } - packageJSON.homepage = 'http://www.example.com/'; - packageJSON.author = 'Test Author'; - }); - - return async () => { - await fs.promises.rm(dir, { recursive: true, force: true }); - }; - }); - - it('throws an error when all is set', async () => { - await updatePackageJSON(dir, async (packageJSON) => { - assert(packageJSON.config.forge.packagerConfig); - packageJSON.config.forge.packagerConfig.all = true; - }); - await expect(api.package({ dir })).rejects.toThrow( - /packagerConfig\.all is not supported by Electron Forge/, - ); - await updatePackageJSON(dir, async (packageJSON) => { - assert(packageJSON.config.forge.packagerConfig); - delete packageJSON.config.forge.packagerConfig.all; - }); - }); - - it('can package to outDir without errors', async () => { - const outDir = `${dir}/foo`; - - expect(fs.existsSync(outDir)).toEqual(false); - - await api.package({ dir, outDir }); - - expect(fs.existsSync(outDir)).toEqual(true); - }); - - it('can make from custom outDir without errors', async () => { - await updatePackageJSON(dir, async (packageJSON) => { - // eslint-disable-next-line n/no-missing-require - packageJSON.config.forge.makers = [ - { - name: require.resolve('@electron-forge/maker-zip'), - } as IForgeResolvableMaker, - ]; - }); - - await api.make({ dir, skipPackage: true, outDir: `${dir}/foo` }); - - // Cleanup the custom outDir - await fs.promises.rm(path.resolve(dir, 'foo'), { - recursive: true, - force: true, - }); - }); - - describe('with prebuilt native module deps installed', () => { - beforeAll(async () => { - await installDependencies(pm, dir, ['ref-napi']); - - return async () => { - await fs.promises.rm(path.resolve(dir, 'node_modules/ref-napi'), { - recursive: true, - force: true, - }); - await updatePackageJSON(dir, async (packageJSON) => { - delete packageJSON.dependencies['ref-napi']; - }); - }; - }); - - it('can package without errors', async () => { - await api.package({ dir }); - }); - }); - - it('can package without errors', async () => { - await updatePackageJSON(dir, async (packageJSON) => { - assert(packageJSON.config.forge.packagerConfig); - packageJSON.config.forge.packagerConfig.asar = true; - }); - - await api.package({ dir }); - }); - - describe('after package', () => { - it('should have deleted the forge config from the packaged app', async () => { - const cleanPackageJSON = await readMetadata({ - src: path.resolve( - dir, - 'out', - `Test-App-${process.platform}-${process.arch}`, - ), - logger: console.error, - }); - expect(cleanPackageJSON).not.toHaveProperty('config.forge'); - }); - - it('should not affect the actual forge config', async () => { - const normalPackageJSON = await readRawPackageJson(dir); - expect(normalPackageJSON).toHaveProperty('config.forge'); - }); - - if (process.platform !== 'win32') { - process.env.DISABLE_SQUIRREL_TEST = 'true'; - } - - function getMakers(good: boolean) { - const allMakers = [ - '@electron-forge/maker-appx', - '@electron-forge/maker-deb', - '@electron-forge/maker-dmg', - '@electron-forge/maker-flatpak', - '@electron-forge/maker-msix', - '@electron-forge/maker-rpm', - '@electron-forge/maker-snap', - '@electron-forge/maker-squirrel', - '@electron-forge/maker-wix', - '@electron-forge/maker-zip', - ]; - return allMakers - .map((maker) => require.resolve(maker)) - .filter((makerPath) => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const MakerClass = require(makerPath).default; - const maker = new MakerClass(); - return ( - maker.isSupportedOnCurrentPlatform() === good && - maker.externalBinariesExist() === good - ); - }) - .map((makerPath) => () => { - const makerDefinition = { - name: makerPath, - platforms: [process.platform], - config: { - devCert, - }, - }; - - if (process.platform === 'win32') { - ( - makerDefinition.config as Record - ).makeVersionWinStoreCompatible = true; - } - - return makerDefinition; - }); - } - - const goodMakers = getMakers(true); - const badMakers = getMakers(false); - - const testMakeTarget = function testMakeTarget( - target: () => { name: string }, - shouldPass: boolean, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...options: any[] - ) { - describe(`make (with target=${target().name})`, async () => { - beforeAll(async () => { - await updatePackageJSON(dir, async (packageJSON) => { - packageJSON.config.forge.makers = [ - target() as IForgeResolvableMaker, - ]; - }); - }); - - for (const optionsFetcher of options) { - if (shouldPass) { - it(`successfully makes for config: ${JSON.stringify(optionsFetcher())}`, async () => { - const outputs = await api.make(optionsFetcher()); - for (const outputResult of outputs) { - for (const output of outputResult.artifacts) { - expect(fs.existsSync(output)).toEqual(true); - expect( - output.startsWith(path.resolve(dir, 'out', 'make')), - ).toEqual(true); - } - } - }); - } else { - it(`fails for config: ${JSON.stringify(optionsFetcher())}`, async () => { - await expect(api.make(optionsFetcher())).rejects.toThrow(); - }); - } - } - }); - }; - - const targetOptionFetcher = () => ({ dir, skipPackage: true }); - for (const maker of goodMakers) { - testMakeTarget(maker, true, targetOptionFetcher); - } - - for (const maker of badMakers) { - testMakeTarget(maker, false, targetOptionFetcher); - } - - describe('make', () => { - it('throws an error when given an unrecognized platform', async () => { - await expect(api.make({ dir, platform: 'dos' })).rejects.toThrow( - /invalid platform/, - ); - }); - - it("throws an error when the specified maker doesn't support the current platform", async () => { - const makerPath = path.resolve( - __dirname, - '../fixture/maker-unsupported', - ); - await expect( - api.make({ - dir, - overrideTargets: [ - { - name: makerPath, - } as IForgeResolvableMaker, - ], - skipPackage: true, - }), - ).rejects.toThrow(/the maker declared that it cannot run/); - }); - - it("throws an error when the specified maker doesn't implement isSupportedOnCurrentPlatform()", async () => { - const makerPath = path.resolve( - __dirname, - '../fixture/maker-incompatible', - ); - await expect( - api.make({ - dir, - overrideTargets: [ - { - name: makerPath, - } as IForgeResolvableMaker, - ], - skipPackage: true, - }), - ).rejects.toThrow(/incompatible with this version/); - }); - - it('throws an error when no makers are configured for the given platform', async () => { - await expect( - api.make({ - dir, - overrideTargets: [ - { - name: path.resolve( - __dirname, - '../fixture/maker-wrong-platform', - ), - } as IForgeResolvableMaker, - ], - platform: 'linux', - skipPackage: true, - }), - ).rejects.toThrow( - 'Could not find any make targets configured for the "linux" platform.', - ); - }); - - it.runIf(process.platform === 'darwin')( - 'can make for the MAS platform successfully', - async () => { - await expect( - api.make({ - dir, - // eslint-disable-next-line n/no-missing-require - overrideTargets: [ - require.resolve('@electron-forge/maker-zip'), - require.resolve('@electron-forge/maker-dmg'), - ], - platform: 'mas', - }), - ).resolves.toHaveLength(2); - }, - ); - }); - }); - }); - }); -}); diff --git a/packages/api/core/spec/slow/init-import.slow.spec.ts b/packages/api/core/spec/slow/init-import.slow.spec.ts new file mode 100644 index 0000000000..4f3913df5a --- /dev/null +++ b/packages/api/core/spec/slow/init-import.slow.spec.ts @@ -0,0 +1,364 @@ +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { + PACKAGE_MANAGERS, + spawnPackageManager, +} from '@electron-forge/core-utils'; +import { ForgeConfig } from '@electron-forge/shared-types'; +import { + ensureTestDirIsNonexistent, + expectLintToPass, +} from '@electron-forge/test-utils'; +import semver from 'semver'; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; + +import { api, InitOptions } from '../../src/api/index'; +import { readRawPackageJson } from '../../src/util/read-package-json'; + +type BeforeInitFunction = () => void; +type PackageJSON = Record & { + config: { + forge: ForgeConfig; + }; + dependencies: Record; +}; + +async function updatePackageJSON( + dir: string, + packageJSONUpdater: (packageJSON: PackageJSON) => Promise, +) { + const packageJSON = await readRawPackageJson(dir); + await packageJSONUpdater(packageJSON); + await fs.promises.writeFile( + path.resolve(dir, 'package.json'), + JSON.stringify(packageJSON), + 'utf-8', + ); +} + +// TODO: move more tests outside of the describe.each block +// if the actual package manager doesn't matter for the test +describe('init params', () => { + let dir: string; + describe('init (with electronVersion)', () => { + beforeEach(async () => { + dir = await ensureTestDirIsNonexistent(); + + return async () => { + await fs.promises.rm(dir, { recursive: true, force: true }); + }; + }); + + it('can define a specific Electron version with a version number', async () => { + await api.init({ + dir, + electronVersion: 'v38.0.0', + }); + const packageJSON = await import(path.resolve(dir, 'package.json')); + expect(packageJSON.devDependencies.electron).toEqual('38.0.0'); + }); + + it('can define a specific Electron nightly version with a version number', async () => { + await api.init({ + dir, + electronVersion: '40.0.0-nightly.20251020', + }); + const packageJSON = await import(path.resolve(dir, 'package.json')); + expect( + semver.valid(packageJSON.devDependencies['electron-nightly']), + ).not.toBeNull(); + expect(packageJSON.devDependencies.electron).not.toBeDefined(); + }); + + it('can define a specific Electron prerelease version with the beta tag', async () => { + await api.init({ + dir, + electronVersion: 'beta', + }); + const packageJSON = await import(path.resolve(dir, 'package.json')); + const prereleaseTag = semver.prerelease( + packageJSON.devDependencies.electron, + ); + expect(prereleaseTag).toEqual( + expect.arrayContaining([expect.stringMatching(/alpha|beta/)]), + ); + }); + + it('can define a specific Electron nightly version with the nightly tag', async () => { + await api.init({ + dir, + electronVersion: 'nightly', + }); + const packageJSON = await import(path.resolve(dir, 'package.json')); + expect( + semver.valid(packageJSON.devDependencies['electron-nightly']), + ).not.toBeNull(); + expect(packageJSON.devDependencies.electron).not.toBeDefined(); + }); + }); +}); + +describe.each([ + PACKAGE_MANAGERS['npm'], + PACKAGE_MANAGERS['yarn'], + PACKAGE_MANAGERS['pnpm'], +])(`init (with $executable)`, (pm) => { + beforeAll(async () => { + const originalCorepackStrict = process.env.COREPACK_ENABLE_STRICT; + if (pm.executable === 'pnpm') { + // temporarily disable corepack to enable pnpm to set links + process.env.COREPACK_ENABLE_STRICT = '0'; + + await spawnPackageManager( + pm, + 'config set node-linker hoisted'.split(' '), + ); + } + + return async () => { + // Unlink packages created during tests (syntax varies by package manager) + if (pm.executable === 'yarn') { + // yarn unlink --all removes all linked packages + await spawnPackageManager(pm, ['unlink', '--all']); + } else if (pm.executable === 'pnpm') { + // pnpm doesn't support --all, and requires Corepack bypass + await spawnPackageManager(pm, ['unlink']); + } + delete process.env.NODE_INSTALLER; + if (originalCorepackStrict === undefined) { + delete process.env.COREPACK_ENABLE_STRICT; + } else { + process.env.COREPACK_ENABLE_STRICT = originalCorepackStrict; + } + }; + }); + + const beforeInitTest = ( + params?: Partial, + beforeInit?: BeforeInitFunction, + ) => { + beforeAll(async () => { + dir = await ensureTestDirIsNonexistent(); + if (beforeInit) { + beforeInit(); + } + await api.init({ ...params, dir }); + }); + }; + + describe('init (with skipGit)', () => { + beforeAll(async () => { + dir = await ensureTestDirIsNonexistent(); + + return async () => { + await fs.promises.rm(dir, { recursive: true, force: true }); + }; + }); + + it('should not initialize a git repo if passed the skipGit option', async () => { + await api.init({ + dir, + skipGit: true, + }); + expect(fs.existsSync(path.join(dir, '.git'))).toEqual(false); + }); + }); + + describe('init', () => { + beforeInitTest(); + + afterAll(() => fs.promises.rm(dir, { recursive: true, force: true })); + + it('should fail in initializing an already initialized directory', async () => { + await expect(api.init({ dir })).rejects.toThrow( + `The specified path: "${dir}" is not empty. Please ensure it is empty before initializing a new project`, + ); + }); + + it('should initialize an already initialized directory when forced to', async () => { + await api.init({ + dir, + force: true, + }); + }); + + it('should create a new folder with a npm module inside', async () => { + expect( + fs.existsSync(dir), + 'the target dir should have been created', + ).toEqual(true); + expect(fs.existsSync(path.join(dir, 'package.json'))).toEqual(true); + expect(fs.existsSync(path.join(dir, '.git'))).toEqual(true); + expect( + fs.existsSync(path.resolve(dir, 'node_modules/electron')), + 'electron should exist', + ).toEqual(true); + expect( + fs.existsSync( + path.resolve(dir, 'node_modules/electron-squirrel-startup'), + ), + 'electron-squirrel-startup should exist', + ).toEqual(true); + expect( + fs.existsSync(path.resolve(dir, 'node_modules/@electron-forge/cli')), + '@electron-forge/cli should exist', + ).toEqual(true); + expect(fs.existsSync(path.join(dir, 'forge.config.js'))).toEqual(true); + }); + + describe('lint', () => { + it('should initially pass the linting process', () => + expectLintToPass(dir)); + }); + }); + + describe.skip('init with CI files enabled', () => { + beforeInitTest({ copyCIFiles: true }); + it.todo('should copy over the CI config files correctly'); + }); + + describe('init (with custom templater)', () => { + beforeInitTest({ + template: path.resolve(__dirname, '../fixture/custom_init'), + }); + + it('should add custom dependencies', async () => { + const packageJSON = await import(path.resolve(dir, 'package.json')); + expect(packageJSON.dependencies).toHaveProperty('debug'); + }); + + it('should add custom devDependencies', async () => { + const packageJSON = await import(path.resolve(dir, 'package.json')); + expect(packageJSON.devDependencies).toHaveProperty('lodash'); + }); + + it('should create dot files correctly', async () => { + expect( + fs.existsSync(dir), + 'the target dir should have been created', + ).toEqual(true); + expect(fs.existsSync(path.join(dir, '.bar'))).toEqual(true); + }); + + it('should create deep files correctly', async () => { + expect(fs.existsSync(path.join(dir, 'src/foo.js'))).toBe(true); + expect(fs.existsSync(path.join(dir, 'src/index.html'))).toBe(true); + }); + + describe('lint', () => { + it('should initially pass the linting process', () => + expectLintToPass(dir)); + }); + + afterAll(async () => { + await fs.promises.rm(dir, { recursive: true, force: true }); + execSync('npm unlink -g', { + cwd: path.resolve(__dirname, '../fixture/custom_init'), + }); + }); + }); + + describe('init (with a templater sans required Forge version)', () => { + beforeAll(async () => { + dir = await ensureTestDirIsNonexistent(); + + return async () => { + await fs.promises.rm(dir, { recursive: true, force: true }); + }; + }); + + it('should fail in initializing', async () => { + await expect( + api.init({ + dir, + template: path.resolve( + __dirname, + '../fixture/template-sans-forge-version', + ), + }), + ).rejects.toThrow(/it does not specify its required Forge version\.$/); + }); + }); + + describe('init (with a templater with a non-matching Forge version)', () => { + beforeAll(async () => { + dir = await ensureTestDirIsNonexistent(); + + return async () => { + await fs.promises.rm(dir, { recursive: true, force: true }); + }; + }); + + it('should fail in initializing', async () => { + await expect( + api.init({ + dir, + template: path.resolve( + __dirname, + '../fixture/template-nonmatching-forge-version', + ), + }), + ).rejects.toThrow( + /is not compatible with this version of Electron Forge/, + ); + }); + }); + + describe('init (with a nonexistent templater)', () => { + beforeAll(async () => { + dir = await ensureTestDirIsNonexistent(); + + return async () => { + await fs.promises.rm(dir, { recursive: true, force: true }); + }; + }); + + it('should fail in initializing', async () => { + await expect( + api.init({ + dir, + template: 'does-not-exist', + }), + ).rejects.toThrow('Failed to locate custom template'); + }); + }); + + describe('import', () => { + beforeEach(async () => { + dir = await ensureTestDirIsNonexistent(); + await fs.promises.mkdir(dir); + execSync( + `git clone https://github.com/electron/minimal-repro.git . --quiet`, + { + cwd: dir, + }, + ); + + return async () => { + await fs.promises.rm(dir, { recursive: true, force: true }); + }; + }); + + it('creates forge.config.js and can successfully package the application', async () => { + await updatePackageJSON(dir, async (packageJSON) => { + packageJSON.name = 'Name'; + packageJSON.productName = 'ProductName'; + }); + + await api.import({ dir }); + + expect(fs.existsSync(path.join(dir, 'forge.config.js'))).toEqual(true); + + await api.package({ dir }); + + const outDirContents = fs.readdirSync(path.join(dir, 'out')); + expect(outDirContents).toHaveLength(1); + expect(outDirContents[0]).toEqual( + `ProductName-${process.platform}-${process.arch}`, + ); + }); + }); + let dir: string; +}); diff --git a/packages/api/core/spec/slow/make.slow.spec.ts b/packages/api/core/spec/slow/make.slow.spec.ts new file mode 100644 index 0000000000..5b24f077ac --- /dev/null +++ b/packages/api/core/spec/slow/make.slow.spec.ts @@ -0,0 +1,224 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { createDefaultCertificate } from '@electron-forge/maker-appx'; +import { IForgeResolvableMaker } from '@electron-forge/shared-types'; +import { + ensureTestDirIsNonexistent, + updatePackageJSON, +} from '@electron-forge/test-utils'; +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; + +import { api } from '../../src/api/index'; + +describe('Make', () => { + const dir = path.resolve(__dirname, '..', 'fixture', 'api-tester'); + let outDir: string; + let makeDir: string; + let devCert: string; + + beforeAll(async () => { + outDir = await ensureTestDirIsNonexistent(); + makeDir = path.join(outDir, 'make'); + await api.package({ dir, outDir }); + + if (process.platform === 'win32') { + await fs.promises.copyFile( + path.join(__dirname, '..', 'fixture', 'bogus-private-key.pvk'), + path.join(outDir, 'default.pvk'), + ); + devCert = await createDefaultCertificate('CN=Test Author', { + certFilePath: outDir, + }); + } + + return async () => { + await fs.promises.rm(outDir, { recursive: true, force: true }); + }; + }); + + afterEach(async () => { + await fs.promises.rm(makeDir, { recursive: true, force: true }); + }); + + it('makes from a custom outDir without errors', async () => { + await api.make({ dir, skipPackage: true, outDir }); + + // out/make/zip/darwin/arm64/api-tester-darwin-arm64-1.0.0.zip + const artifactPath = path.join( + outDir, + 'make', + 'zip', + process.platform, + process.arch, + `api-tester-${process.platform}-${process.arch}-1.0.0.zip`, + ); + + expect(fs.existsSync(artifactPath)).toBe(true); + + // spot check that the zip archive is greater than 50MB + const { size } = await fs.promises.stat(artifactPath); + expect(size).toBeGreaterThan(50 * 1024 * 1024); + }); + + it('throws an error when given an unrecognized platform', async () => { + await expect(api.make({ dir, platform: 'dos' })).rejects.toThrow( + /invalid platform/, + ); + }); + + it("throws an error when the specified maker doesn't implement isSupportedOnCurrentPlatform()", async () => { + const makerPath = path.resolve(__dirname, '../fixture/maker-incompatible'); + await expect( + api.make({ + dir, + overrideTargets: [ + { + name: makerPath, + } as IForgeResolvableMaker, + ], + skipPackage: true, + }), + ).rejects.toThrow(/incompatible with this version/); + }); + + it('throws an error when no makers are configured for the given platform', async () => { + await expect( + api.make({ + dir, + overrideTargets: [ + { + name: path.resolve(__dirname, '../fixture/maker-wrong-platform'), + } as IForgeResolvableMaker, + ], + platform: 'linux', + skipPackage: true, + }), + ).rejects.toThrow( + 'Could not find any make targets configured for the "linux" platform.', + ); + }); + + it.runIf(process.platform === 'darwin')( + 'makes Mac App Store targets', + async () => { + await expect( + api.make({ + dir, + outDir, + overrideTargets: [ + require.resolve('@electron-forge/maker-zip'), + require.resolve('@electron-forge/maker-dmg'), + ], + platform: 'mas', + }), + ).resolves.toHaveLength(2); + }, + ); + + describe('with Makers', () => { + if (process.platform !== 'win32') { + process.env.DISABLE_SQUIRREL_TEST = 'true'; + } + + function getMakers(good: boolean) { + const allMakers = [ + '@electron-forge/maker-appx', + '@electron-forge/maker-deb', + '@electron-forge/maker-dmg', + '@electron-forge/maker-flatpak', + '@electron-forge/maker-msix', + '@electron-forge/maker-rpm', + '@electron-forge/maker-snap', + '@electron-forge/maker-squirrel', + '@electron-forge/maker-wix', + '@electron-forge/maker-zip', + ]; + return allMakers + .map((maker) => require.resolve(maker)) + .filter((makerPath) => { + const MakerClass = require(makerPath).default; + const maker = new MakerClass(); + return ( + maker.isSupportedOnCurrentPlatform() === good && + maker.externalBinariesExist() === good + ); + }) + .map((makerPath) => () => { + const makerDefinition = { + name: makerPath, + platforms: [process.platform], + config: { + devCert, + }, + }; + + if (process.platform === 'win32') { + ( + makerDefinition.config as Record + ).makeVersionWinStoreCompatible = true; + } + + return makerDefinition; + }); + } + + const goodMakers = getMakers(true); + const badMakers = getMakers(false); + + const testMakeTarget = function ( + target: () => { name: string }, + shouldPass: boolean, + ) { + describe(`${path.basename(target().name)}`, async () => { + beforeAll(async () => { + const original = await updatePackageJSON(dir, async (packageJSON) => { + return { + ...packageJSON, + config: { + forge: { + packagerConfig: { + executableName: 'api-tester', + }, + makers: [target() as IForgeResolvableMaker], + }, + }, + }; + }); + + return async () => { + await updatePackageJSON(dir, (_packageJSON) => { + return original; + }); + }; + }); + + if (shouldPass) { + it(`makes the correct artifact`, async () => { + const outputs = await api.make({ dir, outDir, skipPackage: true }); + for (const outputResult of outputs) { + for (const output of outputResult.artifacts) { + expect(fs.existsSync(output)).toEqual(true); + expect(output).toContain(path.resolve(outDir, 'make')); + } + } + }); + } else { + it(`fails`, async () => { + await expect( + api.make({ dir, outDir, skipPackage: true }), + ).rejects.toThrow(); + }); + } + }); + }; + + for (const maker of goodMakers) { + testMakeTarget(maker, true); + } + + for (const maker of badMakers) { + testMakeTarget(maker, false); + } + }); +}); diff --git a/packages/api/core/spec/slow/package.slow.spec.ts b/packages/api/core/spec/slow/package.slow.spec.ts new file mode 100644 index 0000000000..48b7bfbe8b --- /dev/null +++ b/packages/api/core/spec/slow/package.slow.spec.ts @@ -0,0 +1,105 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { + ensureTestDirIsNonexistent, + updatePackageJSON, +} from '@electron-forge/test-utils'; +import { readMetadata } from 'electron-installer-common'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; + +import { api } from '../../src/api/index'; +import { readRawPackageJson } from '../../src/util/read-package-json'; + +describe('Package', () => { + const dir = path.resolve(__dirname, '..', 'fixture', 'api-tester'); + let outDir: string; + beforeEach(async () => { + outDir = await ensureTestDirIsNonexistent(); + + return async () => { + await fs.promises.rm(outDir, { recursive: true, force: true }); + }; + }); + + it('can package an Electron app', async () => { + expect(fs.existsSync(outDir)).toEqual(false); + + await api.package({ dir, outDir }); + + // should respect outDir + expect(fs.existsSync(outDir)).toEqual(true); + + // should remove Forge config from packaged app's package.json + const cleanPackageJSON = await readMetadata({ + src: path.resolve( + outDir, + `api-tester-${process.platform}-${process.arch}`, + ), + logger: console.error, + }); + expect(cleanPackageJSON).not.toHaveProperty('config.forge'); + + // should leave the original Forge config intact + const normalPackageJSON = await readRawPackageJson(dir); + expect(normalPackageJSON).toHaveProperty('config.forge'); + }); + + describe('with packagerConfig.all', () => { + beforeAll(async () => { + const original = await updatePackageJSON(dir, async (packageJSON) => { + packageJSON.config = { + forge: { + packagerConfig: { + all: true, + }, + }, + }; + return packageJSON; + }); + + return async () => { + await updatePackageJSON(dir, async (_packageJSON) => { + return original; + }); + }; + }); + + it('throws an error when packagerConfig.all is set', async () => { + await expect(api.package({ dir })).rejects.toThrow( + /packagerConfig\.all is not supported by Electron Forge/, + ); + }); + }); + + describe('with prebuilt native module dependencies', () => { + beforeAll(async () => { + const original = await updatePackageJSON(dir, async (packageJSON) => { + // add ref-napi from root workspace + packageJSON.dependencies = { + ...packageJSON.dependencies, + 'ref-napi': 'file:../../../../../../node_modules/ref-napi', + }; + return packageJSON; + }); + + return async () => { + await updatePackageJSON(dir, async (_packageJSON) => { + return original; + }); + }; + }); + + beforeEach(async () => { + outDir = await ensureTestDirIsNonexistent(); + + return async () => { + await fs.promises.rm(outDir, { recursive: true, force: true }); + }; + }); + + it('can package without errors', async () => { + await api.package({ dir, outDir }); + }); + }); +}); diff --git a/packages/utils/core-utils/spec/electron-version.spec.ts b/packages/utils/core-utils/spec/electron-version.spec.ts index 0d3edc79e6..c164814bc2 100644 --- a/packages/utils/core-utils/spec/electron-version.spec.ts +++ b/packages/utils/core-utils/spec/electron-version.spec.ts @@ -2,7 +2,8 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { ensureTestDirIsNonexistent } from '@electron-forge/test-utils'; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { devDeps, @@ -58,21 +59,30 @@ describe('updateElectronDependency', () => { }); describe('getElectronVersion', () => { + let dir: string; + beforeEach(async () => { + dir = await ensureTestDirIsNonexistent(); + + return async () => { + await fs.rm(dir, { recursive: true, force: true }); + }; + }); it('fails without devDependencies', async () => { - await expect(getElectronVersion('', {})).rejects.toThrow( + await expect(getElectronVersion(dir, {})).rejects.toThrow( 'does not have any devDependencies', ); }); it('fails without electron devDependencies', async () => - expect(getElectronVersion('', { devDependencies: {} })).rejects.toThrow( + expect(getElectronVersion(dir, { devDependencies: {} })).rejects.toThrow( 'Electron packages in devDependencies', )); it('fails with a non-exact version and no electron installed', async () => { const fixtureDir = path.resolve(fixturePath, 'dummy_app'); + await fs.cp(fixtureDir, dir, { recursive: true }); await expect( - getElectronVersion(fixtureDir, { + getElectronVersion(dir, { devDependencies: { electron: '^4.0.2' }, }), ).rejects.toThrow('Cannot find the package'); @@ -80,6 +90,7 @@ describe('getElectronVersion', () => { it('works with a non-exact version with electron installed', async () => { const fixtureDir = path.resolve(fixturePath, 'non-exact'); + await fs.cp(fixtureDir, dir, { recursive: true }); await expect( getElectronVersion(fixtureDir, { devDependencies: { electron: '^4.0.2' }, @@ -91,7 +102,7 @@ describe('getElectronVersion', () => { const packageJSON = { devDependencies: { 'electron-nightly': '5.0.0-nightly.20190107' }, }; - await expect(getElectronVersion('', packageJSON)).resolves.toEqual( + await expect(getElectronVersion(dir, packageJSON)).resolves.toEqual( '5.0.0-nightly.20190107', ); }); @@ -100,7 +111,9 @@ describe('getElectronVersion', () => { const packageJSON = { devDependencies: { electron: '1.0.0' }, }; - await expect(getElectronVersion('', packageJSON)).resolves.toEqual('1.0.0'); + await expect(getElectronVersion(dir, packageJSON)).resolves.toEqual( + '1.0.0', + ); }); describe('with yarn workspaces', () => { @@ -115,6 +128,7 @@ describe('getElectronVersion', () => { 'packages', 'subpackage', ); + await fs.cp(fixtureDir, dir, { recursive: true }); const packageJSON = { devDependencies: { electron: '^4.0.4' }, }; diff --git a/packages/utils/test-utils/package.json b/packages/utils/test-utils/package.json index 72210258fd..99a1886b3c 100644 --- a/packages/utils/test-utils/package.json +++ b/packages/utils/test-utils/package.json @@ -8,7 +8,8 @@ "main": "dist/index.js", "typings": "dist/index.d.ts", "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0" + "@malept/cross-spawn-promise": "^2.0.0", + "fs-extra": "^10.0.0" }, "engines": { "node": ">= 16.4.0" diff --git a/packages/utils/test-utils/src/index.ts b/packages/utils/test-utils/src/index.ts index 85f894d0e0..66a627f2c5 100644 --- a/packages/utils/test-utils/src/index.ts +++ b/packages/utils/test-utils/src/index.ts @@ -1,8 +1,16 @@ -import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { ExitError, spawn } from '@malept/cross-spawn-promise'; +import fs from 'fs-extra'; + +export type PackageJSON = Record & { + config?: { + forge?: Record; + }; + dependencies?: Record; + devDependencies?: Record; +}; async function runNPM(dir: string, ...args: string[]) { await spawn('npm', args, { cwd: dir }); @@ -63,3 +71,26 @@ export async function mockRequire(mockedUri: string, stub: any) { return Module._load_original(uri, parent); }; } + +/** + * Mutates the `package.json` file in a directory. + * Use the return value to later restore the original `package.json` value + * in a subsequent call of this function. + * + * @param dir - The target directory containing the `package.json` file + * @param callback - A callback function that returns the value of the new `package.json` to be applied + * @returns The original `package.json` prior to mutation + */ +export async function updatePackageJSON( + dir: string, + callback: (packageJSON: PackageJSON) => Promise, +) { + const packageJSON = await fs.readJson(path.resolve(dir, 'package.json')); + const mutated = await callback(JSON.parse(JSON.stringify(packageJSON))); + await fs.promises.writeFile( + path.resolve(dir, 'package.json'), + JSON.stringify(mutated, null, 2), + 'utf-8', + ); + return packageJSON; +} diff --git a/vitest.config.mts b/vitest.config.mts index 9e1fa0116f..b18abb5811 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,8 +20,8 @@ export default defineConfig({ test: { include: ['**/spec/**/*.slow.spec.ts'], name: 'slow', - hookTimeout: 160000, - testTimeout: 160000, + hookTimeout: 240000, + testTimeout: 240000, }, }, ], diff --git a/yarn.lock b/yarn.lock index a272ffa77a..b9215b7c5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1362,6 +1362,7 @@ __metadata: resolution: "@electron-forge/test-utils@workspace:packages/utils/test-utils" dependencies: "@malept/cross-spawn-promise": "npm:^2.0.0" + fs-extra: "npm:^10.0.0" languageName: unknown linkType: soft @@ -1411,6 +1412,25 @@ __metadata: languageName: node linkType: hard +"@electron/get@npm:^2.0.0": + version: 2.0.3 + resolution: "@electron/get@npm:2.0.3" + dependencies: + debug: "npm:^4.1.1" + env-paths: "npm:^2.2.0" + fs-extra: "npm:^8.1.0" + global-agent: "npm:^3.0.0" + got: "npm:^11.8.5" + progress: "npm:^2.0.3" + semver: "npm:^6.2.0" + sumchecker: "npm:^3.0.1" + dependenciesMeta: + global-agent: + optional: true + checksum: 10c0/148957d531bac50c29541515f2483c3e5c9c6ba9f0269a5d536540d2b8d849188a89588f18901f3a84c2b4fd376d1e0c5ea2159eb2d17bda68558f57df19015e + languageName: node + linkType: hard + "@electron/get@npm:^3.0.0": version: 3.0.0 resolution: "@electron/get@npm:3.0.0" @@ -5097,6 +5117,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^22.7.7": + version: 22.19.1 + resolution: "@types/node@npm:22.19.1" + dependencies: + undici-types: "npm:~6.21.0" + checksum: 10c0/6edd93aea86da740cb7872626839cd6f4a67a049d3a3a6639cb592c620ec591408a30989ab7410008d1a0b2d4985ce50f1e488e79c033e4476d3bec6833b0a2f + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -8110,6 +8139,7 @@ __metadata: cross-spawn: "npm:^7.0.3" cross-zip: "npm:^4.0.0" debug: "npm:^4.3.1" + electron: "npm:^39.2.6" electron-installer-common: "npm:^0.10.2" electron-installer-debian: "npm:^3.2.0" electron-installer-dmg: "npm:^5.0.1" @@ -8151,6 +8181,7 @@ __metadata: parse-author: "npm:^2.0.0" prettier: "npm:^3.6.2" rechoir: "npm:^0.8.0" + ref-napi: "npm:^3.0.3" semver: "npm:^7.2.1" source-map-support: "npm:^0.5.13" tsx: "npm:^4.21.0" @@ -8175,6 +8206,8 @@ __metadata: built: true "@malept/electron-installer-flatpak": optional: true + electron: + built: true electron-installer-debian: optional: true electron-installer-dmg: @@ -8369,6 +8402,19 @@ __metadata: languageName: node linkType: hard +"electron@npm:^39.2.6": + version: 39.2.6 + resolution: "electron@npm:39.2.6" + dependencies: + "@electron/get": "npm:^2.0.0" + "@types/node": "npm:^22.7.7" + extract-zip: "npm:^2.0.1" + bin: + electron: cli.js + checksum: 10c0/d3a0f219eb4077e1a76aa6215202d3a22c1026497d79226e244fa3299a17ba0e62b97c2ddf40dfb9225cc089f5beb681dde9e5db3da3ab8f5686334e4b6888f4 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -9717,7 +9763,7 @@ __metadata: languageName: node linkType: hard -"extract-zip@npm:^2.0.0": +"extract-zip@npm:^2.0.0, extract-zip@npm:^2.0.1": version: 2.0.1 resolution: "extract-zip@npm:2.0.1" dependencies: @@ -10593,6 +10639,13 @@ __metadata: languageName: node linkType: hard +"get-symbol-from-current-process-h@npm:^1.0.2": + version: 1.0.2 + resolution: "get-symbol-from-current-process-h@npm:1.0.2" + checksum: 10c0/f33109e08aef7029b16f18032dce669e92efb21992946d7b575e494f32907c3d2c353685e00398b9202c332aab140e444e9333c93f2b81aa662bde3f59e3a25a + languageName: node + linkType: hard + "get-tsconfig@npm:^4.7.5": version: 4.13.0 resolution: "get-tsconfig@npm:4.13.0" @@ -14149,6 +14202,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^3.0.0": + version: 3.2.1 + resolution: "node-addon-api@npm:3.2.1" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/41f21c9d12318875a2c429befd06070ce367065a3ef02952cfd4ea17ef69fa14012732f510b82b226e99c254da8d671847ea018cad785f839a5366e02dd56302 + languageName: node + linkType: hard + "node-api-version@npm:^0.2.0": version: 0.2.0 resolution: "node-api-version@npm:0.2.0" @@ -14193,6 +14255,17 @@ __metadata: languageName: node linkType: hard +"node-gyp-build@npm:^4.2.1": + version: 4.8.4 + resolution: "node-gyp-build@npm:4.8.4" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 10c0/444e189907ece2081fe60e75368784f7782cfddb554b60123743dfb89509df89f1f29c03bbfa16b3a3e0be3f48799a4783f487da6203245fa5bed239ba7407e1 + languageName: node + linkType: hard + "node-gyp@npm:^11.0.0, node-gyp@npm:latest": version: 11.4.2 resolution: "node-gyp@npm:11.4.2" @@ -15905,6 +15978,19 @@ __metadata: languageName: node linkType: hard +"ref-napi@npm:^3.0.3": + version: 3.0.3 + resolution: "ref-napi@npm:3.0.3" + dependencies: + debug: "npm:^4.1.1" + get-symbol-from-current-process-h: "npm:^1.0.2" + node-addon-api: "npm:^3.0.0" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.2.1" + checksum: 10c0/03768cfe6134061c4afe8c5da6cc03ced9763ad1b8401895df2c5f6b10211dfd2bbcf31cca256e2abe9f2d4ddf08333590a6589b2ec5e68efacf70de2c0e2d7b + languageName: node + linkType: hard + "reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9": version: 1.0.9 resolution: "reflect.getprototypeof@npm:1.0.9"