feat(compiler-cli): implement NgTscPlugin on top of the NgCompiler API (#34792)
This commit implements an experimental integration with tsc_wrapped, where it can load the Angular compiler as a plugin and perform Angular transpilation at a user's request. This is an alternative to the current ngc_wrapped mechanism, which is a fork of tsc_wrapped from several years ago. tsc_wrapped has improved significantly since then, and this feature will allow Angular to benefit from those improvements. Currently the plugin API between tsc_wrapped and the Angular compiler is a work in progress, so NgTscPlugin does not yet implement any interfaces from @bazel/typescript (the home of tsc_wrapped). Instead, an interface is defined locally to guide this standardization. PR Close #34792
This commit is contained in:
parent
14aa6d090e
commit
3c69442dbd
|
@ -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
|
||||
|
||||
|
|
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
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<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) => string[];
|
||||
|
||||
readFile(fileName: string): string|undefined { return this.delegate.readFile(fileName); }
|
||||
|
||||
trace(s: string): void { console.error(s); }
|
||||
}
|
|
@ -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<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;
|
||||
// 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<UnifiedModulesHost> {
|
||||
readonly inputFiles: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<UnifiedModulesHost>, inputFiles: ReadonlyArray<string>,
|
||||
options: ts.CompilerOptions): PluginCompilerHost;
|
||||
|
||||
setupCompilation(program: ts.Program, oldProgram?: ts.Program): {
|
||||
ignoreForDiagnostics: Set<ts.SourceFile>,
|
||||
ignoreForEmit: Set<ts.SourceFile>,
|
||||
};
|
||||
|
||||
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<ts.SourceFile>,
|
||||
ignoreForEmit: Set<ts.SourceFile>,
|
||||
} {
|
||||
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<ts.TransformerFactory<ts.SourceFile|ts.Bundle>> =
|
||||
[(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(); }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue