diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 0042abfcb9f..9ec1b3f99c4 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -236,7 +236,6 @@ namespace ts { export interface SolutionBuilderHostBase extends ProgramHost { getModifiedTime(fileName: string): Date | undefined; setModifiedTime(fileName: string, date: Date): void; - deleteFile(fileName: string): void; reportDiagnostic: DiagnosticReporter; // Technically we want to move it out and allow steps of actions on Solution, but for now just merge stuff in build host here reportSolutionBuilderStatus: DiagnosticReporter; @@ -246,10 +245,11 @@ namespace ts { afterProgramEmitAndDiagnostics?(program: T): void; // For testing - now?(): Date; + /*@internal*/ now?(): Date; } export interface SolutionBuilderHost extends SolutionBuilderHostBase { + deleteFile(fileName: string): void; reportErrorSummary?: ReportEmitErrorSummary; } @@ -262,17 +262,16 @@ namespace ts { // Currently used for testing but can be made public if needed: /*@internal*/ getBuildOrder(): ReadonlyArray; + /*@internal*/ resetBuildContext(opts?: BuildOptions): void; // Testing only - - // TODO:: All the below ones should technically only be in watch mode. but thats for later time + /*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus; /*@internal*/ invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel): void; /*@internal*/ buildInvalidatedProject(): void; - - /*@internal*/ resetBuildContext(opts?: BuildOptions): void; } - export interface SolutionBuilderWithWatch extends SolutionBuilder { + export interface SolutionBuilderWithWatch { + buildAllProjects(): ExitStatus; /*@internal*/ startWatching(): void; } @@ -291,7 +290,6 @@ namespace ts { const host = createProgramHost(system, createProgram) as SolutionBuilderHostBase; host.getModifiedTime = system.getModifiedTime ? path => system.getModifiedTime!(path) : returnUndefined; host.setModifiedTime = system.setModifiedTime ? (path, date) => system.setModifiedTime!(path, date) : noop; - host.deleteFile = system.deleteFile ? path => system.deleteFile!(path) : noop; host.reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system); host.reportSolutionBuilderStatus = reportSolutionBuilderStatus || createBuilderStatusReporter(system); return host; @@ -299,6 +297,7 @@ namespace ts { export function createSolutionBuilderHost(system = sys, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportErrorSummary?: ReportEmitErrorSummary) { const host = createSolutionBuilderHostBase(system, createProgram, reportDiagnostic, reportSolutionBuilderStatus) as SolutionBuilderHost; + host.deleteFile = system.deleteFile ? path => system.deleteFile!(path) : noop; host.reportErrorSummary = reportErrorSummary; return host; } @@ -318,16 +317,23 @@ namespace ts { return result; } + export function createSolutionBuilder(host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder { + return createSolutionBuilderWorker(/*watch*/ false, host, rootNames, defaultOptions); + } + + export function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilderWithWatch { + return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions); + } + /** * A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but * can dynamically add/remove other projects based on changes on the rootNames' references - * TODO: use SolutionBuilderWithWatchHost => watchedSolution - * use SolutionBuilderHost => Solution */ - export function createSolutionBuilder(host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; - export function createSolutionBuilder(host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilderWithWatch; - export function createSolutionBuilder(host: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilderWithWatch { - const hostWithWatch = host as SolutionBuilderWithWatchHost; + function createSolutionBuilderWorker(watch: false, host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilderWorker(watch: true, host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilderWithWatch; + function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder | SolutionBuilderWithWatch { + const host = hostOrHostWithWatch as SolutionBuilderHost; + const hostWithWatch = hostOrHostWithWatch as SolutionBuilderWithWatchHost; const currentDirectory = host.getCurrentDirectory(); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); const parseConfigFileHost = parseConfigHostFromCompilerHostLike(host); @@ -360,24 +366,27 @@ namespace ts { const projectErrorsReported = createMap() as ConfigFileMap; let timerToBuildInvalidatedProject: any; let reportFileChangeDetected = false; - const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(host, options); + const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(hostWithWatch, options); // Watches for the solution const allWatchedWildcardDirectories = createMap() as ConfigFileMap>; const allWatchedInputFiles = createMap() as ConfigFileMap>; const allWatchedConfigFiles = createMap() as ConfigFileMap; - return { - buildAllProjects, - cleanAllProjects, - resetBuildContext, - getBuildOrder, - - invalidateProject, - buildInvalidatedProject, - - startWatching - }; + return watch ? + { + buildAllProjects, + startWatching + } : + { + buildAllProjects, + cleanAllProjects, + getBuildOrder, + resetBuildContext, + getUpToDateStatusOfProject, + invalidateProject, + buildInvalidatedProject, + }; function toPath(fileName: string) { return ts.toPath(fileName, currentDirectory, getCanonicalFileName); @@ -392,8 +401,8 @@ namespace ts { return resolvedPath; } - function resetBuildContext(opts = defaultOptions) { - options = opts; + function resetBuildContext(opts?: BuildOptions) { + options = opts || defaultOptions; baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); resolvedConfigFilePaths.clear(); configFileCache.clear(); @@ -462,7 +471,7 @@ namespace ts { } function watchConfigFile(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath) { - if (options.watch && !allWatchedConfigFiles.has(resolvedPath)) { + if (watch && !allWatchedConfigFiles.has(resolvedPath)) { allWatchedConfigFiles.set(resolvedPath, watchFile( hostWithWatch, resolved, @@ -477,7 +486,7 @@ namespace ts { } function watchWildCardDirectories(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (!options.watch) return; + if (!watch) return; updateWatchingWildcardDirectories( getOrCreateValueMapFromConfigFileMap(allWatchedWildcardDirectories, resolvedPath), createMapFromTemplate(parsed.configFileSpecs!.wildcardDirectories), @@ -508,7 +517,7 @@ namespace ts { } function watchInputFiles(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (!options.watch) return; + if (!watch) return; mutateMap( getOrCreateValueMapFromConfigFileMap(allWatchedInputFiles, resolvedPath), arrayToMap(parsed.fileNames, toPath), @@ -565,8 +574,14 @@ namespace ts { scheduleBuildInvalidatedProject(); } + function getUpToDateStatusOfProject(project: string): UpToDateStatus { + const configFileName = resolveProjectName(project); + const configFilePath = toResolvedConfigFilePath(configFileName); + return getUpToDateStatus(parseConfigFile(configFileName, configFilePath), configFilePath); + } + function getBuildOrder() { - return buildOrder || (buildOrder = createBuildOrder(rootNames.map(resolveProjectName))); + return buildOrder || (buildOrder = createBuildOrder(resolveProjectNames(rootNames))); } function getUpToDateStatus(project: ParsedCommandLine | undefined, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { @@ -851,7 +866,7 @@ namespace ts { if (buildProject) { buildSingleInvalidatedProject(buildProject.project, buildProject.reloadLevel); if (hasPendingInvalidatedProjects()) { - if (options.watch && !timerToBuildInvalidatedProject) { + if (watch && !timerToBuildInvalidatedProject) { scheduleBuildInvalidatedProject(); } } @@ -862,7 +877,7 @@ namespace ts { } function reportErrorSummary() { - if (options.watch || (host as SolutionBuilderHost).reportErrorSummary) { + if (watch || host.reportErrorSummary) { // Report errors from the other projects getBuildOrder().forEach(project => { const projectPath = toResolvedConfigFilePath(project); @@ -872,11 +887,11 @@ namespace ts { }); let totalErrors = 0; diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors)); - if (options.watch) { + if (watch) { reportWatchStatus(getWatchErrorSummaryDiagnosticMessage(totalErrors), totalErrors); } else { - (host as SolutionBuilderHost).reportErrorSummary!(totalErrors); + host.reportErrorSummary!(totalErrors); } } } @@ -1165,7 +1180,7 @@ namespace ts { if (host.afterProgramEmitAndDiagnostics) { host.afterProgramEmitAndDiagnostics(program); } - if (options.watch) { + if (watch) { program.releaseProgram(); builderPrograms.set(proj, program); } @@ -1312,8 +1327,12 @@ namespace ts { return resolveConfigFileProjectName(resolvePath(host.getCurrentDirectory(), name)); } + function resolveProjectNames(configFileNames: ReadonlyArray): ResolvedConfigFileName[] { + return configFileNames.map(resolveProjectName); + } + function buildAllProjects(): ExitStatus { - if (options.watch) { reportWatchStatus(Diagnostics.Starting_compilation_in_watch_mode); } + if (watch) { reportWatchStatus(Diagnostics.Starting_compilation_in_watch_mode); } // TODO:: In watch mode as well to use caches for incremental build once we can invalidate caches correctly and have right api // Override readFile for json files and output .d.ts to cache the text const savedReadFileWithCache = readFileWithCache; diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 2621bd303ea..c004f53d38e 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -337,6 +337,61 @@ namespace ts { }); }); + describe("project invalidation", () => { + it("invalidates projects correctly", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); + + builder.buildAllProjects(); + host.assertDiagnosticMessages(/*empty*/); + + // Update a timestamp in the middle project + tick(); + appendText(fs, "/src/logic/index.ts", "function foo() {}"); + const originalWriteFile = fs.writeFileSync; + const writtenFiles = createMap(); + fs.writeFileSync = (path, data, encoding) => { + writtenFiles.set(path, true); + originalWriteFile.call(fs, path, data, encoding); + }; + // Because we haven't reset the build context, the builder should assume there's nothing to do right now + const status = builder.getUpToDateStatusOfProject("/src/logic"); + assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); + verifyInvalidation(/*expectedToWriteTests*/ false); + + // Rebuild this project + fs.writeFileSync("/src/logic/index.ts", `${fs.readFileSync("/src/logic/index.ts")} +export class cNew {}`); + verifyInvalidation(/*expectedToWriteTests*/ true); + + function verifyInvalidation(expectedToWriteTests: boolean) { + // Rebuild this project + tick(); + builder.invalidateProject("/src/logic"); + builder.buildInvalidatedProject(); + // The file should be updated + assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt"); + assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); + assert.isFalse(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should *not* have been rebuilt"); + assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); + writtenFiles.clear(); + + // Build downstream projects should update 'tests', but not 'core' + tick(); + builder.buildInvalidatedProject(); + if (expectedToWriteTests) { + assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt"); + } + else { + assert.equal(writtenFiles.size, 0, "Should not write any new files"); + } + assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have new timestamp"); + assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); + } + }); + }); + describe("lists files", () => { it("listFiles", () => { const fs = projFs.shadow(); diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index 005956a98ad..ef87aa1f9cb 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -17,12 +17,13 @@ namespace ts.tscWatch { } export function createSolutionBuilder(system: WatchedSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { - const host = createSolutionBuilderWithWatchHost(system); - return ts.createSolutionBuilder(host, rootNames, defaultOptions || { watch: true }); + const host = createSolutionBuilderHost(system); + return ts.createSolutionBuilder(host, rootNames, defaultOptions || {}); } - function createSolutionBuilderWithWatch(host: TsBuildWatchSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { - const solutionBuilder = createSolutionBuilder(host, rootNames, defaultOptions); + function createSolutionBuilderWithWatch(system: TsBuildWatchSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { + const host = createSolutionBuilderWithWatchHost(system); + const solutionBuilder = ts.createSolutionBuilderWithWatch(host, rootNames, defaultOptions || { watch: true }); solutionBuilder.buildAllProjects(); solutionBuilder.startWatching(); return solutionBuilder; @@ -1140,9 +1141,7 @@ export function gfoo() { it("incremental updates in verbose mode", () => { const host = createTsBuildWatchSystem(allFiles, { currentDirectory: projectsLocation }); - const solutionBuilder = createSolutionBuilder(host, [`${project}/${SubProject.tests}`], { verbose: true, watch: true }); - solutionBuilder.buildAllProjects(); - solutionBuilder.startWatching(); + createSolutionBuilderWithWatch(host, [`${project}/${SubProject.tests}`], { verbose: true, watch: true }); checkOutputErrorsInitial(host, emptyArray, /*disableConsoleClears*/ undefined, [ `Projects in this build: \r\n * sample1/core/tsconfig.json\r\n * sample1/logic/tsconfig.json\r\n * sample1/tests/tsconfig.json\n\n`, `Project 'sample1/core/tsconfig.json' is out of date because output file 'sample1/core/anotherModule.js' does not exist\n\n`, diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index 500480463bd..35bd29ea774 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -183,6 +183,9 @@ namespace ts { function performBuild(args: string[]) { const { buildOptions, projects, errors } = parseBuildCommand(args); + // Update to pretty if host supports it + updateReportDiagnostic(buildOptions); + if (errors.length > 0) { errors.forEach(reportDiagnostic); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); @@ -194,8 +197,6 @@ namespace ts { return sys.exit(ExitStatus.Success); } - // Update to pretty if host supports it - updateReportDiagnostic(buildOptions); if (projects.length === 0) { printVersion(); printHelp(buildOpts, "--build "); @@ -210,24 +211,21 @@ namespace ts { reportWatchModeWithoutSysSupport(); } - // Use default createProgram - const buildHost = buildOptions.watch ? - createSolutionBuilderWithWatchHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createWatchStatusReporter(buildOptions)) : - createSolutionBuilderHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createReportErrorSummary(buildOptions)); - updateCreateProgram(buildHost); - buildHost.afterProgramEmitAndDiagnostics = (program: BuilderProgram) => reportStatistics(program.getProgram()); - - const builder = createSolutionBuilder(buildHost, projects, buildOptions); - if (buildOptions.clean) { - return sys.exit(builder.cleanAllProjects()); - } - if (buildOptions.watch) { + const buildHost = createSolutionBuilderWithWatchHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createWatchStatusReporter(buildOptions)); + updateCreateProgram(buildHost); + buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(program.getProgram()); + const builder = createSolutionBuilderWithWatch(buildHost, projects, buildOptions); builder.buildAllProjects(); - return (builder as SolutionBuilderWithWatch).startWatching(); + return builder.startWatching(); + } + else { + const buildHost = createSolutionBuilderHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createReportErrorSummary(buildOptions)); + updateCreateProgram(buildHost); + buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(program.getProgram()); + const builder = createSolutionBuilder(buildHost, projects, buildOptions); + return sys.exit(buildOptions.clean ? builder.cleanAllProjects() : builder.buildAllProjects()); } - - return sys.exit(builder.buildAllProjects()); } function createReportErrorSummary(options: CompilerOptions | BuildOptions): ReportEmitErrorSummary | undefined {