Skip to content

Commit 3941818

Browse files
jjviscomiahnpnl
authored andcommitted
fix: preserve Bundler on the CJS path under TypeScript >= 6
Follow-up to #5280, which closed #4198 but deliberately scoped out this scenario to keep its change tied to a single release line. The present change picks it up now that the TS 6 relaxation can be honored via a runtime version check. TypeScript 6 relaxed TS5095 to allow `module: CommonJS` paired with `moduleResolution: bundler`, but `isBundlerCompatibleModuleKind` still treated CommonJS as Bundler-incompatible across the entire peerDependency range, so user-set Bundler was silently substituted to Node10 on the CJS path even on TS 6+. Detect the TypeScript major version at runtime via `this._ts.version` and treat CommonJS as Bundler-compatible when the version is >= 6. Other non-ES module kinds (AMD / UMD / System / None) remain Bundler-incompatible on every TypeScript version, matching TypeScript's own rules. Side effect: on TS >= 6, user-supplied Node16/NodeNext on the CJS path now substitutes to Bundler (mirroring the existing ESM-path substitution) rather than to Node10. Bundler is the closer semantic match for users who explicitly chose Node-aware resolution, and the pair is valid under TS 6. TSDoc on `resolveCompatibleModuleResolution` and `isBundlerCompatibleModuleKind` updated to reflect the new behavior. The inline TS < 6 test block keeps its existing assertions (still correct on the dev TypeScript 5.9.3) and gains a sibling TS >= 6 block that simulates the TS 6 runtime via a `version`-proxied ts module. Validated end-to-end against real TypeScript 6.0.3 via a smoke test running through the built ts-jest dist. Refs #4198
1 parent efb3c2f commit 3941818

2 files changed

Lines changed: 72 additions & 16 deletions

File tree

‎src/legacy/compiler/ts-compiler.spec.ts‎

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -545,19 +545,19 @@ describe('TsCompiler', () => {
545545

546546
// Closes #4198. Each row is a `moduleResolution` value the user explicitly sets in
547547
// their tsconfig. The CJS path forces `module: CommonJS`, which TypeScript binds
548-
// tightly to a small set of compatible resolutions:
548+
// tightly to a small set of compatible resolutions on TS < 6:
549549
// - Node10 / Classic: pass through (always valid with CommonJS)
550550
// - Node16 / NodeNext: substitute → Node10 (TS5110 forbids them with CommonJS)
551-
// - Bundler: substitute → Node10 (TS5095 forbids CommonJS+Bundler on TS ≤ 5;
552-
// a TS6+ enhancement can pass Bundler through in a follow-up)
551+
// - Bundler: substitute → Node10 (TS5095 forbids CommonJS+Bundler on TS < 6)
552+
// The TS >= 6 path is covered separately below.
553553
test.each([
554554
{ moduleResolutionValue: 'Bundler', expectedKind: ts.ModuleResolutionKind.Node10 },
555555
{ moduleResolutionValue: 'Node16', expectedKind: ts.ModuleResolutionKind.Node10 },
556556
{ moduleResolutionValue: 'NodeNext', expectedKind: ts.ModuleResolutionKind.Node10 },
557557
{ moduleResolutionValue: 'Classic', expectedKind: ts.ModuleResolutionKind.Classic },
558558
{ moduleResolutionValue: 'Node10', expectedKind: ts.ModuleResolutionKind.Node10 },
559559
])(
560-
'should resolve user-supplied moduleResolution %p compatibly for non-ESM compilation',
560+
'should resolve user-supplied moduleResolution %p compatibly for non-ESM compilation on TypeScript < 6',
561561
({ moduleResolutionValue, expectedKind }) => {
562562
const configSet = createConfigSet({
563563
tsJestConfig: {
@@ -592,6 +592,56 @@ describe('TsCompiler', () => {
592592
},
593593
)
594594

595+
// TS 6 relaxed TS5095 to allow `module: CommonJS` paired with
596+
// `moduleResolution: Bundler`. The CJS path now honors that: user-supplied
597+
// Bundler is preserved instead of substituted to Node10, and Node16/NodeNext
598+
// substitute to Bundler (matching the ESM-path substitution above) rather
599+
// than Node10. The TS version is simulated by handing the compiler a ts-like
600+
// proxy whose `version` reports 6.x; the real `ts` module is untouched.
601+
test.each([
602+
{ moduleResolutionValue: 'Bundler', expectedKind: ts.ModuleResolutionKind.Bundler },
603+
{ moduleResolutionValue: 'Node16', expectedKind: ts.ModuleResolutionKind.Bundler },
604+
{ moduleResolutionValue: 'NodeNext', expectedKind: ts.ModuleResolutionKind.Bundler },
605+
{ moduleResolutionValue: 'Classic', expectedKind: ts.ModuleResolutionKind.Classic },
606+
{ moduleResolutionValue: 'Node10', expectedKind: ts.ModuleResolutionKind.Node10 },
607+
])(
608+
'should resolve user-supplied moduleResolution %p compatibly for non-ESM compilation on TypeScript >= 6',
609+
({ moduleResolutionValue, expectedKind }) => {
610+
const configSet = createConfigSet({
611+
tsJestConfig: {
612+
...baseTsJestConfig,
613+
tsconfig: {
614+
module: 'CommonJS',
615+
moduleResolution: moduleResolutionValue as TsConfigJson.CompilerOptions['moduleResolution'],
616+
},
617+
},
618+
})
619+
const emptyFile = join(mockFolder, 'empty.ts')
620+
configSet.parsedTsConfig.fileNames.push(emptyFile)
621+
const compiler = new TsCompiler(configSet, new Map())
622+
// @ts-expect-error testing purpose: replace the ts reference on this compiler
623+
compiler._ts = { ...ts, version: '6.0.0' } as unknown as typeof ts
624+
// @ts-expect-error testing purpose
625+
compiler._languageService.getEmitOutput = jest.fn().mockReturnValueOnce({
626+
outputFiles: [{ text: sourceMap }, { text: jsOutput }],
627+
emitSkipped: false,
628+
} as ts.EmitOutput)
629+
// @ts-expect-error testing purpose
630+
compiler.getDiagnostics = jest.fn().mockReturnValue([])
631+
632+
compiler.getCompiledOutput(fileContent, fileName, {
633+
depGraphs: new Map(),
634+
supportsStaticESM: false,
635+
watchMode: false,
636+
})
637+
638+
// @ts-expect-error testing purpose
639+
const usedCompilerOptions = compiler._compilerOptions
640+
641+
expect(usedCompilerOptions.moduleResolution).toBe(expectedKind)
642+
},
643+
)
644+
595645
// Closes #4198 (ESM path). The ESM path forces `module: ESNext` whenever the user
596646
// had a modern Node module kind. ESNext pairs cleanly with Bundler/Node10/Classic
597647
// but not with Node16/NodeNext (TS5110), so those two are substituted to Bundler

‎src/legacy/compiler/ts-compiler.ts‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,11 @@ export class TsCompiler implements TsCompilerInstance {
239239
* supports.)
240240
*
241241
* - User-supplied Bundler with a non-Bundler-compatible forced module is
242-
* also TS5095 on the supported TS range — substitute Node10. (TypeScript
243-
* 6 relaxed this for `module: CommonJS` specifically; that relaxation is
244-
* deliberately not encoded here so behavior stays consistent across the
245-
* full peerDependency range. See `isBundlerCompatibleModuleKind` for the
246-
* follow-up pointer.)
242+
* TS5095, substitute Node10. (TypeScript 6 relaxed this for
243+
* `module: CommonJS` specifically; that relaxation is encoded inside
244+
* `isBundlerCompatibleModuleKind` via a runtime version check, so on
245+
* TS ≥ 6 user-supplied Bundler passes through unchanged on the CJS
246+
* path.)
247247
*
248248
* - Anything else (Node10 / Classic / unset) passes through or falls back
249249
* to Node10. These pairings are valid with every forced module kind.
@@ -291,13 +291,12 @@ export class TsCompiler implements TsCompilerInstance {
291291
* user-supplied-`Bundler` pass-through, so neither path emits an invalid
292292
* pair when the user has selected a non-ES `module`.
293293
*
294-
* Note: TypeScript 6.0 relaxed this restriction for `module: CommonJS`
295-
* specifically (`CommonJS` + `Bundler` is now a valid pair on TS ≥ 6); the
296-
* other non-ES module kinds (`AMD` / `UMD` / `System` / `None`) remain
297-
* Bundler-incompatible on every TypeScript version. The TS 6 relaxation is
298-
* intentionally not encoded here to keep behavior consistent across the full
299-
* peerDependency range (`>=4.3 <7`); honoring it can ride in as a follow-up
300-
* once the TS 6 baseline lands.
294+
* TypeScript 6.0 relaxed TS5095 for `module: CommonJS` specifically
295+
* (`CommonJS` + `Bundler` is a valid pair on TS ≥ 6); the other non-ES
296+
* module kinds (`AMD` / `UMD` / `System` / `None`) remain Bundler-incompatible
297+
* on every TypeScript version. The version is detected at runtime from
298+
* `this._ts.version` so the function stays correct across the full
299+
* peerDependency range (`>=4.3 <7`).
301300
*
302301
* @see https://www.typescriptlang.org/tsconfig/#moduleResolution
303302
*/
@@ -313,6 +312,13 @@ export class TsCompiler implements TsCompilerInstance {
313312
return true
314313
}
315314

315+
// TS 6 made `CommonJS` + `Bundler` a valid pair.
316+
if (moduleKind === M.CommonJS) {
317+
const tsMajor = parseInt(this._ts.version.split('.')[0], 10)
318+
319+
return tsMajor >= 6
320+
}
321+
316322
return false
317323
}
318324

0 commit comments

Comments
 (0)