feat(compiler-cli): expose ngtsc as a TscPlugin (#28435)

This lets us run ngtsc under the tsc_wrapped custom compiler (Used in Bazel)
It also allows others to simply wire ngtsc into an existing typescript compilation binary

PR Close #28435
This commit is contained in:
Alex Eagle 2019-01-29 11:33:37 -08:00 committed by Jason Aden
parent 0eef958735
commit a227c528ca
6 changed files with 182 additions and 5 deletions

View File

@ -36,7 +36,7 @@
"@angular-devkit/core": "^7.0.4",
"@angular-devkit/schematics": "^7.3.0-rc.0",
"@bazel/karma": "~0.22.1",
"@bazel/typescript": "~0.22.1",
"@bazel/typescript": "0.22.1-7-g68fed6a",
"@schematics/angular": "^7.0.4",
"@types/angular": "^1.6.47",
"@types/chokidar": "1.7.3",

View File

@ -24,3 +24,4 @@ export {CompilerOptions as AngularCompilerOptions} from './src/transformers/api'
export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api';
export {ngToTsDiagnostic} from './src/transformers/util';
export {NgTscPlugin} from './src/ngtsc/tsc_plugin';

View File

@ -36,6 +36,8 @@ const requiredNodeModules = {
'@angular/platform-server':
resolveNpmTreeArtifact('angular/packages/platform-server/npm_package'),
'@angular/router': resolveNpmTreeArtifact('angular/packages/router/npm_package'),
// Note, @bazel/typescript does not appear here because it's not listed as a dependency of
// @angular/compiler-cli
'@types/jasmine': resolveNpmTreeArtifact('ngdeps/node_modules/@types/jasmine'),
'@types/node': resolveNpmTreeArtifact('ngdeps/node_modules/@types/node'),

View File

@ -0,0 +1,96 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {PluginCompilerHost} from '@bazel/typescript/tsc_wrapped/plugin_api';
import * as ts from 'typescript';
/**
* Extension of the TypeScript compiler host that supports files added to the Program which
* were never on disk.
*
* This is used for backwards-compatibility with the ViewEngine compiler, which used ngsummary
* and ngfactory files as inputs to the program. We call these inputs "synthetic".
*
* They need to be program inputs because user code may import from these generated files.
*
* TODO(alxhub): remove this after all ng_module users have migrated to Ivy
*/
export class SyntheticFilesCompilerHost implements PluginCompilerHost {
/**
* SourceFiles which are added to the program but which never existed on disk.
*/
syntheticFiles = new Map<string, ts.SourceFile>();
constructor(
private rootFiles: string[], private delegate: ts.CompilerHost,
generatedFiles: (rootFiles: string[]) => {
[fileName: string]: (host: ts.CompilerHost) => ts.SourceFile | undefined
}, ) {
// Allow ngtsc to contribute in-memory synthetic files, which will be loaded
// as if they existed on disk as action inputs.
const angularGeneratedFiles = generatedFiles !(rootFiles);
for (const f of Object.keys(angularGeneratedFiles)) {
const generator = angularGeneratedFiles[f];
const generated = generator(delegate);
if (generated) {
this.syntheticFiles.set(generated.fileName, generated);
}
}
}
fileExists(filePath: string): boolean {
if (this.syntheticFiles.has(filePath)) {
return true;
}
return this.delegate.fileExists(filePath);
}
/** Loads a source file from in-memory map, or delegates. */
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: (message: string) => void): ts.SourceFile|undefined {
const syntheticFile = this.syntheticFiles.get(fileName);
if (syntheticFile) {
return syntheticFile !;
}
return this.delegate.getSourceFile(fileName, languageVersion, onError);
}
get inputFiles() { return [...this.rootFiles, ...Array.from(this.syntheticFiles.keys())]; }
fileNameToModuleId(fileName: string) {
return fileName; // TODO: Ivy logic. don't forget that the delegate has the google3 logic
}
// Delegate everything else to the original compiler host.
getDefaultLibFileName(options: ts.CompilerOptions): string {
return this.delegate.getDefaultLibFileName(options);
}
writeFile(
fileName: string, content: string, writeByteOrderMark: boolean,
onError: ((message: string) => void)|undefined,
sourceFiles: ReadonlyArray<ts.SourceFile>|undefined): void {
this.delegate.writeFile(fileName, content, writeByteOrderMark, onError, sourceFiles);
}
getCanonicalFileName(path: string) { return this.delegate.getCanonicalFileName(path); }
getCurrentDirectory(): string { return this.delegate.getCurrentDirectory(); }
useCaseSensitiveFileNames(): boolean { return this.delegate.useCaseSensitiveFileNames(); }
getNewLine(): string { return this.delegate.getNewLine(); }
getDirectories(path: string) { return this.delegate.getDirectories(path); }
readFile(fileName: string): string|undefined { return this.delegate.readFile(fileName); }
trace(s: string): void { console.error(s); }
}

View File

@ -0,0 +1,78 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {PluginCompilerHost, TscPlugin} from '@bazel/typescript/tsc_wrapped/plugin_api';
import * as ts from 'typescript';
import {SyntheticFilesCompilerHost} from './synthetic_files_compiler_host';
// Copied from tsc_wrapped/plugin_api.ts to avoid a runtime dependency on that package
function createProxy<T>(delegate: T): T {
const proxy = Object.create(null);
for (const k of Object.keys(delegate)) {
proxy[k] = function() { return (delegate as any)[k].apply(delegate, arguments); };
}
return proxy;
}
export class NgTscPlugin implements TscPlugin {
constructor(private angularCompilerOptions: unknown) {}
wrap(program: ts.Program, config: {}, host: ts.CompilerHost) {
const proxy = createProxy(program);
proxy.getSemanticDiagnostics = (sourceFile: ts.SourceFile) => {
const result: ts.Diagnostic[] = [...program.getSemanticDiagnostics(sourceFile)];
// For demo purposes, trigger a diagnostic when the sourcefile has a magic string
if (sourceFile.text.indexOf('diag') >= 0) {
const fake: ts.Diagnostic = {
file: sourceFile,
start: 0,
length: 3,
messageText: 'Example Angular Compiler Diagnostic',
category: ts.DiagnosticCategory.Error,
code: 12345,
// source is the name of the plugin.
source: 'Angular',
};
result.push(fake);
}
return result;
};
return proxy;
}
createTransformers(host: PluginCompilerHost) {
const afterDeclarations: Array<ts.TransformerFactory<ts.SourceFile|ts.Bundle>> =
[(context: ts.TransformationContext) => (sf: ts.SourceFile | ts.Bundle) => {
const visitor = (node: ts.Node): ts.Node => {
if (node.kind === ts.SyntaxKind.ClassDeclaration) {
const clz = node as ts.ClassDeclaration;
// For demo purposes, transform the class name in the .d.ts output
return ts.updateClassDeclaration(
clz, clz.decorators, node.modifiers, ts.createIdentifier('NEWNAME'),
clz.typeParameters, clz.heritageClauses, clz.members);
}
return ts.visitEachChild(node, visitor, context);
};
return visitor(sf) as ts.SourceFile;
}];
return {afterDeclarations};
}
wrapHost(inputFiles: string[], compilerHost: ts.CompilerHost) {
return new SyntheticFilesCompilerHost(inputFiles, compilerHost, this.generatedFiles);
}
generatedFiles(rootFiles: string[]) {
return {
'file-1.ts': (host: ts.CompilerHost) =>
ts.createSourceFile('file-1.ts', 'contents', ts.ScriptTarget.ES5),
};
}
}

View File

@ -160,10 +160,10 @@
semver "5.6.0"
tmp "0.0.33"
"@bazel/typescript@~0.22.1":
version "0.22.1"
resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-0.22.1.tgz#b52c00e8560019e2f9d273d45c04785e0ec9d9bd"
integrity sha512-88DaCCnNg8rPlKP0eAQEZuoiJkEPeiItpUS3oBR1sFQNBRJb56D25ahK8+N6LJk4qaH+ZQ1/AHOPDhfEEWvDzA==
"@bazel/typescript@0.22.1-7-g68fed6a":
version "0.22.1-7-g68fed6a"
resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-0.22.1-7-g68fed6a.tgz#d6340fbfcfdeb3893e66ec8a435b850cfa83d60f"
integrity sha512-EBPxbge/RBas7zSLvCjUgtpIVdjU+AgZ6YyCMTaYcn4hzD1eYQUpGHT+fa9/icHvk/84wWGXCFRb1+uZsBofVA==
dependencies:
protobufjs "5.0.3"
semver "5.6.0"