Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ yarn-error.log
packages/**/.npmignore
packages/**/tsconfig.json
!packages/template/*/tmpl/tsconfig.json
!packages/*/*/spec/fixture/*/tsconfig.json
!packages/api/core/spec/fixture/esm_dep_ts_conf/node_modules
!packages/api/core/spec/fixture/ts7_dep_conf/node_modules
!packages/api/core/spec/fixture/ts7_with_ts6_dep_conf/node_modules
packages/**/tslint.json
packages/**/yarn.lock
packages/*/*/index.ts
Expand Down
1 change: 1 addition & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"ignore": ["spec/fixture/**"],
"ignoreDependencies": [
"@electron-forge/template-fixture",
"@typescript/typescript6",
"electron-forge-template-fixture-two",
"@electron-forge/maker-.*"
]
Expand Down
13 changes: 11 additions & 2 deletions packages/api/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,17 @@
"@electron-forge/test-utils": "workspace:*",
"electron-forge-template-fixture-two": "portal:./spec/fixture/electron-forge-template-fixture",
"electron-installer-common": "^0.10.2",
"vitest": "catalog:"
"typescript": "^6.0.2",
"vitest": "catalog:",
"webpack": "^5.69.1"
},
"peerDependencies": {
"typescript": ">=4.7.0 <7"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"dependencies": {
"@electron-forge/core-utils": "workspace:*",
Expand All @@ -35,7 +45,6 @@
"@electron/packager": "^20.0.1",
"debug": "^4.3.1",
"graceful-fs": "^4.2.11",
"jiti": "^2.4.2",
"listr2": "^7.0.2"
},
"engines": {
Expand Down
150 changes: 150 additions & 0 deletions packages/api/core/spec/fast/util/forge-config.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

import { ResolvedForgeConfig } from '@electron-forge/shared-types';
import { describe, expect, it, vi } from 'vitest';
import { Compilation } from 'webpack';

import findConfig, {
forgeConfigIsValidFilePath,
Expand Down Expand Up @@ -347,6 +350,153 @@ describe('findConfig', () => {
const conf = await findConfig(fixturePath);
expect(conf.buildIdentifier).toEqual('async-typescript-esm');
});

it('should support top-level await and extensionless relative imports', async () => {
const fixturePath = path.resolve(
import.meta.dirname,
'../../fixture/tla_ts_conf',
);
const conf = await findConfig(fixturePath);
expect(conf.buildIdentifier).toEqual('tla-relative-import');
});

// Regression test for https://github.com/electron/forge/issues/3949 —
// CJS packages imported by a TypeScript config must be the same instances
// that the rest of the process (e.g. the webpack plugin) sees.
it('should share dependency instances with the loading process', async () => {
type WebpackDepConfig = ResolvedForgeConfig & {
stage: number;
compilationClass: unknown;
};
const fixturePath = path.resolve(
import.meta.dirname,
'../../fixture/webpack_dep_ts_conf',
);
const conf = (await findConfig(fixturePath)) as WebpackDepConfig;
expect(conf.stage).toBeDefined();
expect(conf.stage).toEqual(
Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER,
);
expect(conf.compilationClass).toBe(Compilation);
});

it('should load ESM-only dependencies that use top-level await', async () => {
type EsmDepConfig = ResolvedForgeConfig & { answer: number };
const fixturePath = path.resolve(
import.meta.dirname,
'../../fixture/esm_dep_ts_conf',
);
const conf = (await findConfig(fixturePath)) as EsmDepConfig;
expect(conf.buildIdentifier).toEqual('esm-tla-dep');
expect(conf.answer).toEqual(42);
});

it('should respect "paths" aliases from the project tsconfig', async () => {
const fixturePath = path.resolve(
import.meta.dirname,
'../../fixture/paths_ts_conf',
);
const conf = await findConfig(fixturePath);
expect(conf.buildIdentifier).toEqual('tsconfig-paths-alias');
});

it('should throw a helpful error when typescript is not installed', async () => {
const spy = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined);
const fixturePath = path.resolve(
import.meta.dirname,
'../../fixture/missing_ts_dep_conf',
);
// Copy the fixture outside the repo so the monorepo's own `typescript`
// install is not resolvable from the project directory.
const tmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'forge-missing-ts-'),
);
try {
await fs.cp(fixturePath, tmpDir, { recursive: true });
await expect(findConfig(tmpDir)).rejects.toThrow(
/requires the "typescript" package \(version 4\.7\.0 or later\) to be installed/,
);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
spy.mockRestore();
}
});

it('should throw a helpful error when the project TypeScript is v7+', async () => {
const spy = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined);
const fixturePath = path.resolve(
import.meta.dirname,
'../../fixture/ts7_dep_conf',
);
try {
await expect(findConfig(fixturePath)).rejects.toThrow(
/TypeScript 7\+ \(the native compiler\) no longer provides the JavaScript compiler API/,
);
} finally {
spy.mockRestore();
}
});

it('should fall back to a side-by-side @typescript/typescript6 when the project TypeScript is v7+', async () => {
const fixturePath = path.resolve(
import.meta.dirname,
'../../fixture/ts7_with_ts6_dep_conf',
);
const conf = await findConfig(fixturePath);
expect(conf.buildIdentifier).toEqual('ts7-with-ts6-fallback');
});

it('should surface type errors when the config fails to load', async () => {
const spy = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined);
const fixturePath = path.resolve(
import.meta.dirname,
'../../fixture/typed_error_ts_conf',
);
try {
// The config crashes at runtime; the loader should type-check it and
// report the actual type error instead of the runtime stack trace.
await expect(findConfig(fixturePath)).rejects.toThrow(/TS2339/);
} finally {
spy.mockRestore();
}
});

describe('FORGE_TYPECHECK_CONFIG', () => {
it('should not type-check configs that load successfully by default', async () => {
type TypeErrorOnlyConfig = ResolvedForgeConfig & {
buildIdentifier: string;
};
const fixturePath = path.resolve(
import.meta.dirname,
'../../fixture/type_error_only_ts_conf',
);
const conf = (await findConfig(fixturePath)) as TypeErrorOnlyConfig;
expect(conf.buildIdentifier).toEqual('type-error-only');
});

it('should fail on type errors when FORGE_TYPECHECK_CONFIG is set', async () => {
const spy = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined);
const fixturePath = path.resolve(
import.meta.dirname,
'../../fixture/type_error_only_ts_conf',
);
process.env.FORGE_TYPECHECK_CONFIG = '1';
try {
await expect(findConfig(fixturePath)).rejects.toThrow(/TS2322/);
} finally {
delete process.env.FORGE_TYPECHECK_CONFIG;
spy.mockRestore();
}
});
});
});

describe('ESM and CJS module formats', () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/api/core/spec/fixture/esm_dep_ts_conf/forge.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// The TypeScript config loader must load ESM-only dependencies (here one
// with top-level await) through Node's native ESM loader.
import type { ForgeConfig } from '@electron-forge/shared-types';

// @ts-expect-error the fixture package ships no type definitions
import { answer } from 'esm-tla-dep';

const config: ForgeConfig & { answer: number } = {
buildIdentifier: 'esm-tla-dep',
answer,
};

export default config;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions packages/api/core/spec/fixture/esm_dep_ts_conf/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "",
"productName": "",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"start": "electron-forge start"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@electron-forge/shared-types": "*",
"electron": "99.99.99",
"esm-tla-dep": "1.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This fixture is copied to a temporary directory (where no `typescript`
// package is resolvable) to assert the loader's helpful error message. It
// deliberately has no imports so it stands alone outside the repo.
const config = {
buildIdentifier: 'missing-typescript',
};

export default config;
16 changes: 16 additions & 0 deletions packages/api/core/spec/fixture/missing_ts_dep_conf/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "",
"productName": "",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"start": "electron-forge start"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"electron": "99.99.99"
}
}
11 changes: 11 additions & 0 deletions packages/api/core/spec/fixture/paths_ts_conf/forge.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// The TypeScript config loader must respect `paths` aliases from the
// project's own tsconfig.json.
import type { ForgeConfig } from '@electron-forge/shared-types';

import { getBuildIdentifier } from '@fixture/identifier';

const config: ForgeConfig = {
buildIdentifier: getBuildIdentifier(),
};

export default config;
17 changes: 17 additions & 0 deletions packages/api/core/spec/fixture/paths_ts_conf/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "",
"productName": "",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"start": "electron-forge start"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@electron-forge/shared-types": "*",
"electron": "99.99.99"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getBuildIdentifier(): string {
return 'tsconfig-paths-alias';
}
13 changes: 13 additions & 0 deletions packages/api/core/spec/fixture/paths_ts_conf/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "preserve",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@fixture/*": ["src/*"]
}
}
}
14 changes: 14 additions & 0 deletions packages/api/core/spec/fixture/tla_ts_conf/forge.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Top-level await plus an extensionless relative TypeScript import, in a
// project without "type": "module" — the template-shaped config that the
// TypeScript config loader must keep supporting (see #3872 / #3676).
import type { ForgeConfig } from '@electron-forge/shared-types';

import { getBuildIdentifier } from './src/identifier';

const buildIdentifier = await getBuildIdentifier();

const config: ForgeConfig = {
buildIdentifier,
};

export default config;
17 changes: 17 additions & 0 deletions packages/api/core/spec/fixture/tla_ts_conf/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "",
"productName": "",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"start": "electron-forge start"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@electron-forge/shared-types": "*",
"electron": "99.99.99"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function getBuildIdentifier(): Promise<string> {
return 'tla-relative-import';
}
9 changes: 9 additions & 0 deletions packages/api/core/spec/fixture/ts7_dep_conf/forge.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This fixture's `node_modules/typescript` is a TypeScript 7-shaped stub
// (`"type": "module"`, no root `"."` export) used to assert the loader's
// actionable error. It deliberately has no imports — the loader must fail
// before ever transpiling this file.
const config = {
buildIdentifier: 'typescript-seven',
};

export default config;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading