Skip to content
Permalink
Browse files

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
  • Loading branch information...
alexeagle authored and jasonaden committed Jan 29, 2019
1 parent 0eef958 commit a227c528ca21f5d519f4126aaaa0cb6e641699d4
@@ -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",
@@ -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';
@@ -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'),

@@ -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); }
}
@@ -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),
};
}
}
@@ -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"

0 comments on commit a227c52

Please sign in to comment.
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.