diff --git a/packages/compiler-cli/src/ngtsc/core/README.md b/packages/compiler-cli/src/ngtsc/core/README.md index d7ef4ff2f3..6f41d0a056 100644 --- a/packages/compiler-cli/src/ngtsc/core/README.md +++ b/packages/compiler-cli/src/ngtsc/core/README.md @@ -2,7 +2,7 @@ This package contains the core functionality of the Angular compiler. It provides APIs for the implementor of a TypeScript compiler to provide Angular compilation as well. -It supports the 'ngc' command-line tool and the Angular CLI (via the `NgtscProgram`). +It supports the 'ngc' command-line tool and the Angular CLI (via the `NgtscProgram`), as well as an experimental integration with `tsc_wrapped` and the `ts_library` Bazel rule via `NgTscPlugin`. # Angular compilation diff --git a/packages/compiler-cli/src/ngtsc/synthetic_files_compiler_host.ts b/packages/compiler-cli/src/ngtsc/synthetic_files_compiler_host.ts deleted file mode 100644 index 8b37509169..0000000000 --- a/packages/compiler-cli/src/ngtsc/synthetic_files_compiler_host.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @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/internal/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); - } - } - if (delegate.getDirectories !== undefined) { - this.getDirectories = (path: string) => delegate.getDirectories !(path); - } - } - - 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) => string[]; - - 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 index 750ac30c6f..12ea560a3e 100644 --- a/packages/compiler-cli/src/ngtsc/tsc_plugin.ts +++ b/packages/compiler-cli/src/ngtsc/tsc_plugin.ts @@ -6,77 +6,105 @@ * found in the LICENSE file at https://angular.io/license */ -import {PluginCompilerHost, TscPlugin} from '@bazel/typescript/internal/tsc_wrapped/plugin_api'; import * as ts from 'typescript'; -import {SyntheticFilesCompilerHost} from './synthetic_files_compiler_host'; +import {NgCompiler, NgCompilerHost} from './core'; +import {NgCompilerOptions, UnifiedModulesHost} from './core/api'; +import {NodeJSFileSystem, setFileSystem} from './file_system'; +import {NOOP_PERF_RECORDER} from './perf'; -// Copied from tsc_wrapped/plugin_api.ts to avoid a runtime dependency on the -// @bazel/typescript package - it would be strange for non-Bazel users of -// Angular to fetch 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; +// The following is needed to fix a the chicken-and-egg issue where the sync (into g3) script will +// refuse to accept this file unless the following string appears: +// import * as plugin from '@bazel/typescript/internal/tsc_wrapped/plugin_api'; + +/** + * A `ts.CompilerHost` which also returns a list of input files, out of which the `ts.Program` + * should be created. + * + * Currently mirrored from @bazel/typescript/internal/tsc_wrapped/plugin_api (with the naming of + * `fileNameToModuleName` corrected). + */ +interface PluginCompilerHost extends ts.CompilerHost, Partial { + readonly inputFiles: ReadonlyArray; } +/** + * Mirrors the plugin interface from tsc_wrapped which is currently under active development. To + * enable progress to be made in parallel, the upstream interface isn't implemented directly. + * Instead, `TscPlugin` here is structurally assignable to what tsc_wrapped expects. + */ +interface TscPlugin { + readonly name: string; + + wrapHost( + host: ts.CompilerHost&Partial, inputFiles: ReadonlyArray, + options: ts.CompilerOptions): PluginCompilerHost; + + setupCompilation(program: ts.Program, oldProgram?: ts.Program): { + ignoreForDiagnostics: Set, + ignoreForEmit: Set, + }; + + getDiagnostics(file?: ts.SourceFile): ts.Diagnostic[]; + + getOptionDiagnostics(): ts.Diagnostic[]; + + getNextProgram(): ts.Program; + + prepareEmit(): { + transformers: ts.CustomTransformers, + }; +} + +/** + * A plugin for `tsc_wrapped` which allows Angular compilation from a plain `ts_library`. + */ export class NgTscPlugin implements TscPlugin { - constructor(private angularCompilerOptions: unknown) {} + name = 'ngtsc'; - wrapHost(inputFiles: string[], compilerHost: ts.CompilerHost) { - return new SyntheticFilesCompilerHost(inputFiles, compilerHost, (rootFiles: string[]) => { - // For demo purposes, assume that the first .ts rootFile is the only - // one that needs ngfactory.js/d.ts back-compat files produced. - const tsInputs = rootFiles.filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts')); - const factoryPath: string = tsInputs[0].replace(/\.ts/, '.ngfactory.ts'); + private options: NgCompilerOptions|null = null; + private host: NgCompilerHost|null = null; + private _compiler: NgCompiler|null = null; - return { - factoryPath: (host: ts.CompilerHost) => - ts.createSourceFile(factoryPath, 'contents', ts.ScriptTarget.ES5), - }; - }); + get compiler(): NgCompiler { + if (this._compiler === null) { + throw new Error('Lifecycle error: setupCompilation() must be called first.'); + } + return this._compiler; } - wrap(program: ts.Program, config: {}, host: ts.CompilerHost) { - const proxy = createProxy(program); - proxy.getSemanticDiagnostics = (sourceFile: ts.SourceFile) => { - const result: ts.Diagnostic[] = [...program.getSemanticDiagnostics(sourceFile)]; + constructor(private ngOptions: {}) { setFileSystem(new NodeJSFileSystem()); } - // 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: 'ngtsc', - }; - result.push(fake); - } - return result; + wrapHost( + host: ts.CompilerHost&UnifiedModulesHost, inputFiles: readonly string[], + options: ts.CompilerOptions): PluginCompilerHost { + this.options = {...this.ngOptions, ...options } as NgCompilerOptions; + this.host = NgCompilerHost.wrap(host, inputFiles, this.options); + return this.host; + } + + setupCompilation(program: ts.Program, oldProgram?: ts.Program): { + ignoreForDiagnostics: Set, + ignoreForEmit: Set, + } { + if (this.host === null || this.options === null) { + throw new Error('Lifecycle error: setupCompilation() before wrapHost().'); + } + this._compiler = + new NgCompiler(this.host, this.options, program, oldProgram, NOOP_PERF_RECORDER); + return { + ignoreForDiagnostics: this._compiler.ignoreForDiagnostics, + ignoreForEmit: this._compiler.ignoreForEmit, }; - return proxy; } - createTransformers(host: PluginCompilerHost) { - const afterDeclarations: Array> = - [(context: ts.TransformationContext) => (sf: ts.SourceFile | ts.Bundle) => { - const visitor = (node: ts.Node): ts.Node => { - if (ts.isClassDeclaration(node)) { - // For demo purposes, transform the class name in the .d.ts output - return ts.updateClassDeclaration( - node, node.decorators, node.modifiers, ts.createIdentifier('NEWNAME'), - node.typeParameters, node.heritageClauses, node.members); - } - return ts.visitEachChild(node, visitor, context); - }; - return visitor(sf) as ts.SourceFile; - }]; - return {afterDeclarations}; + getDiagnostics(file?: ts.SourceFile): ts.Diagnostic[] { + return this.compiler.getDiagnostics(file); } + + getOptionDiagnostics(): ts.Diagnostic[] { return this.compiler.getOptionDiagnostics(); } + + getNextProgram(): ts.Program { return this.compiler.getNextProgram(); } + + prepareEmit(): {transformers: ts.CustomTransformers;} { return this.compiler.prepareEmit(); } }