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[] ||