Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions packages/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,20 @@ Supported transformer factories:

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

- `import('typescript').TransformerFactory` annotated **TransformerFactory** bellow
- `import('typescript').CustomTransformerFactory` annotated **CustomTransformerFactory** bellow
- `import('typescript').TransformerFactory` annotated **TransformerFactory** below
- `import('typescript').CustomTransformerFactory` annotated **CustomTransformerFactory** below

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

```js
{
type: 'program',
factory: (program: Program) => TransformerFactory | CustomTransformerFactory
// An optional `getProgram` getter is provided in all modes. In non‑watch it returns
// the same Program as the first argument. In watch mode, when the
// `recreateTransformersOnRebuild` option is enabled, the getter reflects the latest
// Program across rebuilds; otherwise it refers to the initial Program.
factory: (program: Program, getProgram?: () => Program) =>
TransformerFactory | CustomTransformerFactory
}
```

Expand All @@ -167,16 +172,24 @@ typescript({
transformers: {
before: [
{
// Allow the transformer to get a Program reference in it's factory
// Allow the transformer to get a Program reference in its factory.
// Prefer deferring `getProgram()` usage to transformation time so watch
// mode can see the freshest Program when `recreateTransformersOnRebuild`
// is enabled.
type: 'program',
factory: (program) => {
return ProgramRequiringTransformerFactory(program);
factory: (program, getProgram) => {
const get = getProgram ?? (() => program);
return (context) => (source) => {
const latest = get();
// use `latest` here
return ts.visitEachChild(source, (n) => n, context);
};
}
},
{
type: 'typeChecker',
factory: (typeChecker) => {
// Allow the transformer to get a TypeChecker reference in it's factory
// Allow the transformer to get a TypeChecker reference in its factory
return TypeCheckerRequiringTransformerFactory(typeChecker);
}
}
Expand Down Expand Up @@ -209,8 +222,8 @@ Supported transformer factories:

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

- `import('typescript').TransformerFactory` annotated **TransformerFactory** bellow
- `import('typescript').CustomTransformerFactory` annotated **CustomTransformerFactory** bellow
- `import('typescript').TransformerFactory` annotated **TransformerFactory** below
- `import('typescript').CustomTransformerFactory` annotated **CustomTransformerFactory** below

The example above could be written like this:

Expand Down Expand Up @@ -245,6 +258,34 @@ typescript({
});
```

Note on watch mode

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.

### `recreateTransformersOnRebuild`

Type: `Boolean`<br>
Default: `false` (legacy behavior)

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.

```js
// Opt-in to per-rebuild transformer recreation in watch mode
typescript({
recreateTransformersOnRebuild: true,
transformers: {
before: [
{
type: 'program',
factory(program, getProgram) {
/* ... */
}
}
]
}
});
```

### `cacheDir`

Type: `String`<br>
Expand Down
20 changes: 9 additions & 11 deletions packages/typescript/src/customTransformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,19 @@ export function mergeTransformers(

if ('type' in transformer) {
if (typeof transformer.factory === 'function') {
// Allow custom factories to grab the extra information required
program = program || builder.getProgram();
typeChecker = typeChecker || program.getTypeChecker();

let factory: ReturnType<typeof transformer.factory>;

if (transformer.type === 'program') {
program = program || builder.getProgram();

factory = transformer.factory(program);
const currentProgram = program ?? builder.getProgram();
// Pass a getter so transformers can access the latest Program in watch mode
factory = transformer.factory(currentProgram, () => builder.getProgram());
program = currentProgram;
} else {
program = program || builder.getProgram();
typeChecker = typeChecker || program.getTypeChecker();

factory = transformer.factory(typeChecker);
const currentProgram = program ?? builder.getProgram();
const currentTypeChecker = typeChecker ?? currentProgram.getTypeChecker();
factory = transformer.factory(currentTypeChecker);
program = currentProgram;
typeChecker = currentTypeChecker;
}

// Forward the requested reference to the custom transformer factory
Expand Down
4 changes: 3 additions & 1 deletion packages/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
outputToFilesystem,
noForceEmit,
transformers,
recreateTransformersOnRebuild,
tsconfig,
tslib,
typescript: ts
Expand Down Expand Up @@ -180,7 +181,8 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
status(diagnostic) {
watchProgramHelper.handleStatus(diagnostic);
},
transformers
transformers,
recreateTransformersOnRebuild
});
}
},
Expand Down
3 changes: 3 additions & 0 deletions packages/typescript/src/options/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const getPluginOptions = (options: RollupTypescriptOptions) => {
filterRoot,
noForceEmit,
transformers,
recreateTransformersOnRebuild,
tsconfig,
tslib,
typescript,
Expand All @@ -41,6 +42,8 @@ export const getPluginOptions = (options: RollupTypescriptOptions) => {
typescript: typescript || defaultTs,
tslib: tslib || getTsLibPath(),
transformers,
// Only enable when explicitly set to true to avoid truthy string pitfalls in JS configs
recreateTransformersOnRebuild: recreateTransformersOnRebuild === true,
outputToFilesystem
};
};
16 changes: 15 additions & 1 deletion packages/typescript/src/watchProgram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ interface CreateProgramOptions {
resolveModule: Resolver;
/** Custom TypeScript transformers */
transformers?: CustomTransformerFactories | ((program: Program) => CustomTransformers);
/**
* Advanced: when true, recreate custom transformer factories on each
* TypeScript watch rebuild. Defaults to legacy behavior (false), which
* reuses the same factories for the lifetime of the watch session.
*/
recreateTransformersOnRebuild?: boolean;
}

type DeferredResolve = ((value: boolean | PromiseLike<boolean>) => void) | (() => void);
Expand Down Expand Up @@ -142,7 +148,8 @@ function createWatchHost(
writeFile,
status,
resolveModule,
transformers
transformers,
recreateTransformersOnRebuild
}: CreateProgramOptions
): WatchCompilerHostOfFilesAndCompilerOptions<BuilderProgram> {
const createProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram;
Expand All @@ -162,6 +169,13 @@ function createWatchHost(
...baseHost,
/** Override the created program so an in-memory emit is used */
afterProgramCreate(program) {
// Optionally recompute custom transformers for each new builder program in watch mode
// so factories capture the current Program/TypeChecker and any provided getters can
// return the latest values. When disabled (default), legacy behavior reuses the
// same factories across rebuilds.
if (recreateTransformersOnRebuild) {
createdTransformers = void 0;
}
const origEmit = program.emit;
// eslint-disable-next-line no-param-reassign
program.emit = (
Expand Down
Loading
Loading