ts-jest 29.4.6 → 29.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,127 @@
1
+ # PR 3 — Compiler Factory + CompilerFactoryInterface
2
+
3
+ ## Overview
4
+
5
+ Introduces `CompilerFactoryInterface` and `CompilerFactory` — the single routing point that decides
6
+ which compiler implementation to instantiate based on resolved config.
7
+
8
+ **User-visible change**: NO
9
+ **Depends on**: PR 1 (`ResolvedConfigInterface`, `DiagnosticsMode`), PR 2 (`CompilerInterface`,
10
+ `LanguageServiceCompiler`, `FastTranspileCompiler`)
11
+ **Invariant**: zero changes to any existing file
12
+
13
+ ---
14
+
15
+ ## Files to Create
16
+
17
+ ```
18
+ src/experimental/
19
+ interfaces/
20
+ compiler-factory.interface.ts
21
+ compiler/
22
+ compiler-factory.ts
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Requirements
28
+
29
+ ### `src/experimental/interfaces/compiler-factory.interface.ts`
30
+
31
+ ```ts
32
+ interface CompilerFactoryInterface {
33
+ create(config: ResolvedConfigInterface, runtimeCacheFS: Map<string, string>): CompilerInterface
34
+ }
35
+ ```
36
+
37
+ Single method. `runtimeCacheFS` is a mutable map shared at runtime for in-memory file content
38
+ (used by `LanguageServiceCompiler`; ignored by `FastTranspileCompiler`).
39
+
40
+ ---
41
+
42
+ ### `src/experimental/compiler/compiler-factory.ts`
43
+
44
+ **Routing logic**:
45
+
46
+ ```
47
+ config.diagnosticsMode === 'disabled'
48
+ → FastTranspileCompiler(config)
49
+
50
+ config.diagnosticsMode !== 'disabled'
51
+ → LanguageServiceCompiler(config, runtimeCacheFS)
52
+ ```
53
+
54
+ `diagnosticsMode` is derived in PR 1 (`diagnostics-config.ts`) from `noCheck` in the resolved
55
+ tsconfig. The factory does not inspect `noCheck` directly — it reads the already-derived mode from
56
+ `config.diagnosticsMode`.
57
+
58
+ **SWC stub**:
59
+
60
+ ```ts
61
+ case 'swc':
62
+ throw new NotImplementedError(
63
+ 'SWC compiler support is not yet implemented. ' +
64
+ 'Track progress at https://github.com/kulshekhar/ts-jest/issues/XXXX'
65
+ )
66
+ ```
67
+
68
+ **esbuild stub**:
69
+
70
+ ```ts
71
+ case 'esbuild':
72
+ throw new NotImplementedError(
73
+ 'esbuild compiler support is not yet implemented. ' +
74
+ 'Track progress at https://github.com/kulshekhar/ts-jest/issues/XXXX'
75
+ )
76
+ ```
77
+
78
+ SWC/esbuild stubs are behind a `compilerBackend` option (reserved for future use, not yet in
79
+ `TsJestTransformerOptions`). Currently unreachable in normal flow.
80
+
81
+ **`NotImplementedError`**:
82
+
83
+ ```ts
84
+ class NotImplementedError extends Error {
85
+ constructor(message: string) {
86
+ super(message)
87
+ this.name = 'NotImplementedError'
88
+ }
89
+ }
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Import Rules
95
+
96
+ - `src/experimental/interfaces/*` — allowed
97
+ - `src/experimental/compiler/language-service-compiler.ts` — allowed
98
+ - `src/experimental/compiler/fast-transpile-compiler.ts` — allowed
99
+ - `src/utils/*` — allowed
100
+ - `src/legacy/**` — FORBIDDEN
101
+ - All other `src/` folders — FORBIDDEN
102
+
103
+ ---
104
+
105
+ ## Test Requirements
106
+
107
+ Unit tests under `src/experimental/compiler/__tests__/`.
108
+
109
+ | Test file | Key scenarios |
110
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
111
+ | `compiler-factory.spec.ts` | `diagnosticsMode: 'disabled'` → returns `FastTranspileCompiler`; `diagnosticsMode: 'full'` → returns `LanguageServiceCompiler`; `diagnosticsMode: 'syntactic'` → returns `LanguageServiceCompiler`; SWC stub throws `NotImplementedError`; esbuild stub throws `NotImplementedError` |
112
+
113
+ **Existing tests must remain green** (no changes to legacy):
114
+
115
+ - `src/legacy/compiler/ts-jest-compiler.spec.ts`
116
+ - Legacy `TsJestCompiler` still uses `TsCompiler` directly — untouched
117
+
118
+ ---
119
+
120
+ ## Acceptance Criteria
121
+
122
+ - [ ] All new unit tests pass
123
+ - [ ] `ts-jest-compiler.spec.ts` unmodified and green
124
+ - [ ] `tsc --noEmit` passes
125
+ - [ ] Factory correctly routes based on `diagnosticsMode`
126
+ - [ ] SWC and esbuild stubs throw `NotImplementedError` with issue tracker link
127
+ - [ ] Every exported symbol has TSDoc
@@ -0,0 +1,229 @@
1
+ # PR 4 — Transformer Concern Modules
2
+
3
+ ## Overview
4
+
5
+ Extracts the five discrete concerns from `TsJestTransformer` into focused, independently testable
6
+ modules. This PR produces no user-facing changes — it is groundwork for the new transformer in PR 5.
7
+
8
+ **User-visible change**: NO
9
+ **Depends on**: PR 1 (`ResolvedConfigInterface`), PR 2 (`CompilerInterface`), PR 3 (`CompilerFactoryInterface`)
10
+ **Invariant**: zero changes to any existing file
11
+
12
+ ---
13
+
14
+ ## Files to Create
15
+
16
+ ```
17
+ src/experimental/transformer/
18
+ cache-store.ts
19
+ file-processor.ts
20
+ cache-key-builder.ts
21
+ hooks-runner.ts
22
+ babel-pipeline.ts
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Requirements
28
+
29
+ ### `src/experimental/transformer/cache-store.ts`
30
+
31
+ Replaces the static `_cachedConfigSets: CachedConfigSet[]` array in `TsJestTransformer`.
32
+
33
+ **Purpose**: deduplicates `ResolvedConfig` instances across `getCacheKey` and `process` calls
34
+ (Jest sometimes splits these calls across separate invocations with the same config but different
35
+ object references).
36
+
37
+ **2-pass deduplication**:
38
+
39
+ ```
40
+ Pass 1 — object identity:
41
+ Compare jestConfig reference (===)
42
+ → Hit: return cached ResolvedConfig
43
+
44
+ Pass 2 — serialised string:
45
+ Serialise jestConfig (excluding cacheDirectory) + cacheSuffix
46
+ → Hit: update stored .value reference, return cached ResolvedConfig
47
+
48
+ Miss:
49
+ Merge transformerOptions into config.globals['ts-jest']
50
+ Create new ResolvedConfig
51
+ Compute _transformCfgStr = serialize(jest-without-cacheDirectory) + cacheSuffix
52
+ Detect watch = process.argv.includes('--watch')
53
+ Store and return
54
+ ```
55
+
56
+ ```ts
57
+ class CacheStore {
58
+ private _entries: CacheEntry[]
59
+
60
+ getOrCreate(
61
+ jestConfig: Config.ProjectConfig,
62
+ transformerOptions: TsJestTransformerOptions,
63
+ factory: (config: Config.ProjectConfig, options: TsJestTransformerOptions) => ResolvedConfigInterface,
64
+ ): ResolvedConfigInterface
65
+ }
66
+ ```
67
+
68
+ Thread-safety: not required (Node.js single-threaded; Jest workers are separate processes).
69
+
70
+ ---
71
+
72
+ ### `src/experimental/transformer/file-processor.ts`
73
+
74
+ Implements the 5-branch file dispatch logic extracted from `processWithTs()`.
75
+
76
+ ```ts
77
+ function processFile(
78
+ fileContent: string,
79
+ filePath: string,
80
+ options: ProcessFileOptions,
81
+ config: ResolvedConfigInterface,
82
+ compiler: CompilerInterface,
83
+ ): CompiledOutput
84
+ ```
85
+
86
+ **5 branches in exact order** (first matching branch wins):
87
+
88
+ 1. `config.shouldStringifyContent(filePath)`
89
+ → `{ code: \`module.exports=\${JSON.stringify(fileContent)}\` }`
90
+
91
+ 2. `filePath.endsWith('.d.ts')`
92
+ → `{ code: '' }`
93
+
94
+ 3. `isNodeModule(filePath) && (isJs(filePath) || isJsx(filePath))`
95
+ → fast `ts.transpileModule` with `{ module: useESM && supportsStaticESM ? ESNext : CommonJS }`
96
+
97
+ - `isNodeModule`: `filePath.includes(path.sep + 'node_modules' + path.sep)`
98
+
99
+ 4. `isJs(filePath) || isJsx(filePath) || isTs(filePath) || isTsx(filePath)` (not node_modules)
100
+ → `compiler.getCompiledOutput(fileContent, filePath, compileOptions)`
101
+
102
+ 5. Fallback
103
+ → warn "unknown extension, returning as-is" + return `{ code: fileContent }`
104
+
105
+ ---
106
+
107
+ ### `src/experimental/transformer/cache-key-builder.ts`
108
+
109
+ Pure function — `fs.statSync` injected for testability.
110
+
111
+ ```ts
112
+ function buildCacheKey(
113
+ fileContent: string,
114
+ filePath: string,
115
+ options: CacheKeyOptions,
116
+ config: ResolvedConfigInterface,
117
+ compiler: CompilerInterface,
118
+ statSync: (path: string) => { mtimeMs: number },
119
+ ): string
120
+ ```
121
+
122
+ **Elements hashed in exact order**:
123
+
124
+ 1. `_transformCfgStr` (from `CacheStore` entry)
125
+ 2. `config.rootDir`
126
+ 3. `instrument ? 'instrument:on' : 'instrument:off'`
127
+ 4. `supportsStaticESM ? 'esm:on' : 'esm:off'`
128
+ 5. `fileContent`
129
+ 6. `filePath`
130
+ 7. If `!config.isolatedModules && config.tsCacheDir`:
131
+ - For each module in `compiler.getResolvedModules(fileContent, filePath, runtimeCacheFS)`:
132
+ - `moduleName`
133
+ - `statSync(moduleName).mtimeMs.toString()`
134
+
135
+ Returns `sha1(elements.join(''))`.
136
+
137
+ ---
138
+
139
+ ### `src/experimental/transformer/hooks-runner.ts`
140
+
141
+ Encapsulates `TS_JEST_HOOKS` reading and invocation.
142
+
143
+ ```ts
144
+ function runAfterProcessHook(
145
+ result: TransformResult,
146
+ fileContent: string,
147
+ filePath: string,
148
+ jestOptions: TransformOptions,
149
+ ): TransformResult
150
+ ```
151
+
152
+ **Behaviour**:
153
+
154
+ - Read `process.env.TS_JEST_HOOKS` **at call time** (not at module load time)
155
+ - If not set → return `result` as-is
156
+ - Resolve hook file path relative to `process.cwd()`
157
+ - `require()` the hooks module
158
+ - Call `hooksModule.afterProcess(result, fileContent, filePath, jestOptions)`
159
+ - If return value is truthy → return it
160
+ - Else → return original `result`
161
+
162
+ This matches the legacy behavior in `TsJestTransformer.runTsJestHook()` exactly.
163
+
164
+ ---
165
+
166
+ ### `src/experimental/transformer/babel-pipeline.ts`
167
+
168
+ Shared sync + async Babel transformation logic.
169
+
170
+ ```ts
171
+ function transformWithBabel(
172
+ result: CompiledOutput,
173
+ filePath: string,
174
+ options: TransformOptions,
175
+ babelJestTransformer: BabelTransformer,
176
+ ): TransformResult
177
+
178
+ async function transformWithBabelAsync(
179
+ result: CompiledOutput,
180
+ filePath: string,
181
+ options: TransformOptions,
182
+ babelJestTransformer: BabelTransformer,
183
+ ): Promise<TransformResult>
184
+ ```
185
+
186
+ **Requirements**:
187
+
188
+ - Force `instrument: false` when passing to `babelJestTransformer.process()` / `processAsync()`
189
+ (instrumentation is handled by Jest, not Babel)
190
+ - Pass `result.code` + `result.map` as the content to transform
191
+ - Return the Babel transformer's output directly
192
+
193
+ ---
194
+
195
+ ## Import Rules
196
+
197
+ - `src/experimental/interfaces/*` — allowed
198
+ - `src/experimental/config/*` — allowed
199
+ - `src/experimental/compiler/*` — allowed
200
+ - `src/utils/*` — allowed (sha1, logger, ts-error)
201
+ - `src/types.ts` — allowed
202
+ - `src/legacy/**` — FORBIDDEN
203
+ - All other `src/` folders — FORBIDDEN
204
+
205
+ ---
206
+
207
+ ## Test Requirements
208
+
209
+ Unit tests under `src/experimental/transformer/__tests__/`.
210
+
211
+ | Test file | Key scenarios |
212
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
213
+ | `cache-store.spec.ts` | identity hit returns same instance; serialised string hit returns same instance and updates reference; miss creates new instance; watch detection via process.argv |
214
+ | `file-processor.spec.ts` | each branch fires in correct priority order; stringify branch; .d.ts returns empty; node_module JS → fast transpile; TS/TSX → compiler; unknown ext → warn + passthrough |
215
+ | `cache-key-builder.spec.ts` | same inputs → same key; any single input change → different key; isolated mode skips module mtimes; non-isolated includes module mtime |
216
+ | `hooks-runner.spec.ts` | no TS_JEST_HOOKS env → passthrough; env set → requires module + calls afterProcess; truthy return → used; falsy return → original used; env read at call time not load time |
217
+ | `babel-pipeline.spec.ts` | calls babelJest with instrument:false; async variant works; result code passed through |
218
+
219
+ ---
220
+
221
+ ## Acceptance Criteria
222
+
223
+ - [ ] All new unit tests pass
224
+ - [ ] All existing legacy tests unmodified and green
225
+ - [ ] `tsc --noEmit` passes
226
+ - [ ] No import from `src/legacy/`
227
+ - [ ] `cache-key-builder.ts` has no direct `fs` import — `statSync` is injected
228
+ - [ ] `hooks-runner.ts` reads `TS_JEST_HOOKS` at call time, not at module load time
229
+ - [ ] Every exported symbol has TSDoc
@@ -0,0 +1,211 @@
1
+ # PR 5 — New TsJestTransformer + `future.v30_newTransformer` Opt-in
2
+
3
+ ## Overview
4
+
5
+ Assembles all experimental modules into a new `TsJestTransformer` class and wires it behind the
6
+ `future.v30_newTransformer` opt-in flag. Users explicitly opt in by adding the flag to their Jest
7
+ config. The legacy transformer continues to be the default.
8
+
9
+ **User-visible change**: YES — opt-in only via `future: { v30_newTransformer: true }`
10
+ **Depends on**: PR 1, PR 2, PR 3, PR 4
11
+ **Existing file changes**: minimal, additive only (`src/types.ts`, `src/index.ts`)
12
+
13
+ ---
14
+
15
+ ## Files to Create
16
+
17
+ ```
18
+ src/experimental/transformer/
19
+ transformer.ts
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Files to Modify (additive only)
25
+
26
+ ```
27
+ src/types.ts — add optional future field to TsJestTransformerOptions
28
+ src/index.ts — createTransformer branches on future.v30_newTransformer
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Requirements
34
+
35
+ ### `src/experimental/transformer/transformer.ts`
36
+
37
+ Clean-room `SyncTransformer<TsJestTransformerOptions>` implementation. No static state, no
38
+ `process.env` writes, all I/O injected.
39
+
40
+ **Constructor**:
41
+
42
+ ```ts
43
+ constructor(
44
+ tsJestConfig?: TsJestTransformerOptions,
45
+ private readonly _fs: Pick<typeof fs, 'statSync'> = fs,
46
+ )
47
+ ```
48
+
49
+ - No `process.env.TS_JEST = '1'` (removed in v30)
50
+ - `_cacheStore = new CacheStore()`
51
+ - `_runtimeCacheFS = new Map<string, string>()`
52
+
53
+ **`_configsFor(jestConfig)`** (private):
54
+
55
+ - Delegates to `_cacheStore.getOrCreate(jestConfig, this._tsJestConfig, ResolvedConfig)`
56
+ - Returns `{ config: ResolvedConfigInterface, compiler: CompilerInterface }`
57
+ - `compiler` created lazily via `CompilerFactory.create(config, _runtimeCacheFS)`
58
+ - Compiler is cached per config entry (same config → same compiler instance)
59
+
60
+ **`process(fileContent, filePath, jestOptions)`**:
61
+
62
+ 1. `{ config, compiler } = _configsFor(jestOptions.transformerConfig ?? {})`
63
+ 2. `result = processFile(fileContent, filePath, jestOptions, config, compiler)`
64
+ 3. If `config.babelJestTransformer`: `result = transformWithBabel(result, filePath, jestOptions, babelJestTransformer)`
65
+ 4. `result = runAfterProcessHook(result, fileContent, filePath, jestOptions)`
66
+ 5. Return `result`
67
+
68
+ **`processAsync(fileContent, filePath, jestOptions)`**:
69
+
70
+ 1. Same as `process` steps 1–2
71
+ 2. Collect diagnostics: if `result.diagnostics?.length > 0` → throw `TSError`
72
+ 3. If `config.babelJestTransformer`: `result = await transformWithBabelAsync(...)`
73
+ 4. `result = runAfterProcessHook(...)`
74
+ 5. Return `result`
75
+
76
+ **`getCacheKey(fileContent, filePath, options)`**:
77
+
78
+ - `{ config, compiler } = _configsFor(options.transformerConfig ?? {})`
79
+ - Returns `buildCacheKey(fileContent, filePath, options, config, compiler, this._fs.statSync)`
80
+
81
+ **`getCacheKeyAsync`**: delegates to `getCacheKey` (same logic, wraps in Promise)
82
+
83
+ **Protected extension points** (for subclassing, preserved from legacy):
84
+
85
+ ```ts
86
+ protected _createConfigSet(config: Config.ProjectConfig): ResolvedConfigInterface
87
+ protected _createCompiler(config: ResolvedConfigInterface): CompilerInterface
88
+ ```
89
+
90
+ **v30 behavioral differences vs legacy** (only active under this flag):
91
+
92
+ | Scenario | Legacy behavior | v30 behavior |
93
+ | ----------------------------------- | ---------------------------------------------- | ---------------------------------------------------------- |
94
+ | `isolatedModules: true` in tsconfig | routes to `ts.transpileModule` (no type check) | routes to `LanguageServiceCompiler` (type checking works) |
95
+ | `diagnostics: false` | silently disables diagnostics | accepted + deprecation warning pointing to `noCheck: true` |
96
+ | `noCheck: true` in tsconfig | unknown (not supported) | routes to `FastTranspileCompiler` |
97
+ | `process.env.TS_JEST = '1'` | set in constructor | NOT set |
98
+
99
+ ---
100
+
101
+ ### `src/types.ts` — Additive change
102
+
103
+ Add `future` field to `TsJestTransformerOptions` (the existing public type):
104
+
105
+ ```ts
106
+ // Addition only — existing fields unchanged
107
+ future?: {
108
+ /**
109
+ * Opt in to the v30 transformer rewrite.
110
+ * @experimental
111
+ */
112
+ v30_newTransformer?: boolean
113
+ }
114
+ ```
115
+
116
+ No existing fields modified. No existing types removed.
117
+
118
+ ---
119
+
120
+ ### `src/index.ts` — Additive change
121
+
122
+ Branch `createTransformer` on the future flag:
123
+
124
+ ```ts
125
+ export const createTransformer = (tsJestConfig?: TsJestTransformerOptions) => {
126
+ if (tsJestConfig?.future?.v30_newTransformer) {
127
+ const { NewTsJestTransformer } = require('./experimental/transformer/transformer')
128
+ return new NewTsJestTransformer(tsJestConfig)
129
+ }
130
+ return new TsJestTransformer(tsJestConfig) // legacy path unchanged
131
+ }
132
+ ```
133
+
134
+ The `require()` is intentional: avoids loading any experimental code unless the flag is set.
135
+ No other changes to `src/index.ts`.
136
+
137
+ ---
138
+
139
+ ## Usage (opt-in)
140
+
141
+ ```js
142
+ // jest.config.js
143
+ module.exports = {
144
+ transform: {
145
+ '^.+\\.tsx?$': [
146
+ 'ts-jest',
147
+ {
148
+ future: { v30_newTransformer: true },
149
+ },
150
+ ],
151
+ },
152
+ }
153
+ ```
154
+
155
+ Users not setting this flag get identical behavior to before.
156
+
157
+ ---
158
+
159
+ ## Import Rules
160
+
161
+ ### `src/experimental/transformer/transformer.ts`
162
+
163
+ - `src/experimental/**` — all submodules allowed
164
+ - `src/utils/*` — allowed
165
+ - `src/types.ts` — allowed
166
+ - `src/legacy/**` — FORBIDDEN
167
+
168
+ ### `src/index.ts` change
169
+
170
+ - Uses `require()` for lazy load of experimental transformer
171
+ - No static import of experimental code at module level
172
+
173
+ ---
174
+
175
+ ## Test Requirements
176
+
177
+ Unit tests under `src/experimental/transformer/__tests__/`.
178
+
179
+ | Test file | Key scenarios |
180
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
181
+ | `transformer.spec.ts` | `process` happy path; `processAsync` throws TSError on diagnostics; `getCacheKey` returns stable key; babel pipeline invoked when configured; hooks runner invoked; no `process.env.TS_JEST` set; `isolatedModules: true` → `LanguageServiceCompiler`; `noCheck: true` → `FastTranspileCompiler`; `diagnostics: false` → deprecation warning |
182
+
183
+ Integration (e2e) — add a new e2e project `e2e/v30-transformer/` exercising the new transformer
184
+ via `future: { v30_newTransformer: true }`:
185
+
186
+ | Scenario | e2e test |
187
+ | ------------------------------------ | ---------------------------------------------------------------- |
188
+ | Basic TS compilation | `.spec.ts` with simple import, passes |
189
+ | Type error caught | `.spec.ts` with deliberate type error, jest reports it |
190
+ | `noCheck: true` skips type errors | `.spec.ts` with type error + `noCheck: true` in tsconfig, passes |
191
+ | Source maps work | Source map points to original `.ts` line numbers |
192
+ | `isolatedModules: true` + type error | Type error IS caught (fix for legacy bug) |
193
+
194
+ **All existing tests must remain green** — legacy path is completely unchanged.
195
+
196
+ ---
197
+
198
+ ## Acceptance Criteria
199
+
200
+ - [ ] All new unit tests pass
201
+ - [ ] New e2e tests pass
202
+ - [ ] All existing unit + e2e tests unmodified and green
203
+ - [ ] `tsc --noEmit` passes
204
+ - [ ] `process.env.TS_JEST` is NOT set by new transformer
205
+ - [ ] `future.v30_newTransformer: false` or absent → legacy `TsJestTransformer` used (no regression)
206
+ - [ ] `future.v30_newTransformer: true` → new transformer used
207
+ - [ ] `isolatedModules: true` → `LanguageServiceCompiler` (type checking works)
208
+ - [ ] `diagnostics: false` → deprecation warning logged
209
+ - [ ] `noCheck: true` + `diagnostics.warnOnly` → silent (no warning)
210
+ - [ ] No experimental code loaded unless flag is set
211
+ - [ ] Every exported symbol has TSDoc