Skip to content

Commit 6e4160f

Browse files
committed
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
1 parent 4c8092d commit 6e4160f

File tree

6 files changed

+77
-13
lines changed

6 files changed

+77
-13
lines changed

packages/typescript/README.md

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,10 @@ Supported transformer factories:
150150
```js
151151
{
152152
type: 'program',
153-
// In watch mode, the optional `getProgram` returns the latest Program
153+
// An optional `getProgram` getter is always provided. In non‑watch it returns
154+
// the same Program as the first argument. In watch mode, enabling the
155+
// `recreateTransformersOnRebuild` option makes the getter reflect the latest
156+
// Program across rebuilds; otherwise it refers to the initial Program.
154157
factory: (program: Program, getProgram?: () => Program) =>
155158
TransformerFactory | CustomTransformerFactory
156159
}
@@ -169,11 +172,18 @@ typescript({
169172
transformers: {
170173
before: [
171174
{
172-
// Allow the transformer to get a Program reference in its factory
173-
// and, in watch mode, access the latest Program via `getProgram()`
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.
174179
type: 'program',
175180
factory: (program, getProgram) => {
176-
return ProgramRequiringTransformerFactory(getProgram ? getProgram() : program);
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+
};
177187
}
178188
},
179189
{
@@ -250,7 +260,31 @@ typescript({
250260
251261
Note on watch mode
252262
253-
When running Rollup in watch mode, this plugin recreates custom transformer factories after each TypeScript program rebuild so they receive the current Program and TypeChecker. If your transformer needs to query the latest type information across rebuilds, prefer using the optional `getProgram()` parameter provided to `program`-based transformer factories.
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+
```
254288
255289
### `cacheDir`
256290

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: 2 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,7 @@ export const getPluginOptions = (options: RollupTypescriptOptions) => {
4142
typescript: typescript || defaultTs,
4243
tslib: tslib || getTsLibPath(),
4344
transformers,
45+
recreateTransformersOnRebuild,
4446
outputToFilesystem
4547
};
4648
};

packages/typescript/src/watchProgram.ts

Lines changed: 15 additions & 5 deletions
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,10 +169,13 @@ function createWatchHost(
162169
...baseHost,
163170
/** Override the created program so an in-memory emit is used */
164171
afterProgramCreate(program) {
165-
// Ensure we recompute custom transformers for each new builder program in watch mode
166-
// so factories capture the current Program/TypeChecker and any provided getters return
167-
// the latest values. This avoids freezing the initial Program across rebuilds.
168-
createdTransformers = void 0;
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+
}
169179
const origEmit = program.emit;
170180
// eslint-disable-next-line no-param-reassign
171181
program.emit = (

packages/typescript/test/test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,6 +1714,7 @@ test.serial(
17141714
typescript({
17151715
tsconfig: false,
17161716
compilerOptions: { module: 'esnext' },
1717+
recreateTransformersOnRebuild: true,
17171718
// Use a fake TS that simulates two watch rebuilds by calling afterProgramCreate twice
17181719
typescript: fakeTypescript({
17191720
createWatchProgram(host) {
@@ -1795,6 +1796,7 @@ test.serial('recreates typeChecker-based transformers per rebuild in watch mode'
17951796
typescript({
17961797
tsconfig: false,
17971798
compilerOptions: { module: 'esnext' },
1799+
recreateTransformersOnRebuild: true,
17981800
// Fake TS that simulates two watch rebuilds, each returning a distinct TypeChecker
17991801
typescript: fakeTypescript({
18001802
createWatchProgram(host) {

packages/typescript/types/index.d.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,14 @@ interface ProgramTransformerFactory<T extends TransformerStage> {
2929
type: 'program';
3030

3131
/**
32-
* Factory that may receive a getter for the latest Program when running in watch mode.
33-
* The second parameter is optional to preserve backwards compatibility with existing
32+
* Factory receives the current Program and may also receive a getter for retrieving
33+
* the Program at call time. The getter is provided in all modes. In non-watch it
34+
* returns the same Program as the first argument. In watch mode:
35+
* - When `recreateTransformersOnRebuild` is enabled (plugin option), the getter
36+
* reflects the latest Program across rebuilds.
37+
* - When disabled (default, legacy behavior), factories are reused and the getter
38+
* refers to the initial Program from when the factory was created.
39+
* The second parameter remains optional for backwards compatibility with existing
3440
* transformer factories.
3541
*/
3642
factory(program: Program, getProgram?: () => Program): StagedTransformerFactory<T>;
@@ -94,6 +100,14 @@ export interface RollupTypescriptPluginOptions {
94100
* Override force setting of `noEmit` and `emitDeclarationOnly` and use what is defined in `tsconfig.json`
95101
*/
96102
noForceEmit?: boolean;
103+
/**
104+
* Advanced: when true, recreate custom transformer factories on each TypeScript
105+
* watch rebuild so that `program`/`typeChecker`-based factories are rebuilt and
106+
* `getProgram()` (when used) reflects the latest Program across rebuilds.
107+
* Defaults to false (legacy behavior), which reuses factories for the lifetime
108+
* of the watch session.
109+
*/
110+
recreateTransformersOnRebuild?: boolean;
97111
}
98112

99113
export interface FlexibleCompilerOptions extends CompilerOptions {

0 commit comments

Comments
 (0)