Skip to content

Commit

Permalink
feat: support incremental TypeScript semantic diagnostics
Browse files Browse the repository at this point in the history
  • Loading branch information
alan-agius4 committed May 19, 2023
1 parent 9b1ba29 commit d3b9488
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 39 deletions.
2 changes: 1 addition & 1 deletion src/lib/file-system/file-watcher.ts
Expand Up @@ -24,7 +24,7 @@ export function createFileWatch(

const watch = chokidar.watch([], {
ignoreInitial: true,
ignored: [...ignoredPaths, /\.map$/],
ignored: [...ignoredPaths, /\.map$/, /.tsbuildinfo$/],
persistent: true,
});

Expand Down
Expand Up @@ -54,6 +54,7 @@ function analyseEntryPoint(graph: BuildGraph, entryPoint: EntryPointNode, entryP
moduleResolutionCache,
undefined,
undefined,
undefined,
analysesSourcesFileCache,
);

Expand Down
2 changes: 1 addition & 1 deletion src/lib/ng-package/entry-point/compile-ngc.transform.ts
Expand Up @@ -48,14 +48,14 @@ export const compileNgcTransformFactory = (
graph,
tsConfig,
moduleResolutionCache,
options,
{
outDir: path.dirname(esm2022),
declarationDir: path.dirname(declarations),
declaration: true,
target: ts.ScriptTarget.ES2022,
},
entryPoint.cache.stylesheetProcessor,
options.watch,
);
} catch (error) {
spinner.fail();
Expand Down
6 changes: 3 additions & 3 deletions src/lib/ng-package/package.transform.ts
Expand Up @@ -19,7 +19,7 @@ import {
} from 'rxjs';
import { createFileWatch } from '../file-system/file-watcher';
import { BuildGraph } from '../graph/build-graph';
import { STATE_IN_PROGRESS } from '../graph/node';
import { STATE_DIRTY, STATE_DONE, STATE_IN_PROGRESS } from '../graph/node';
import { Transform } from '../graph/transform';
import { colors } from '../utils/color';
import { rmdir } from '../utils/fs';
Expand Down Expand Up @@ -161,7 +161,7 @@ const watchTransformFactory =
for (const entryPoint of graph.filter(isEntryPoint)) {
const isDirty = [...allNodesToClean].some(dependent => entryPoint.dependents.has(dependent));
if (isDirty) {
entryPoint.state = 'dirty';
entryPoint.state = STATE_DIRTY;

uriToClean.forEach(url => {
entryPoint.cache.analysesSourcesFileCache.delete(fileUrlPath(url));
Expand Down Expand Up @@ -241,7 +241,7 @@ const scheduleEntryPoints = (epTransform: Transform): Transform =>
// Build entry points with lower depth values first.
return from(groups).pipe(
map((epUrl: string): EntryPointNode => graph.find(byEntryPoint().and(ep => ep.url === epUrl))),
filter((entryPoint: EntryPointNode): boolean => entryPoint.state !== 'done'),
filter((entryPoint: EntryPointNode): boolean => entryPoint.state !== STATE_DONE),
concatMap(ep =>
observableOf(ep).pipe(
// Mark the entry point as 'in-progress'
Expand Down
118 changes: 84 additions & 34 deletions src/lib/ngc/compile-source-files.ts
@@ -1,7 +1,9 @@
import type { CompilerOptions, ParsedConfiguration } from '@angular/compiler-cli';
import { CompilerOptions, ParsedConfiguration } from '@angular/compiler-cli';
import { join } from 'node:path';
import ts from 'typescript';
import { BuildGraph } from '../graph/build-graph';
import { EntryPointNode, PackageNode, isEntryPointInProgress, isPackage } from '../ng-package/nodes';
import { NgPackagrOptions } from '../ng-package/options.di';
import { StylesheetProcessor } from '../styles/stylesheet-processor';
import { augmentProgramWithVersioning, cacheCompilerHost } from '../ts/cache-compiler-host';
import * as log from '../utils/log';
Expand All @@ -11,28 +13,46 @@ export async function compileSourceFiles(
graph: BuildGraph,
tsConfig: ParsedConfiguration,
moduleResolutionCache: ts.ModuleResolutionCache,
options: NgPackagrOptions,
extraOptions?: Partial<CompilerOptions>,
stylesheetProcessor?: StylesheetProcessor,
watch?: boolean,
) {
const { NgtscProgram, formatDiagnostics } = await ngCompilerCli();

const { cacheDirectory, watch, cacheEnabled } = options;
const tsConfigOptions: CompilerOptions = { ...tsConfig.options, ...extraOptions };
const entryPoint: EntryPointNode = graph.find(isEntryPointInProgress());
const ngPackageNode: PackageNode = graph.find(isPackage);
const inlineStyleLanguage = ngPackageNode.data.inlineStyleLanguage;

const cacheDir = cacheEnabled && cacheDirectory;
if (cacheDir) {
tsConfigOptions.incremental ??= true;
tsConfigOptions.tsBuildInfoFile ??= join(
cacheDir,
`tsbuildinfo/${entryPoint.data.entryPoint.flatModuleFile}.tsbuildinfo`,
);
}

const emittedFiles = new Set<string>();
const tsCompilerHost = cacheCompilerHost(
graph,
entryPoint,
tsConfigOptions,
moduleResolutionCache,
emittedFiles,
stylesheetProcessor,
inlineStyleLanguage,
);

const cache = entryPoint.cache;
const sourceFileCache = cache.sourcesFileCache;
let usingBuildInfo = false;

let oldBuilder = cache.oldBuilder;
if (!oldBuilder && cacheDir) {
oldBuilder = ts.readBuilderProgram(tsConfigOptions, tsCompilerHost);
usingBuildInfo = true;
}

// Create the Angular specific program that contains the Angular compiler
const angularProgram = new NgtscProgram(tsConfig.rootNames, tsConfigOptions, tsCompilerHost, cache.oldNgtscProgram);
Expand All @@ -46,11 +66,11 @@ export async function compileSourceFiles(
augmentProgramWithVersioning(typeScriptProgram);

let builder: ts.BuilderProgram | ts.EmitAndSemanticDiagnosticsBuilderProgram;
if (watch) {
if (watch || cacheDir) {
builder = cache.oldBuilder = ts.createEmitAndSemanticDiagnosticsBuilderProgram(
typeScriptProgram,
tsCompilerHost,
cache.oldBuilder,
oldBuilder,
);
cache.oldNgtscProgram = angularProgram;
} else {
Expand Down Expand Up @@ -93,6 +113,24 @@ export async function compileSourceFiles(

affectedFiles.add(result.affected as ts.SourceFile);
}

// Add all files with associated template type checking files.
// Stored TS build info does not have knowledge of the AOT compiler or the typechecking state of the templates.
// To ensure that errors are reported correctly, all AOT component diagnostics need to be analyzed even if build
// info is present.
if (usingBuildInfo) {
for (const sourceFile of builder.getSourceFiles()) {
if (ignoreForDiagnostics.has(sourceFile) && sourceFile.fileName.endsWith('.ngtypecheck.ts')) {
// This file name conversion relies on internal compiler logic and should be converted
// to an official method when available. 15 is length of `.ngtypecheck.ts`
const originalFilename = sourceFile.fileName.slice(0, -15) + '.ts';
const originalSourceFile = builder.getSourceFile(originalFilename);
if (originalSourceFile) {
affectedFiles.add(originalSourceFile);
}
}
}
}
}

// Collect program level diagnostics
Expand All @@ -108,39 +146,32 @@ export async function compileSourceFiles(

// Collect source file specific diagnostics
for (const sourceFile of builder.getSourceFiles()) {
if (!ignoreForDiagnostics.has(sourceFile)) {
allDiagnostics.push(
...builder.getDeclarationDiagnostics(sourceFile),
...builder.getSyntacticDiagnostics(sourceFile),
...builder.getSemanticDiagnostics(sourceFile),
);
if (ignoreForDiagnostics.has(sourceFile)) {
continue;
}

allDiagnostics.push(
...builder.getDeclarationDiagnostics(sourceFile),
...builder.getSyntacticDiagnostics(sourceFile),
...builder.getSemanticDiagnostics(sourceFile),
);

// Declaration files cannot have template diagnostics
if (sourceFile.isDeclarationFile) {
continue;
}

// Collect sources that are required to be emitted
if (!ignoreForEmit.has(sourceFile) && !angularCompiler.incrementalCompilation.safeToSkipEmit(sourceFile)) {
// If required to emit, diagnostics may have also changed
if (!ignoreForDiagnostics.has(sourceFile)) {
affectedFiles.add(sourceFile);
}
} else if (sourceFileCache && !affectedFiles.has(sourceFile) && !ignoreForDiagnostics.has(sourceFile)) {
// Use cached Angular diagnostics for unchanged and unaffected files
const angularDiagnostics = sourceFileCache.getAngularDiagnostics(sourceFile);
if (angularDiagnostics?.length) {
allDiagnostics.push(...angularDiagnostics);
}
}
}

// Collect new Angular diagnostics for files affected by changes
for (const affectedFile of affectedFiles) {
const angularDiagnostics = angularCompiler.getDiagnosticsForFile(affectedFile, /** OptimizeFor.WholeProgram */ 1);
// Only request Angular template diagnostics for affected files to avoid
// overhead of template diagnostics for unchanged files.
if (affectedFiles.has(sourceFile)) {
const angularDiagnostics = angularCompiler.getDiagnosticsForFile(
sourceFile,
affectedFiles.size === 1 ? /** OptimizeFor.SingleFile **/ 0 : /** OptimizeFor.WholeProgram */ 1,
);

allDiagnostics.push(...angularDiagnostics);
sourceFileCache.updateAngularDiagnostics(affectedFile, angularDiagnostics);
allDiagnostics.push(...angularDiagnostics);
sourceFileCache.updateAngularDiagnostics(sourceFile, angularDiagnostics);
}
}

const otherDiagnostics = [];
Expand All @@ -157,14 +188,33 @@ export async function compileSourceFiles(
log.msg(formatDiagnostics(errorDiagnostics));
}

const transformers = angularCompiler.prepareEmit().transformers;
if ('getSemanticDiagnosticsOfNextAffectedFile' in builder) {
// TypeScript will loop until there are no more affected files in the program
while (builder.emitNextAffectedFile(undefined, undefined, undefined, transformers)) {
// empty
}
}

if (errorDiagnostics.length) {
throw new Error(formatDiagnostics(errorDiagnostics));
}

const transformers = angularCompiler.prepareEmit().transformers;
for (const sourceFile of builder.getSourceFiles()) {
if (!ignoreForEmit.has(sourceFile)) {
builder.emit(sourceFile, undefined, undefined, undefined, transformers);
if (ignoreForEmit.has(sourceFile)) {
continue;
}

if (emittedFiles.has(sourceFile.fileName)) {
angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
continue;
}

if (angularCompiler.incrementalCompilation.safeToSkipEmit(sourceFile)) {
continue;
}

builder.emit(sourceFile, undefined, undefined, undefined, transformers);
angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
}
}
20 changes: 20 additions & 0 deletions src/lib/ts/cache-compiler-host.ts
Expand Up @@ -2,6 +2,7 @@ import type { CompilerHost, CompilerOptions } from '@angular/compiler-cli';
import convertSourceMap from 'convert-source-map';

import { createHash } from 'crypto';
import assert from 'node:assert';
import * as path from 'path';
import ts from 'typescript';
import { NgPackageConfig } from '../../ng-package.schema';
Expand All @@ -17,6 +18,7 @@ export function cacheCompilerHost(
entryPoint: EntryPointNode,
compilerOptions: CompilerOptions,
moduleResolutionCache: ts.ModuleResolutionCache,
emittedFiles?: Set<string>,
stylesheetProcessor?: StylesheetProcessor,
inlineStyleLanguage?: NgPackageConfig['inlineStyleLanguage'],
sourcesFileCache: FileCache = entryPoint.cache.sourcesFileCache,
Expand Down Expand Up @@ -75,6 +77,19 @@ export function cacheCompilerHost(
onError?: (message: string) => void,
sourceFiles?: ReadonlyArray<ts.SourceFile>,
) => {
if (fileName.includes('.ngtypecheck.')) {
return;
}

if (!sourceFiles?.length && fileName.endsWith('.tsbuildinfo')) {
// Save builder info contents to specified location
compilerHost.writeFile.call(this, fileName, data, writeByteOrderMark, onError, sourceFiles);

return;
}

assert(sourceFiles?.length === 1, 'Invalid TypeScript program emit for ' + fileName);

if (fileName.endsWith('.d.ts')) {
if (fileName === flatModuleFileDtsPath) {
if (hasIndexEntryFile) {
Expand All @@ -89,6 +104,7 @@ export function cacheCompilerHost(
}

sourceFiles.forEach(source => {
emittedFiles?.add(source.fileName);
const cache = sourcesFileCache.getOrCreate(source.fileName);
if (!cache.declarationFileName) {
cache.declarationFileName = ensureUnixPath(fileName);
Expand All @@ -111,6 +127,10 @@ export function cacheCompilerHost(
version,
map,
});

sourceFiles.forEach(source => {
emittedFiles?.add(source.fileName);
});
}

compilerHost.writeFile.call(this, fileName, data, writeByteOrderMark, onError, sourceFiles);
Expand Down

0 comments on commit d3b9488

Please sign in to comment.