Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: unecessary program updates by removing timeout methods #1693

Merged
merged 10 commits into from Mar 12, 2020
68 changes: 43 additions & 25 deletions packages/typescript-estree/src/create-program/createWatchProgram.ts
@@ -1,5 +1,6 @@
import debug from 'debug';
import fs from 'fs';
import semver from 'semver';
import * as ts from 'typescript';
import { Extra } from '../parser-options';
import { WatchCompilerHostOfConfigFile } from './WatchCompilerHostOfConfigFile';
Expand All @@ -18,7 +19,7 @@ const log = debug('typescript-eslint:typescript-estree:createWatchProgram');
*/
const knownWatchProgramMap = new Map<
CanonicalPath,
ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram>
ts.WatchOfConfigFile<ts.BuilderProgram>
>();

/**
Expand Down Expand Up @@ -226,21 +227,25 @@ function getProgramsForProjects(
return results;
}

const isRunningNoTimeoutFix = semver.satisfies(ts.version, '>=3.9.0-beta', {
includePrerelease: true,
});

function createWatchProgram(
tsconfigPath: string,
extra: Extra,
): ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> {
): ts.WatchOfConfigFile<ts.BuilderProgram> {
log('Creating watch program for %s.', tsconfigPath);

// create compiler host
const watchCompilerHost = ts.createWatchCompilerHost(
tsconfigPath,
createDefaultCompilerOptionsFromExtra(extra),
ts.sys,
ts.createSemanticDiagnosticsBuilderProgram,
ts.createAbstractBuilder,
diagnosticReporter,
/*reportWatchStatus*/ () => {},
) as WatchCompilerHostOfConfigFile<ts.SemanticDiagnosticsBuilderProgram>;
) as WatchCompilerHostOfConfigFile<ts.BuilderProgram>;

// ensure readFile reads the code being linted instead of the copy on disk
const oldReadFile = watchCompilerHost.readFile;
Expand All @@ -250,7 +255,7 @@ function createWatchProgram(
filePath === currentLintOperationState.filePath
? currentLintOperationState.code
: oldReadFile(filePath, encoding);
if (fileContent) {
if (fileContent !== undefined) {
parsedFilesSeenHash.set(filePath, createHash(fileContent));
}
return fileContent;
Expand Down Expand Up @@ -309,25 +314,38 @@ function createWatchProgram(
);
oldOnDirectoryStructureHostCreate(host);
};

/*
* The watch change callbacks TS provides us all have a 250ms delay before firing
* https://github.com/microsoft/TypeScript/blob/b845800bdfcc81c8c72e2ac6fdc2c1df0cdab6f9/src/compiler/watch.ts#L1013
*
* We live in a synchronous world, so we can't wait for that.
* This is a bit of a hack, but it lets us immediately force updates when we detect a tsconfig or directory change
*/
const oldSetTimeout = watchCompilerHost.setTimeout;
watchCompilerHost.setTimeout = (cb, ms, ...args): unknown => {
if (ms === 250) {
cb();
return null;
}

return oldSetTimeout?.(cb, ms, ...args);
};

return ts.createWatchProgram(watchCompilerHost);
watchCompilerHost.trace = log;

// Since we don't want to asynchronously update program we want to disable timeout methods
// So any changes in the program will be delayed and updated when getProgram is called on watch
let callback: (() => void) | undefined;
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
if (isRunningNoTimeoutFix) {
watchCompilerHost.setTimeout = undefined;
watchCompilerHost.clearTimeout = undefined;
} else {
log('Running without timeout fix');
// But because of https://github.com/microsoft/TypeScript/pull/37308 we cannot just set it to undefined
// instead save it and call before getProgram is called
watchCompilerHost.setTimeout = (cb, _ms, ...args): unknown => {
callback = cb.bind(/*this*/ undefined, ...args);
return callback;
};
watchCompilerHost.clearTimeout = (): void => {
callback = undefined;
};
}
const watch = ts.createWatchProgram(watchCompilerHost);
if (!isRunningNoTimeoutFix) {
const originalGetProgram = watch.getProgram;
watch.getProgram = (): ts.BuilderProgram => {
if (callback) {
callback();
}
callback = undefined;
return originalGetProgram.call(watch);
};
}
return watch;
}

function hasTSConfigChanged(tsconfigPath: CanonicalPath): boolean {
Expand All @@ -347,7 +365,7 @@ function hasTSConfigChanged(tsconfigPath: CanonicalPath): boolean {
}

function maybeInvalidateProgram(
existingWatch: ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram>,
existingWatch: ts.WatchOfConfigFile<ts.BuilderProgram>,
filePath: CanonicalPath,
tsconfigPath: CanonicalPath,
): ts.Program | null {
Expand Down