Call ngtsc when registered as a tsc_wrapped plugin.

PiperOrigin-RevId: 232681550
diff --git a/internal/build_defs.bzl b/internal/build_defs.bzl
index fbf93d4..b300f90 100644
--- a/internal/build_defs.bzl
+++ b/internal/build_defs.bzl
@@ -341,6 +341,9 @@
             """,
             default = Label("@npm//typescript:typescript__typings"),
         ),
+        "compile_angular_templates": attr.bool(
+            doc = """Run the Angular ngtsc compiler under ts_library""",
+        ),
         "supports_workers": attr.bool(
             doc = """Intended for internal use only.
             Allows you to disable the Bazel Worker strategy for this library.
diff --git a/internal/common/tsconfig.bzl b/internal/common/tsconfig.bzl
index 0b1fcda..349dc6f 100644
--- a/internal/common/tsconfig.bzl
+++ b/internal/common/tsconfig.bzl
@@ -155,6 +155,9 @@
         "expectedDiagnostics": getattr(ctx.attr, "expected_diagnostics", []),
     }
 
+    if hasattr(ctx.attr, "compile_angular_templates") and ctx.attr.compile_angular_templates:
+        bazel_options["compileAngularTemplates"] = True
+
     if disable_strict_deps:
         bazel_options["disableStrictDeps"] = disable_strict_deps
         bazel_options["allowedStrictDeps"] = []
diff --git a/internal/tsc_wrapped/tsc_wrapped.ts b/internal/tsc_wrapped/tsc_wrapped.ts
index 983ac78..9aa7473 100644
--- a/internal/tsc_wrapped/tsc_wrapped.ts
+++ b/internal/tsc_wrapped/tsc_wrapped.ts
@@ -10,6 +10,7 @@
 import * as bazelDiagnostics from './diagnostics';
 import {constructManifest} from './manifest';
 import * as perfTrace from './perf_trace';
+import {PluginCompilerHost, TscPlugin} from './plugin_api';
 import {PLUGIN as strictDepsPlugin} from './strict_deps';
 import {BazelOptions, parseTsconfig, resolveNormalizedPath} from './tsconfig';
 import {debug, log, runAsWorker, runWorkerLoop} from './worker';
@@ -47,7 +48,7 @@
  */
 export function gatherDiagnostics(
     options: ts.CompilerOptions, bazelOpts: BazelOptions, program: ts.Program,
-    disabledTsetseRules: string[]): ts.Diagnostic[] {
+    disabledTsetseRules: string[], angularPlugin?: TscPlugin): ts.Diagnostic[] {
   // Install extra diagnostic plugins
   if (!bazelOpts.disableStrictDeps) {
     const ignoredFilesPrefixes: string[] = [];
@@ -73,8 +74,9 @@
     let selectedTsetsePlugin = bazelConformancePlugin;
     program = selectedTsetsePlugin.wrap(program, disabledTsetseRules);
   }
-
-  // TODO(alexeagle): support plugins registered by config
+  if (angularPlugin) {
+    program = angularPlugin.wrap(program);
+  }
 
   const diagnostics: ts.Diagnostic[] = [];
   perfTrace.wrap('type checking', () => {
@@ -129,7 +131,13 @@
     throw new Error(
         'Impossible state: if parseTsconfig returns no errors, then parsed should be non-null');
   }
-  const {options, bazelOpts, files, disabledTsetseRules} = parsed;
+  const {
+    options,
+    bazelOpts,
+    files,
+    disabledTsetseRules,
+    angularCompilerOptions
+  } = parsed;
 
   if (bazelOpts.maxCacheSizeMb !== undefined) {
     const maxCacheSizeBytes = bazelOpts.maxCacheSizeMb * (1 << 20);
@@ -154,14 +162,16 @@
   const perfTracePath = bazelOpts.perfTracePath;
   if (!perfTracePath) {
     return runFromOptions(
-        fileLoader, options, bazelOpts, files, disabledTsetseRules);
+        fileLoader, options, bazelOpts, files, disabledTsetseRules,
+        angularCompilerOptions);
   }
 
   log('Writing trace to', perfTracePath);
   const success = perfTrace.wrap(
       'runOneBuild',
       () => runFromOptions(
-          fileLoader, options, bazelOpts, files, disabledTsetseRules));
+          fileLoader, options, bazelOpts, files, disabledTsetseRules,
+          angularCompilerOptions));
   if (!success) return false;
   // Force a garbage collection pass.  This keeps our memory usage
   // consistent across multiple compilations, and allows the file
@@ -182,20 +192,47 @@
 
 function runFromOptions(
     fileLoader: FileLoader, options: ts.CompilerOptions,
-    bazelOpts: BazelOptions, files: string[],
-    disabledTsetseRules: string[]): boolean {
+    bazelOpts: BazelOptions, files: string[], disabledTsetseRules: string[],
+    angularCompilerOptions?: {[key: string]: unknown}): boolean {
   perfTrace.snapshotMemoryUsage();
   cache.resetStats();
   cache.traceStats();
+
   const compilerHostDelegate =
       ts.createCompilerHost({target: ts.ScriptTarget.ES5});
 
   const moduleResolver = bazelOpts.isJsTranspilation ?
       makeJsModuleResolver(bazelOpts.workspaceName) :
       ts.resolveModuleName;
-  const compilerHost = new CompilerHost(
+  const tsickleCompilerHost: CompilerHost = new CompilerHost(
       files, options, bazelOpts, compilerHostDelegate, fileLoader,
       moduleResolver);
+  let compilerHost: PluginCompilerHost = tsickleCompilerHost;
+
+  let angularPlugin: TscPlugin|undefined;
+  if (bazelOpts.compileAngularTemplates) {
+    try {
+      const ngOptions = 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');
+      angularPlugin = new ngtsc.NgTscPlugin(ngOptions);
+    } catch (e) {
+      console.error(e);
+      throw new Error(
+          'when using `ts_library(compile_angular_templates=True)`, ' +
+          'you must install @angular/compiler-cli');
+    }
+
+    // Wrap host only needed until after Ivy cleanup
+    // TODO(alexeagle): remove after ngsummary and ngfactory files eliminated
+    compilerHost = angularPlugin!.wrapHost!(files, compilerHost);
+  }
 
 
   const oldProgram = cache.getProgram(bazelOpts.target);
@@ -209,8 +246,8 @@
     // If there are any TypeScript type errors abort now, so the error
     // 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);
+    let diagnostics = gatherDiagnostics(
+        options, bazelOpts, program, disabledTsetseRules, angularPlugin);
     if (!expectDiagnosticsWhitelist.length ||
         expectDiagnosticsWhitelist.some(p => bazelOpts.target.startsWith(p))) {
       diagnostics = bazelDiagnostics.filterExpected(
@@ -235,11 +272,22 @@
 
   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, compilerHost, compilationTargets, options, bazelOpts);
+        program, tsickleCompilerHost, compilationTargets, options, bazelOpts,
+        transforms);
   } else {
-    diagnostics = emitWithTypescript(program, compilationTargets);
+    diagnostics = emitWithTypescript(program, compilationTargets, transforms);
   }
 
   if (diagnostics.length > 0) {
@@ -253,10 +301,14 @@
 }
 
 function emitWithTypescript(
-    program: ts.Program, compilationTargets: ts.SourceFile[]): ts.Diagnostic[] {
+    program: ts.Program, compilationTargets: ts.SourceFile[],
+    transforms: ts.CustomTransformers): ts.Diagnostic[] {
   const diagnostics: ts.Diagnostic[] = [];
   for (const sf of compilationTargets) {
-    const result = program.emit(sf);
+    const result = program.emit(
+        sf, /*writeFile*/ undefined,
+        /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ undefined,
+        transforms);
     diagnostics.push(...result.diagnostics);
   }
   return diagnostics;
@@ -272,7 +324,8 @@
 export function emitWithTsickle(
     program: ts.Program, compilerHost: CompilerHost,
     compilationTargets: ts.SourceFile[], options: ts.CompilerOptions,
-    bazelOpts: BazelOptions): ts.Diagnostic[] {
+    bazelOpts: BazelOptions,
+    transforms: ts.CustomTransformers): ts.Diagnostic[] {
   const emitResults: tsickle.EmitResult[] = [];
   const diagnostics: ts.Diagnostic[] = [];
   // The 'tsickle' import above is only used in type positions, so it won't
@@ -295,7 +348,13 @@
     for (const sf of compilationTargets) {
       perfTrace.wrap(`emit ${sf.fileName}`, () => {
         emitResults.push(optTsickle.emitWithTsickle(
-            program, compilerHost, compilerHost, options, sf));
+            program, compilerHost, compilerHost, options, sf,
+            /*writeFile*/ undefined,
+            /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ undefined, {
+              beforeTs: transforms.before,
+              afterTs: transforms.after,
+              afterDeclarations: transforms.afterDeclarations,
+            }));
       });
     }
   });
diff --git a/internal/tsc_wrapped/tsconfig.ts b/internal/tsc_wrapped/tsconfig.ts
index 076439c..060b5ff 100644
--- a/internal/tsc_wrapped/tsconfig.ts
+++ b/internal/tsc_wrapped/tsconfig.ts
@@ -156,11 +156,17 @@
    * compilation unit.
    */
   hasImplementation?: boolean;
+
+  /**
+   * Enable the Angular ngtsc plugin.
+   */
+  compileAngularTemplates?: boolean;
 }
 
 export interface ParsedTsConfig {
   options: ts.CompilerOptions;
   bazelOpts: BazelOptions;
+  angularCompilerOptions?: {[k: string]: unknown};
   files: string[];
   disabledTsetseRules: string[];
   config: {};