Skip to content

Commit b6f027b

Browse files
feat(typescript): expose latest Program to transformers in watch mode (#1923)
* feat(typescript): expose latest Program to transformers in watch mode\n\n- Add optional getProgram to ProgramTransformerFactory.factory\n- Pass getProgram() to program-scoped transformer factories\n- Recompute custom transformers after each TS watch rebuild\n- Docs: document getProgram and watch-mode behavior\n- Test: ensure factories are recreated and getProgram returns the latest Program * test(typescript): close bundle in new watch-mode transformer test; refactor customTransformers for clearer lazy initialization * test(typescript): add watch-mode test for typeChecker factories; tidy emitted-file cleanup in program test * fix(typescript): replace runtime undefined with void 0 in new code\n\n- watchProgram: use void 0 when resetting cached transformers in watch mode\n- tests: use void 0 in new watch-mode transformer test to satisfy no-undefined\n\nRefs PR #1923 review feedback from @shellscape * feat(typescript): add `recreateTransformersOnRebuild` option; default to legacy watch behavior\n\n- Gate watch-mode transformer recreation behind new option (default false)\n- Plumb option through plugin options -> watch host\n- Types/README: document option and clarify `getProgram` semantics\n- Tests: enable option for watch-mode freshness tests * test(typescript): add legacy-behavior regression and format via ESLint * docs(typescript): clarify getProgram getter semantics; fix typos in README (its/TypeScript factory, below) * fix(typescript): strict boolean normalization for recreateTransformersOnRebuild\n\nUse === true instead of Boolean(...) to avoid enabling the option for truthy strings in JS configs. Refs PR #1923 review by @shellscape. --------- Co-authored-by: CharlieHelps <charlie@charlielabs.ai>
1 parent a9cdbb5 commit b6f027b

File tree

7 files changed

+328
-23
lines changed

7 files changed

+328
-23
lines changed

packages/typescript/README.md

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,20 @@ Supported transformer factories:
142142

143143
- all **built-in** TypeScript custom transformer factories:
144144

145-
- `import('typescript').TransformerFactory` annotated **TransformerFactory** bellow
146-
- `import('typescript').CustomTransformerFactory` annotated **CustomTransformerFactory** bellow
145+
- `import('typescript').TransformerFactory` annotated **TransformerFactory** below
146+
- `import('typescript').CustomTransformerFactory` annotated **CustomTransformerFactory** below
147147

148148
- **ProgramTransformerFactory** represents a transformer factory allowing the resulting transformer to grab a reference to the **Program** instance
149149

150150
```js
151151
{
152152
type: 'program',
153-
factory: (program: Program) => TransformerFactory | CustomTransformerFactory
153+
// An optional `getProgram` getter is provided in all modes. In non‑watch it returns
154+
// the same Program as the first argument. In watch mode, when the
155+
// `recreateTransformersOnRebuild` option is enabled, the getter reflects the latest
156+
// Program across rebuilds; otherwise it refers to the initial Program.
157+
factory: (program: Program, getProgram?: () => Program) =>
158+
TransformerFactory | CustomTransformerFactory
154159
}
155160
```
156161

@@ -167,16 +172,24 @@ typescript({
167172
transformers: {
168173
before: [
169174
{
170-
// Allow the transformer to get a Program reference in it's factory
175+
// Allow the transformer to get a Program reference in its factory.
176+
// Prefer deferring `getProgram()` usage to transformation time so watch
177+
// mode can see the freshest Program when `recreateTransformersOnRebuild`
178+
// is enabled.
171179
type: 'program',
172-
factory: (program) => {
173-
return ProgramRequiringTransformerFactory(program);
180+
factory: (program, getProgram) => {
181+
const get = getProgram ?? (() => program);
182+
return (context) => (source) => {
183+
const latest = get();
184+
// use `latest` here
185+
return ts.visitEachChild(source, (n) => n, context);
186+
};
174187
}
175188
},
176189
{
177190
type: 'typeChecker',
178191
factory: (typeChecker) => {
179-
// Allow the transformer to get a TypeChecker reference in it's factory
192+
// Allow the transformer to get a TypeChecker reference in its factory
180193
return TypeCheckerRequiringTransformerFactory(typeChecker);
181194
}
182195
}
@@ -209,8 +222,8 @@ Supported transformer factories:
209222
210223
- all **built-in** TypeScript custom transformer factories:
211224
212-
- `import('typescript').TransformerFactory` annotated **TransformerFactory** bellow
213-
- `import('typescript').CustomTransformerFactory` annotated **CustomTransformerFactory** bellow
225+
- `import('typescript').TransformerFactory` annotated **TransformerFactory** below
226+
- `import('typescript').CustomTransformerFactory` annotated **CustomTransformerFactory** below
214227
215228
The example above could be written like this:
216229
@@ -245,6 +258,34 @@ typescript({
245258
});
246259
```
247260
261+
Note on watch mode
262+
263+
By default (legacy behavior), this plugin reuses the same custom transformer factories for the lifetime of a watch session. Advanced users can opt into recreating factories on every TypeScript rebuild by enabling the `recreateTransformersOnRebuild` option. When enabled, both `program`- and `typeChecker`-based factories are rebuilt per watch cycle, and `getProgram()` (when used) reflects the latest Program across rebuilds.
264+
265+
### `recreateTransformersOnRebuild`
266+
267+
Type: `Boolean`<br>
268+
Default: `false` (legacy behavior)
269+
270+
When `true`, the plugin recreates custom transformer factories on each TypeScript watch rebuild. This ensures factories capture the current `Program`/`TypeChecker` per cycle and that the optional `getProgram()` getter provided to `program`-based factories reflects the latest `Program` across rebuilds. Most users do not need this; enable it if your transformers depend on up‑to‑date Program/TypeChecker identities.
271+
272+
```js
273+
// Opt-in to per-rebuild transformer recreation in watch mode
274+
typescript({
275+
recreateTransformersOnRebuild: true,
276+
transformers: {
277+
before: [
278+
{
279+
type: 'program',
280+
factory(program, getProgram) {
281+
/* ... */
282+
}
283+
}
284+
]
285+
}
286+
});
287+
```
288+
248289
### `cacheDir`
249290
250291
Type: `String`<br>

packages/typescript/src/customTransformers.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,19 @@ export function mergeTransformers(
3535

3636
if ('type' in transformer) {
3737
if (typeof transformer.factory === 'function') {
38-
// Allow custom factories to grab the extra information required
39-
program = program || builder.getProgram();
40-
typeChecker = typeChecker || program.getTypeChecker();
41-
4238
let factory: ReturnType<typeof transformer.factory>;
4339

4440
if (transformer.type === 'program') {
45-
program = program || builder.getProgram();
46-
47-
factory = transformer.factory(program);
41+
const currentProgram = program ?? builder.getProgram();
42+
// Pass a getter so transformers can access the latest Program in watch mode
43+
factory = transformer.factory(currentProgram, () => builder.getProgram());
44+
program = currentProgram;
4845
} else {
49-
program = program || builder.getProgram();
50-
typeChecker = typeChecker || program.getTypeChecker();
51-
52-
factory = transformer.factory(typeChecker);
46+
const currentProgram = program ?? builder.getProgram();
47+
const currentTypeChecker = typeChecker ?? currentProgram.getTypeChecker();
48+
factory = transformer.factory(currentTypeChecker);
49+
program = currentProgram;
50+
typeChecker = currentTypeChecker;
5351
}
5452

5553
// Forward the requested reference to the custom transformer factory

packages/typescript/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
3535
outputToFilesystem,
3636
noForceEmit,
3737
transformers,
38+
recreateTransformersOnRebuild,
3839
tsconfig,
3940
tslib,
4041
typescript: ts
@@ -180,7 +181,8 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
180181
status(diagnostic) {
181182
watchProgramHelper.handleStatus(diagnostic);
182183
},
183-
transformers
184+
transformers,
185+
recreateTransformersOnRebuild
184186
});
185187
}
186188
},

packages/typescript/src/options/plugin.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const getPluginOptions = (options: RollupTypescriptOptions) => {
2121
filterRoot,
2222
noForceEmit,
2323
transformers,
24+
recreateTransformersOnRebuild,
2425
tsconfig,
2526
tslib,
2627
typescript,
@@ -41,6 +42,8 @@ export const getPluginOptions = (options: RollupTypescriptOptions) => {
4142
typescript: typescript || defaultTs,
4243
tslib: tslib || getTsLibPath(),
4344
transformers,
45+
// Only enable when explicitly set to true to avoid truthy string pitfalls in JS configs
46+
recreateTransformersOnRebuild: recreateTransformersOnRebuild === true,
4447
outputToFilesystem
4548
};
4649
};

packages/typescript/src/watchProgram.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ interface CreateProgramOptions {
4242
resolveModule: Resolver;
4343
/** Custom TypeScript transformers */
4444
transformers?: CustomTransformerFactories | ((program: Program) => CustomTransformers);
45+
/**
46+
* Advanced: when true, recreate custom transformer factories on each
47+
* TypeScript watch rebuild. Defaults to legacy behavior (false), which
48+
* reuses the same factories for the lifetime of the watch session.
49+
*/
50+
recreateTransformersOnRebuild?: boolean;
4551
}
4652

4753
type DeferredResolve = ((value: boolean | PromiseLike<boolean>) => void) | (() => void);
@@ -142,7 +148,8 @@ function createWatchHost(
142148
writeFile,
143149
status,
144150
resolveModule,
145-
transformers
151+
transformers,
152+
recreateTransformersOnRebuild
146153
}: CreateProgramOptions
147154
): WatchCompilerHostOfFilesAndCompilerOptions<BuilderProgram> {
148155
const createProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram;
@@ -162,6 +169,13 @@ function createWatchHost(
162169
...baseHost,
163170
/** Override the created program so an in-memory emit is used */
164171
afterProgramCreate(program) {
172+
// Optionally recompute custom transformers for each new builder program in watch mode
173+
// so factories capture the current Program/TypeChecker and any provided getters can
174+
// return the latest values. When disabled (default), legacy behavior reuses the
175+
// same factories across rebuilds.
176+
if (recreateTransformersOnRebuild) {
177+
createdTransformers = void 0;
178+
}
165179
const origEmit = program.emit;
166180
// eslint-disable-next-line no-param-reassign
167181
program.emit = (

0 commit comments

Comments
 (0)