Skip to content
Open
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
40 changes: 39 additions & 1 deletion packages/typescript/src/watchProgram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,5 +229,43 @@ export default function createWatchProgram(
context: PluginContext,
options: CreateProgramOptions
) {
return ts.createWatchProgram(createWatchHost(ts, context, options));
const host = createWatchHost(ts, context, options);

// TypeScript schedules program updates through the host's timers and does not
// clear a pending update when the program is closed. Track the timers the
// program arms so `close()` can cancel whatever is still pending; otherwise a
// file change in the last 250ms of a one-shot build fires after `buildEnd`,
// recreates every file watcher on the closed program and keeps Rollup alive.
const timers = new Set<unknown>();
const { setTimeout: schedule, clearTimeout: cancel } = host;
if (schedule && cancel) {
host.setTimeout = (callback, ms, ...args) => {
const timer: unknown = schedule(
() => {
timers.delete(timer);
callback(...args);
},
ms,
...args
);
timers.add(timer);
return timer;
};
host.clearTimeout = (timer) => {
timers.delete(timer);
cancel(timer);
};
}

const program = ts.createWatchProgram(host);
const { close } = program;
program.close = () => {
close.call(program);
if (cancel) {
timers.forEach((timer) => cancel(timer));
}
timers.clear();
};

return program;
}
69 changes: 69 additions & 0 deletions packages/typescript/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,75 @@ test.sequential('supports overriding the TypeScript version', async () => {
const result = await evaluateBundle(bundle);
expect(result).toBe(1337);
});
test.sequential('clears a pending program update when the watch program is closed', async () => {
const scheduled = [];
const cancelled = [];
let updateTimer = null;
let updateFired = false;
let closed = false;
const bundle = await rollup({
input: 'fixtures/overriding-typescript/main.ts',
onwarn,
plugins: [
typescript({
tsconfig: false,
typescript: fakeTypescript({
createWatchCompilerHost() {
return {
afterProgramCreate() {},
// Record the timers instead of arming them so the outcome does not depend on
// how quickly the build reaches `buildEnd`.
setTimeout(callback, ms) {
const timer = { callback, ms };
scheduled.push(timer);
return timer;
},
clearTimeout(timer) {
cancelled.push(timer);
}
};
},
createWatchProgram(host) {
const program = {
emit(_, writeFile) {
writeFile(
path.join(__dirname, 'fixtures/overriding-typescript/main.js'),
'export default 1337;'
);
}
};
host.afterProgramCreate(program);
program.emit();

// A file change in the last 250ms of the build arms TypeScript's update timer
// and `close()` leaves it running.
updateTimer = host.setTimeout(
() => {
updateFired = true;
},
250,
'timerToUpdateProgram'
);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return {
close() {
closed = true;
}
};
}
})
})
]
});
await getCode(bundle, outputOptions);

expect(closed).toBe(true);
expect(scheduled).toHaveLength(1);
expect(scheduled[0].ms).toBe(250);
expect(updateTimer).toBe(scheduled[0]);
expect(cancelled).toHaveLength(1);
expect(cancelled[0]).toBe(updateTimer);
expect(updateFired).toBe(false);
});
test.sequential('should not resolve .d.ts files', async () => {
const bundle = await rollup({
input: 'fixtures/dts/main.ts',
Expand Down