Skip to content

Commit b12548f

Browse files
committed
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
1 parent a9cdbb5 commit b12548f

File tree

5 files changed

+101
-7
lines changed

5 files changed

+101
-7
lines changed

packages/typescript/README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ Supported transformer factories:
150150
```js
151151
{
152152
type: 'program',
153-
factory: (program: Program) => TransformerFactory | CustomTransformerFactory
153+
// In watch mode, the optional `getProgram` returns the latest Program
154+
factory: (program: Program, getProgram?: () => Program) =>
155+
TransformerFactory | CustomTransformerFactory
154156
}
155157
```
156158

@@ -167,10 +169,11 @@ typescript({
167169
transformers: {
168170
before: [
169171
{
170-
// Allow the transformer to get a Program reference in it's factory
172+
// Allow the transformer to get a Program reference in its factory
173+
// and, in watch mode, access the latest Program via `getProgram()`
171174
type: 'program',
172-
factory: (program) => {
173-
return ProgramRequiringTransformerFactory(program);
175+
factory: (program, getProgram) => {
176+
return ProgramRequiringTransformerFactory(getProgram ? getProgram() : program);
174177
}
175178
},
176179
{
@@ -245,6 +248,10 @@ typescript({
245248
});
246249
```
247250

251+
Note on watch mode
252+
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.
254+
248255
### `cacheDir`
249256

250257
Type: `String`<br>

packages/typescript/src/customTransformers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ export function mergeTransformers(
3737
if (typeof transformer.factory === 'function') {
3838
// Allow custom factories to grab the extra information required
3939
program = program || builder.getProgram();
40-
typeChecker = typeChecker || program.getTypeChecker();
4140

4241
let factory: ReturnType<typeof transformer.factory>;
4342

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

47-
factory = transformer.factory(program);
46+
// Pass a getter so transformers can access the latest Program in watch mode
47+
factory = transformer.factory(program, () => builder.getProgram());
4848
} else {
4949
program = program || builder.getProgram();
5050
typeChecker = typeChecker || program.getTypeChecker();

packages/typescript/src/watchProgram.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ function createWatchHost(
162162
...baseHost,
163163
/** Override the created program so an in-memory emit is used */
164164
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 = undefined;
165169
const origEmit = program.emit;
166170
// eslint-disable-next-line no-param-reassign
167171
program.emit = (

packages/typescript/test/test.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,3 +1694,81 @@ test.serial('excludes user-configured outDir from processing when allowJs is tru
16941694
fs.rmSync(outDir, { recursive: true, force: true });
16951695
}
16961696
});
1697+
1698+
test.serial(
1699+
'recreates transformers per rebuild and exposes getProgram in watch mode',
1700+
async (t) => {
1701+
const observations = [];
1702+
1703+
const dirName = path.join(__dirname, 'fixtures', 'transformers');
1704+
const outputJs = path.join(dirName, 'main.js');
1705+
1706+
// ensure a clean slate for emitted file
1707+
if (fs.existsSync(outputJs)) {
1708+
fs.unlinkSync(outputJs);
1709+
}
1710+
1711+
const bundle = await rollup({
1712+
input: 'fixtures/transformers/main.ts',
1713+
plugins: [
1714+
typescript({
1715+
tsconfig: false,
1716+
compilerOptions: { module: 'esnext' },
1717+
// Use a fake TS that simulates two watch rebuilds by calling afterProgramCreate twice
1718+
typescript: fakeTypescript({
1719+
createWatchProgram(host) {
1720+
const makeBuilder = (id, value) => {
1721+
const innerProgram = { id };
1722+
return {
1723+
getProgram() {
1724+
return innerProgram;
1725+
},
1726+
emit(_, writeFile) {
1727+
writeFile(outputJs, `export default ${value};`);
1728+
}
1729+
};
1730+
};
1731+
1732+
const p1 = makeBuilder('one', 101);
1733+
host.afterProgramCreate(p1);
1734+
p1.emit();
1735+
1736+
const p2 = makeBuilder('two', 202);
1737+
host.afterProgramCreate(p2);
1738+
p2.emit();
1739+
1740+
return { close() {} };
1741+
}
1742+
}),
1743+
transformers: {
1744+
before: [
1745+
{
1746+
type: 'program',
1747+
factory(program, getProgram) {
1748+
observations.push({
1749+
p: program && program.id,
1750+
gp: getProgram ? getProgram().id : undefined
1751+
});
1752+
// no-op transformer
1753+
return function passthroughFactory(context) {
1754+
return function passthrough(source) {
1755+
return ts.visitEachChild(source, (n) => n, context);
1756+
};
1757+
};
1758+
}
1759+
}
1760+
]
1761+
}
1762+
})
1763+
],
1764+
onwarn
1765+
});
1766+
1767+
await getCode(bundle, { format: 'esm', dir: dirName }, true);
1768+
1769+
t.deepEqual(observations, [
1770+
{ p: 'one', gp: 'one' },
1771+
{ p: 'two', gp: 'two' }
1772+
]);
1773+
}
1774+
);

packages/typescript/types/index.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ export type CustomTransformerFactories = {
2828
interface ProgramTransformerFactory<T extends TransformerStage> {
2929
type: 'program';
3030

31-
factory(program: Program): StagedTransformerFactory<T>;
31+
/**
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
34+
* transformer factories.
35+
*/
36+
factory(program: Program, getProgram?: () => Program): StagedTransformerFactory<T>;
3237
}
3338

3439
interface TypeCheckerTransformerFactory<T extends TransformerStage> {

0 commit comments

Comments
 (0)