feat(core): replace jiti with a zero-dependency TypeScript config loader#4311
Draft
claude[bot] wants to merge 3 commits into
Draft
feat(core): replace jiti with a zero-dependency TypeScript config loader#4311claude[bot] wants to merge 3 commits into
claude[bot] wants to merge 3 commits into
Conversation
jiti evaluates a config's node_modules dependencies through its own parallel module system, so packages loaded by the config (e.g. webpack) are duplicate instances of the ones the rest of the process uses. For TypeScript configs using the webpack plugin this made Compilation.PROCESS_ASSETS_STAGE_* comparisons silently fail and dropped index.html from packaged apps (#3949). The new loader uses the user project's own typescript package instead: the config's project-local TS graph is transpiled to temp sibling files, import specifiers are rewritten so CJS dependencies go through the real require() (shared Module._cache) and ESM dependencies stay native imports, and the entry is evaluated with a native await import(). All temp files are deleted in a finally block and nothing process-global is registered. Because the user's own compiler is at hand, configs that fail to load are type-checked and the load error is replaced with proper TypeScript diagnostics; FORGE_TYPECHECK_CONFIG=1 opts in to type-checking on every load. The TypeScript templates' tsconfigs now include the node types explicitly so the stock configs type-check cleanly under TypeScript 6 (which no longer includes @types packages automatically). Removes the jiti dependency and adds no runtime dependency in its place. Fixes #3949
5 tasks
nikwen
reviewed
Jul 1, 2026
| let loaded: MaybeESM<ForgeConfig | AsyncForgeConfigGenerator>; | ||
| if (loadFn) { | ||
| loaded = await loadFn(forgeConfigPath); | ||
| if (['.cts', '.mts', '.ts'].includes(path.extname(forgeConfigPath))) { |
Member
There was a problem hiding this comment.
Can we share this array with the TS_EXTENSIONS extension list? Maybe expose an isTypeScriptFileExtension function from the loader or something like that?
…nst TypeScript 7
The config loader needs the project's typescript package, so declare it
as an optional peerDependency of @electron-forge/core with the
empirically verified range ">=4.7.0 <7":
- Floor: the full loader behavior matrix (extensionless relative TS
imports, single-instance webpack Compilation identity, top-level
await, a .cts helper imported as ./helper.cjs, async-function configs)
passes on typescript 4.7.2/4.7.4/4.8.4 against a template-shaped
project; 4.4 fails (no .cjs -> .cts resolution mapping). 4.5/4.6 pass
incidentally, but 4.7 is the first release where .cts/.mts support is
documented.
- Cap: TypeScript 7 (the native compiler, already published under the
plain "typescript" name on the rc dist-tag) is "type": "module"
with no root "." export and drops the JavaScript compiler API this
loader is built on, so require('typescript') would die with
ERR_PACKAGE_PATH_NOT_EXPORTED before any feature detection.
The loader now reads the version from typescript/package.json (still
exported in 7.x) before requiring the module. On v7+ it transparently
falls back to a side-by-side @typescript/typescript6 install
(Microsoft's escape hatch for API-dependent tools; verified against the
real typescript@7.0.1-rc and @typescript/typescript6@6.0.1) and
otherwise throws an actionable error suggesting exactly that. The
missing-typescript error now also states the minimum supported version.
|
No dependency changes detected. Learn more about Socket for GitHub. 👍 No dependency changes detected in pull request |
The fixture manifests declared the real registry versions typescript@7.0.1-rc and @typescript/typescript6@6.0.1 as devDependencies, which dependency scanners pick up as a (never actually installed) dependency change. Drop those declarations and give the committed package stubs synthetic versions — the specs resolve them straight from the fixture's checked-in node_modules, so nothing is ever fetched from the registry.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Samuel Attard, Niklas Wenzel · Slack thread
Summarize your changes:
Fixes #3949. Also resolves the TODO from #4059: "Check if we still need jiti for the configuration loading" — we don't; this PR removes it.
Before
On Windows, TypeScript-config projects using the Webpack plugin silently drop
index.htmlfrom packaged apps: jiti evaluates the config'snode_modulesdependencies through its own parallel module system, so the config gets a second copy of webpack andCompilation.PROCESS_ASSETS_STAGE_*comparisons inside the plugin quietly fail (#3949). On top of that, type errors in a Forge config never surface — you get a runtime crash or silent misbehavior instead of a diagnostic.After
TypeScript configs load through the project's own TypeScript compiler and Node's real module system — one webpack,
index.htmlemitted — and type errors in the config produce real TypeScript diagnostics.How
The loader (
packages/api/core/src/util/load-ts-config.ts) resolvestypescriptfrom the user's project (every TS template ships it; a helpful error is thrown if it's missing), collects the config's project-local TS graph viats.preProcessFile+ts.resolveModuleName(tsconfigpathsrespected), transpiles each file withts.transpileModule(ES2022 ESM; CommonJS for.cts; inline source maps pointing back at the TS source), and rewrites the emitted imports via the TS AST: relative/pathsspecifiers point at temp sibling files, bare specifiers resolving to CJS deps become realcreateRequirebindings (sharedModule._cache— the #3949 fix), ESM deps stay native imports (top-level-await deps keep working), and dynamic imports of project TS files are hoisted so post-load hooks still work. The entry temp file is evaluated with a nativeawait import(); all temps are uniquely named per load and deleted infinally; nothing process-global is ever registered.Type checking: if the config fails to load, it is type-checked with the user's compiler and the raw crash is replaced with formatted TS diagnostics (zero happy-path cost). Setting
FORGE_TYPECHECK_CONFIG=1opts in to a fatal type check before every load. (Env-var switch chosen to match the existingFORGE_VITE_*precedent; deliberately notELECTRON_FORGE_*, since that prefix is magically mapped onto config properties by the config proxy.) Because TypeScript 6 no longer includes@typespackages automatically, the TypeScript templates' tsconfigs now set"types": ["node"](+@types/nodedevDep) so stock template configs type-check clean — verified against a template-shaped project under TS 6 (previouslyTS2591: Cannot find name 'require'inwebpack.plugins.ts).What #3907 required, verified
.ts/.cts/.mtsconfigs (incl. async function configs) loadforge-configspecs, unchangednode_modulesdeps (webpackCompilationidentity +PROCESS_ASSETS_STAGEvalue)webpack_dep_ts_conf— "should share dependency instances with the loading process"tla_ts_conf— "should support top-level await and extensionless relative imports"esm_dep_ts_conf— "should load ESM-only dependencies that use top-level await"pathsaliasespaths_ts_conf— "should respect "paths" aliases from the project tsconfig"typescriptisn't installedmissing_ts_dep_conf— "should throw a helpful error when typescript is not installed"typed_error_ts_conf— "should surface type errors when the config fails to load" (assertsTS2339)type_error_only_ts_conf—FORGE_TYPECHECK_CONFIGdescribe block (assertsTS2322)finallywith unique per-load names; validated in a standalone harness that diffsModule._*/globalThisand scans for leftoversAll 29 pre-existing
forge-configspecs pass unchanged; the full fast suite is green (416 passed / 1 skipped / 7 todo), as areyarn build(tsgo),yarn lint:js,yarn knip(default +--production) andyarn constraints.Dependencies
Removes
jiti; adds nothing at runtime — the loader uses the project’s owntypescript(the templates already ship it), now declared as an optional peerDependency of@electron-forge/core:"typescript": ">=4.7.0 <7"withpeerDependenciesMeta.typescript.optional: true(only needed when the project actually uses a TypeScript config).>=4.7.0), verified empirically against a template-shaped project (extensionless relative TS imports, webpackCompilationidentity +PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER === 3000, top-level await, a.ctshelper imported as./helper.cjs, async-function configs): the full matrix passes on 4.7.2, 4.7.4 and 4.8.4 (4.9.5/5.0.4/6.0.3 were already validated). 4.5/4.6 happen to pass too — the.cts/.mtsresolver machinery predates its official 4.7 release — but 4.7 is the first version where that support is documented; 4.4.4 fails (TS2307: no.cjs→.ctsmapping).<7): TypeScript 7 (the native compiler) is already published under the plaintypescriptname (rcdist-tag:7.0.1-rc) as"type": "module"with no root"."export, sorequire('typescript')throwsERR_PACKAGE_PATH_NOT_EXPORTEDbefore any feature detection could run — and its newunstable/*JSON-RPC API drops the classic compiler API this loader is built on (transpileModule,resolveModuleName,preProcessFile, …). TS 7.1’s planned “stable API” is a new class-based API, i.e. a future loader port, not a version bump. The loader therefore reads the version fromtypescript/package.json(still exported in 7.x) before requiring the module; on v7+ it transparently falls back to a side-by-side@typescript/typescript6install when present (Microsoft’s escape hatch for API-dependent tools — verified end-to-end against the realtypescript@7.0.1-rc+@typescript/typescript6@6.0.1, full matrix green) and otherwise fails with an actionable error suggestingnpm install --save-dev @typescript/typescript6(or pinningtypescript@6).New devDependencies in
@electron-forge/coreonly:typescript(type-only import) andwebpack(for the #3949 identity test).Tradeoffs
.forge-<hex>.mjs/.cjsfiles are written next to the config while it loads (deleted infinally, unique names per load, so no stale ESM-cache hits).export * from 'cjs-dep're-exports in a config are unsupported and fail with a loud, actionable error.index.htmlno longer listed in Webpack assets #3949 reporter is still pending.Generated by Claude Code