Wire up Angular 9 templates as a ts_library plugin
PiperOrigin-RevId: 297183761
diff --git a/internal/common/compilation.bzl b/internal/common/compilation.bzl
index a514e9e..8e36bfa 100644
--- a/internal/common/compilation.bzl
+++ b/internal/common/compilation.bzl
@@ -156,7 +156,7 @@
# Temporary until all imports of ngfactory/ngsummary files are removed
# TODO(alexeagle): clean up after Ivy launch
- if getattr(ctx, "compile_angular_templates", False):
+ if getattr(ctx.attr, "use_angular_plugin", False):
closure_js_files += [ctx.actions.declare_file(basename + ".ngfactory.mjs")]
closure_js_files += [ctx.actions.declare_file(basename + ".ngsummary.mjs")]
@@ -166,9 +166,11 @@
# Temporary until all imports of ngfactory/ngsummary files are removed
# TODO(alexeagle): clean up after Ivy launch
- if getattr(ctx, "compile_angular_templates", False):
+ if getattr(ctx.attr, "use_angular_plugin", False):
devmode_js_files += [ctx.actions.declare_file(basename + ".ngfactory.js")]
devmode_js_files += [ctx.actions.declare_file(basename + ".ngsummary.js")]
+ declaration_files += [ctx.actions.declare_file(basename + ".ngfactory.d.ts")]
+ declaration_files += [ctx.actions.declare_file(basename + ".ngsummary.d.ts")]
return struct(
closure_js = closure_js_files,
devmode_js = devmode_js_files,
@@ -336,7 +338,7 @@
replay_params = None
if has_sources:
- inputs = compilation_inputs + [tsconfig]
+ inputs = compilation_inputs + [tsconfig] + getattr(ctx.files, "angular_assets", [])
replay_params = compile_action(
ctx,
inputs,
@@ -382,7 +384,7 @@
))
devmode_compile_action(
ctx,
- compilation_inputs + [tsconfig_json_es5],
+ compilation_inputs + [tsconfig_json_es5] + getattr(ctx.files, "angular_assets", []),
outputs,
tsconfig_json_es5,
node_profile_args,
diff --git a/internal/common/tsconfig.bzl b/internal/common/tsconfig.bzl
index 14b4df5..d30b1c5 100644
--- a/internal/common/tsconfig.bzl
+++ b/internal/common/tsconfig.bzl
@@ -159,8 +159,18 @@
"expectedDiagnostics": getattr(ctx.attr, "expected_diagnostics", []),
}
- if hasattr(ctx.attr, "compile_angular_templates") and ctx.attr.compile_angular_templates:
- bazel_options["compileAngularTemplates"] = True
+ if getattr(ctx.attr, "use_angular_plugin", False):
+ bazel_options["angularCompilerOptions"] = {
+ # Needed for back-compat with explicit AOT bootstrap
+ # which has imports from generated .ngfactory files
+ "generateNgFactoryShims": True,
+ # Needed for back-compat with AOT tests which import the
+ # .ngsummary files
+ "generateNgSummaryShims": True,
+ # Bazel expects output files will always be produced
+ "allowEmptyCodegenFiles": True,
+ "assets": [a.path for a in getattr(ctx.files, "angular_assets", [])],
+ }
if disable_strict_deps:
bazel_options["disableStrictDeps"] = disable_strict_deps
diff --git a/internal/tsc_wrapped/plugin_api.ts b/internal/tsc_wrapped/plugin_api.ts
index e09954d..2bcfa21 100644
--- a/internal/tsc_wrapped/plugin_api.ts
+++ b/internal/tsc_wrapped/plugin_api.ts
@@ -17,8 +17,8 @@
/**
* @fileoverview
- * Provides APIs for extending TypeScript.
- * Based on the LanguageService plugin API in TS 2.3
+ * Provides APIs for extending TypeScript command-line compiler.
+ * It's roughly analogous to how the Language Service allows plugins.
*/
import * as ts from 'typescript';
@@ -29,13 +29,6 @@
* In vanilla tsc, these are the ts.ParsedCommandLine#fileNames
*/
inputFiles: ReadonlyArray<string>;
-
- /**
- * A helper the transformer can use when generating new import statements
- * @param fileName the absolute path to the file as referenced in the ts.Program
- * @return a string suitable for use in an import statement
- */
- fileNameToModuleId: (fileName: string) => string;
}
/**
@@ -48,10 +41,10 @@
*
* The methods on the plugin will be called in the order shown below:
* - wrapHost to intercept CompilerHost methods and contribute inputFiles to the program
- * - wrap to intercept diagnostics requests on the program
+ * - setupCompilation to capture the ts.Program
* - createTransformers once it's time to emit
*/
-export interface TscPlugin {
+export interface EmitPlugin {
/**
* Allow plugins to add additional files to the program.
* For example, Angular creates ngsummary and ngfactory files.
@@ -59,27 +52,19 @@
* @param inputFiles the files that were part of the original program
* @param compilerHost: the original host (likely a ts.CompilerHost) that we can delegate to
*/
- wrapHost?(inputFiles: string[], compilerHost: PluginCompilerHost): PluginCompilerHost;
+ wrapHost?(compilerHost: ts.CompilerHost, inputFiles: string[], options: ts.CompilerOptions): PluginCompilerHost;
- /**
- * Same API as ts.LanguageService: allow the plugin to contribute additional
- * diagnostics
- * IMPORTANT: plugins must propagate the diagnostics from the original program.
- * Execution of plugins is not additive; only the result from the top-most
- * wrapped Program is used.
- */
- wrap(p: ts.Program, config?: {}, host?: ts.CompilerHost): ts.Program;
+ setupCompilation(program: ts.Program, oldProgram?: ts.Program): void;
+ getNextProgram?(): ts.Program;
+
/**
* Allow plugins to contribute additional TypeScript CustomTransformers.
* These can modify the TS AST, JS AST, or .d.ts output AST.
*/
- createTransformers?(host: PluginCompilerHost): ts.CustomTransformers;
+ createTransformers(): ts.CustomTransformers;
}
-// TODO(alexeagle): this should be unioned with tsserverlibrary.PluginModule
-export type Plugin = TscPlugin;
-
/**
* The proxy design pattern, allowing us to customize behavior of the delegate
* object.
diff --git a/internal/tsc_wrapped/tsc_wrapped.ts b/internal/tsc_wrapped/tsc_wrapped.ts
index 077a948..71ca1ee 100644
--- a/internal/tsc_wrapped/tsc_wrapped.ts
+++ b/internal/tsc_wrapped/tsc_wrapped.ts
@@ -10,7 +10,7 @@
import * as bazelDiagnostics from './diagnostics';
import {constructManifest} from './manifest';
import * as perfTrace from './perf_trace';
-import {DiagnosticPlugin, PluginCompilerHost, TscPlugin} from './plugin_api';
+import {DiagnosticPlugin, PluginCompilerHost, EmitPlugin} from './plugin_api';
import {Plugin as StrictDepsPlugin} from './strict_deps';
import {BazelOptions, parseTsconfig, resolveNormalizedPath} from './tsconfig';
import {debug, log, runAsWorker, runWorkerLoop} from './worker';
@@ -62,11 +62,8 @@
*/
export function gatherDiagnostics(
options: ts.CompilerOptions, bazelOpts: BazelOptions, program: ts.Program,
- disabledTsetseRules: string[], angularPlugin?: TscPlugin,
+ disabledTsetseRules: string[],
plugins: DiagnosticPlugin[] = []): ts.Diagnostic[] {
- if (angularPlugin) {
- program = angularPlugin.wrap(program);
- }
const diagnostics: ts.Diagnostic[] = [];
perfTrace.wrap('type checking', () => {
@@ -218,13 +215,7 @@
throw new Error(
'Impossible state: if parseTsconfig returns no errors, then parsed should be non-null');
}
- const {
- options,
- bazelOpts,
- files,
- disabledTsetseRules,
- angularCompilerOptions
- } = parsed;
+ const {options, bazelOpts, files, disabledTsetseRules} = parsed;
let sourceFiles: string[] = [];
if (bazelOpts.isJsTranspilation) {
@@ -259,8 +250,7 @@
const perfTracePath = bazelOpts.perfTracePath;
if (!perfTracePath) {
const {diagnostics} = createProgramAndEmit(
- fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules,
- angularCompilerOptions);
+ fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules);
if (diagnostics.length > 0) {
console.error(bazelDiagnostics.format(bazelOpts.target, diagnostics));
return false;
@@ -271,8 +261,7 @@
log('Writing trace to', perfTracePath);
const success = perfTrace.wrap('runOneBuild', () => {
const {diagnostics} = createProgramAndEmit(
- fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules,
- angularCompilerOptions);
+ fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules);
if (diagnostics.length > 0) {
console.error(bazelDiagnostics.format(bazelOpts.target, diagnostics));
return false;
@@ -321,8 +310,7 @@
*/
export function createProgramAndEmit(
fileLoader: FileLoader, options: ts.CompilerOptions,
- bazelOpts: BazelOptions, files: string[], disabledTsetseRules: string[],
- angularCompilerOptions?: {[key: string]: unknown}):
+ bazelOpts: BazelOptions, files: string[], disabledTsetseRules: string[]):
{program?: ts.Program, diagnostics: ts.Diagnostic[]} {
// Beware! createProgramAndEmit must not print to console, nor exit etc.
// Handle errors by reporting and returning diagnostics.
@@ -336,44 +324,52 @@
const moduleResolver = bazelOpts.isJsTranspilation ?
makeJsModuleResolver(bazelOpts.workspaceName) :
ts.resolveModuleName;
+
+ // Files which should be allowed to be read, but aren't TypeScript code
+ const assets: string[] = [];
+ if (bazelOpts.angularCompilerOptions) {
+ if (bazelOpts.angularCompilerOptions.assets) {
+ assets.push(...bazelOpts.angularCompilerOptions.assets);
+ }
+ }
+
const tsickleCompilerHost = new CompilerHost(
- files, options, bazelOpts, compilerHostDelegate, fileLoader,
+ [...files, ...assets], options, bazelOpts, compilerHostDelegate, fileLoader,
moduleResolver);
let compilerHost: PluginCompilerHost = tsickleCompilerHost;
const diagnosticPlugins: DiagnosticPlugin[] = [];
- let angularPlugin: TscPlugin|undefined;
- if (bazelOpts.compileAngularTemplates) {
+ let angularPlugin: EmitPlugin&DiagnosticPlugin|undefined;
+ if (bazelOpts.angularCompilerOptions) {
try {
- const ngOptions = angularCompilerOptions || {};
+ const ngOptions = bazelOpts.angularCompilerOptions;
// Add the rootDir setting to the options passed to NgTscPlugin.
// Required so that synthetic files added to the rootFiles in the program
// can be given absolute paths, just as we do in tsconfig.ts, matching
// the behavior in TypeScript's tsconfig parsing logic.
ngOptions['rootDir'] = options.rootDir;
- // Dynamically load the Angular compiler installed as a peerDep
- const ngtsc = require('@angular/compiler-cli');
+ let angularPluginEntryPoint = '@angular/compiler-cli';
- // TODO(alexeagle): re-enable after Angular API changes land
- // See https://github.com/angular/angular/pull/34792
- // and pending CL/289493608
- // By commenting this out, we allow Angular caretaker to sync changes from
- // GitHub without having to coordinate any Piper patches in the same CL.
- // angularPlugin = new ngtsc.NgTscPlugin(ngOptions);
+ // Dynamically load the Angular compiler.
+ // Lazy load, so that code that does not use the plugin doesn't even
+ // have to spend the time to parse and load the plugin's source.
+ //
+ // tslint:disable-next-line:no-require-imports
+ const ngtsc = require(angularPluginEntryPoint);
+ angularPlugin = new ngtsc.NgTscPlugin(ngOptions);
+ diagnosticPlugins.push(angularPlugin!);
} catch (e) {
return {
diagnostics: [errorDiag(
- 'when using `ts_library(compile_angular_templates=True)`, ' +
+ 'when using `ts_library(use_angular_plugin=True)`, ' +
`you must install @angular/compiler-cli (was: ${e})`)]
};
}
- // Wrap host only needed until after Ivy cleanup
- // TODO(alexeagle): remove after ngsummary and ngfactory files eliminated
- if (angularPlugin) {
- compilerHost = angularPlugin.wrapHost!(files, compilerHost);
- }
+ // Wrap host so that Ivy compiler can add a file to it (has synthetic types for checking templates)
+ // TODO(arick): remove after ngsummary and ngfactory files eliminated
+ compilerHost = angularPlugin!.wrapHost!(compilerHost, files, options);
}
@@ -384,6 +380,16 @@
compilerHost.inputFiles, options, compilerHost, oldProgram));
cache.putProgram(bazelOpts.target, program);
+ let transformers: ts.CustomTransformers = {
+ before: [],
+ after: [],
+ afterDeclarations: [],
+ };
+ if (angularPlugin) {
+ angularPlugin.setupCompilation(program);
+ transformers = angularPlugin.createTransformers();
+ }
+
for (const pluginConfig of options['plugins'] as ts.PluginImport[] || []) {
if (pluginConfig.name === 'ts-lit-plugin') {
const litTscPlugin =
@@ -402,8 +408,7 @@
// messages refer to the original source. After any subsequent passes
// (decorator downleveling or tsickle) we do not type check.
let diagnostics = gatherDiagnostics(
- options, bazelOpts, program, disabledTsetseRules, angularPlugin,
- diagnosticPlugins);
+ options, bazelOpts, program, disabledTsetseRules, diagnosticPlugins);
if (!expectDiagnosticsWhitelist.length ||
expectDiagnosticsWhitelist.some(p => bazelOpts.target.startsWith(p))) {
diagnostics = bazelDiagnostics.filterExpected(
@@ -415,33 +420,43 @@
'expected_diagnostics, but got ' + bazelOpts.target));
}
+ // The Angular plugin creates a new program with template type-check information
+ // This consumes (destroys) the old program so it's not suitable for re-use anymore
+ // Ask Angular to give us the updated reusable program.
+ if (angularPlugin) {
+ cache.putProgram(bazelOpts.target, angularPlugin.getNextProgram!());
+ }
+
if (diagnostics.length > 0) {
debug('compilation failed at', new Error().stack!);
return {program, diagnostics};
}
}
+ // Angular might have added files like input.ngfactory.ts or input.ngsummary.ts
+ // and these need to be emitted.
+ // TODO(arick): remove after Ivy is enabled and ngsummary/ngfactory files no longer needed
+ function isAngularFile(sf: ts.SourceFile) {
+ if (!/\.ng(factory|summary)\.ts$/.test(sf.fileName)) {
+ return false;
+ }
+ return isCompilationTarget(bazelOpts, {
+ fileName: sf.fileName.slice(0, /*'.ngfactory|ngsummary.ts'.length*/ -13) + '.ts'
+ } as ts.SourceFile);
+ }
+
const compilationTargets = program.getSourceFiles().filter(
- fileName => isCompilationTarget(bazelOpts, fileName));
+ sf => isCompilationTarget(bazelOpts, sf) || isAngularFile(sf));
let diagnostics: ts.Diagnostic[] = [];
let useTsickleEmit = bazelOpts.tsickle;
- let transforms: ts.CustomTransformers = {
- before: [],
- after: [],
- afterDeclarations: [],
- };
-
- if (angularPlugin) {
- transforms = angularPlugin.createTransformers!(compilerHost);
- }
if (useTsickleEmit) {
diagnostics = emitWithTsickle(
program, tsickleCompilerHost, compilationTargets, options, bazelOpts,
- transforms);
+ transformers);
} else {
- diagnostics = emitWithTypescript(program, compilationTargets, transforms);
+ diagnostics = emitWithTypescript(program, compilationTargets, transformers);
}
if (diagnostics.length > 0) {
diff --git a/internal/tsc_wrapped/tsconfig.ts b/internal/tsc_wrapped/tsconfig.ts
index 13851ca..be964eb 100644
--- a/internal/tsc_wrapped/tsconfig.ts
+++ b/internal/tsc_wrapped/tsconfig.ts
@@ -179,9 +179,17 @@
hasImplementation?: boolean;
/**
- * Enable the Angular ngtsc plugin.
+ * If present, run the Angular ngtsc plugin with the given options.
*/
- compileAngularTemplates?: boolean;
+ angularCompilerOptions?: {
+ [k: string]: any,
+ assets: string[],
+ // Ideally we would
+ // import {AngularCompilerOptions} from '@angular/compiler-cli';
+ // and the type would be AngularCompilerOptions&{assets: string[]};
+ // but we don't want a dependency from @bazel/typescript to @angular/compiler-cli
+ // as it's conceptually cyclical.
+ };
/**
* Override for ECMAScript target language level to use for devmode.
@@ -372,6 +380,10 @@
bazelOpts.nodeModulesPrefix =
resolveNormalizedPath(options.rootDir!, bazelOpts.nodeModulesPrefix);
}
+ if (bazelOpts.angularCompilerOptions && bazelOpts.angularCompilerOptions.assets) {
+ bazelOpts.angularCompilerOptions.assets = bazelOpts.angularCompilerOptions.assets.map(
+ f => resolveNormalizedPath(options.rootDir!, f));
+ }
let disabledTsetseRules: string[] = [];
for (const pluginConfig of options['plugins'] as PluginImportWithConfig[] ||