From a227c528ca21f5d519f4126aaaa0cb6e641699d4 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Tue, 29 Jan 2019 11:33:37 -0800 Subject: [PATCH] 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 --- package.json | 2 +- packages/compiler-cli/index.ts | 1 + .../integrationtest/test_helpers.js | 2 + .../ngtsc/synthetic_files_compiler_host.ts | 96 +++++++++++++++++++ packages/compiler-cli/src/ngtsc/tsc_plugin.ts | 78 +++++++++++++++ yarn.lock | 8 +- 6 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/synthetic_files_compiler_host.ts create mode 100644 packages/compiler-cli/src/ngtsc/tsc_plugin.ts diff --git a/package.json b/package.json index 74e153997b..8cb0fff2ee 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/compiler-cli/index.ts b/packages/compiler-cli/index.ts index c7fe86a9f6..444b421eb3 100644 --- a/packages/compiler-cli/index.ts +++ b/packages/compiler-cli/index.ts @@ -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'; diff --git a/packages/compiler-cli/integrationtest/test_helpers.js b/packages/compiler-cli/integrationtest/test_helpers.js index c638b63c01..c0c87fc3c2 100644 --- a/packages/compiler-cli/integrationtest/test_helpers.js +++ b/packages/compiler-cli/integrationtest/test_helpers.js @@ -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'), diff --git a/packages/compiler-cli/src/ngtsc/synthetic_files_compiler_host.ts b/packages/compiler-cli/src/ngtsc/synthetic_files_compiler_host.ts new file mode 100644 index 0000000000..2a799c4aca --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/synthetic_files_compiler_host.ts @@ -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(); + + 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|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); } +} diff --git a/packages/compiler-cli/src/ngtsc/tsc_plugin.ts b/packages/compiler-cli/src/ngtsc/tsc_plugin.ts new file mode 100644 index 0000000000..3aa127a2b2 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/tsc_plugin.ts @@ -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(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> = + [(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), + }; + } +} diff --git a/yarn.lock b/yarn.lock index ac369fbc85..3807c04a3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"