feat(typescript): support for ESM variant of the Angular compiler plugin (#2982)

As of v13, the `@angular/compiler-cli` package will come as strict ESM
package. This means that the import currently in `tsc_wrapped` does
not work for v13+ of Angular. This commit adds an interop allowing for
both the ESM variant, and CJS variant of the Angular compiler to work.
diff --git a/third_party/github.com/bazelbuild/rules_typescript/internal/BUILD.bazel b/third_party/github.com/bazelbuild/rules_typescript/internal/BUILD.bazel
index 5440894..943d00e 100644
--- a/third_party/github.com/bazelbuild/rules_typescript/internal/BUILD.bazel
+++ b/third_party/github.com/bazelbuild/rules_typescript/internal/BUILD.bazel
@@ -61,6 +61,7 @@
     data = _TSC_WRAPPED_SRCS + [
         "//internal:tsconfig.json",
         "@npm//@types/node",
+        "@npm//@angular/compiler-cli",
         "@npm//protobufjs",
         "@npm//tsickle",
         "@npm//tsutils",
diff --git a/third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/angular_plugin.ts b/third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/angular_plugin.ts
new file mode 100644
index 0000000..47bc080
--- /dev/null
+++ b/third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/angular_plugin.ts
@@ -0,0 +1,23 @@
+// The `@angular/compiler-cli` module is optional so we only
+// access as type-only at the file top-level.
+import type {NgTscPlugin} from '@angular/compiler-cli';
+
+type CompilerCliModule = typeof import('@angular/compiler-cli');
+
+/**
+ * Gets the constructor for instantiating the Angular `ngtsc`
+ * emit plugin supported by `tsc_wrapped`.
+ */
+export async function getAngularEmitPlugin(): Promise<typeof NgTscPlugin|null> {
+  try {
+    // Note: This is an interop allowing for the `@angular/compiler-cli` package
+    // to be shipped as strict ESM, or as CommonJS. If the CLI is a CommonJS
+    // package (pre v13 of Angular), then the exports are in the `default` property.
+    // See: https://nodejs.org/api/esm.html#esm_import_statements.
+    const exports = await import('@angular/compiler-cli') as
+        Partial<CompilerCliModule> & {default?: CompilerCliModule}
+    return exports.NgTscPlugin ?? exports.default?.NgTscPlugin ?? null;
+  } catch {
+    return null;
+  }
+}
diff --git a/third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/perf_trace.ts b/third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/perf_trace.ts
index 3e0399f..c1f44e1 100644
--- a/third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/perf_trace.ts
+++ b/third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/perf_trace.ts
@@ -59,6 +59,20 @@
 }
 
 /**
+ * Records the execution of the given async function by invoking it. Execution
+ * is recorded until the async function completes.
+ */
+export async function wrapAsync<T>(name: string, f: () => Promise<T>): Promise<T> {
+  const start = now();
+  try {
+    return await f();
+  } finally {
+    const end = now();
+    events.push({name, ph: 'X', pid: 1, ts: start, dur: (end - start)});
+  }
+}
+
+/**
  * counter records a snapshot of counts.  The counter name identifies a
  * single graph, while the counts object provides data for each count
  * of a line on the stacked bar graph.
diff --git a/third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/tsc_wrapped.ts b/third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/tsc_wrapped.ts
index 79160f2..be4931e 100644
--- a/third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/tsc_wrapped.ts
+++ b/third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/tsc_wrapped.ts
@@ -1,8 +1,10 @@
 import * as fs from 'fs';
 import * as path from 'path';
-import * as tsickle from 'tsickle';
 import * as ts from 'typescript';
 
+// Tsickle is optional, but this import is just used for typechecking.
+import type * as tsickle from 'tsickle';
+
 import {Plugin as BazelConformancePlugin} from '../tsetse/runner';
 
 import {CachedFileLoader, FileLoader, ProgramAndFileCache, UncachedFileLoader} from './cache';
@@ -14,11 +16,12 @@
 import {Plugin as StrictDepsPlugin} from './strict_deps';
 import {BazelOptions, parseTsconfig, resolveNormalizedPath} from './tsconfig';
 import {debug, log, runAsWorker, runWorkerLoop} from './worker';
+import { getAngularEmitPlugin } from './angular_plugin';
 
 /**
  * Top-level entry point for tsc_wrapped.
  */
-export function main(args: string[]) {
+export async function main(args: string[]) {
   if (runAsWorker(args)) {
     log('Starting TypeScript compiler persistent worker...');
     runWorkerLoop(runOneBuild);
@@ -27,7 +30,7 @@
   } else {
     debug('Running a single build...');
     if (args.length === 0) throw new Error('Not enough arguments');
-    if (!runOneBuild(args)) {
+    if (!await runOneBuild(args)) {
       return 1;
     }
   }
@@ -190,8 +193,8 @@
  * multiple times (once per bazel request) when running as a bazel worker.
  * Any encountered errors are written to stderr.
  */
-function runOneBuild(
-    args: string[], inputs?: {[path: string]: string}): boolean {
+async function runOneBuild(
+    args: string[], inputs?: {[path: string]: string}): Promise<boolean> {
   if (args.length !== 1) {
     console.error('Expected one argument: path to tsconfig.json');
     return false;
@@ -242,9 +245,9 @@
     fileLoader = new UncachedFileLoader();
   }
 
-  const diagnostics = perfTrace.wrap('createProgramAndEmit', () => {
-    return createProgramAndEmit(
-               fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules)
+  const diagnostics = await perfTrace.wrapAsync('createProgramAndEmit', async () => {
+    return (await createProgramAndEmit(
+               fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules))
         .diagnostics;
   });
 
@@ -289,10 +292,10 @@
  *
  * Callers should check and emit diagnostics.
  */
-export function createProgramAndEmit(
+export async function createProgramAndEmit(
     fileLoader: FileLoader, options: ts.CompilerOptions,
     bazelOpts: BazelOptions, files: string[], disabledTsetseRules: string[]):
-    {program?: ts.Program, diagnostics: ts.Diagnostic[]} {
+    Promise<{program?: ts.Program, diagnostics: ts.Diagnostic[]}> {
   // Beware! createProgramAndEmit must not print to console, nor exit etc.
   // Handle errors by reporting and returning diagnostics.
   perfTrace.snapshotMemoryUsage();
@@ -322,32 +325,29 @@
 
   let angularPlugin: EmitPlugin&DiagnosticPlugin|undefined;
   if (bazelOpts.angularCompilerOptions) {
-    try {
-      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 emit plugin.
+    // 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.
+    const NgEmitPluginCtor = await getAngularEmitPlugin();
 
-      let angularPluginEntryPoint = '@angular/compiler-cli';
-
-      // 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) {
+    if (NgEmitPluginCtor === null) {
       return {
         diagnostics: [errorDiag(
             'when using `ts_library(use_angular_plugin=True)`, ' +
-            `you must install @angular/compiler-cli (was: ${e})`)]
+            `you must install @angular/compiler-cli.`)]
       };
     }
 
+    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;
+
+    angularPlugin = new NgEmitPluginCtor(ngOptions);
+    diagnosticPlugins.push(angularPlugin);
+
     // 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);
@@ -684,5 +684,10 @@
   // completing pending operations, such as writing to stdout or emitting the
   // v8 performance log. Rather, set the exit code and fall off the main
   // thread, which will cause node to terminate cleanly.
-  process.exitCode = main(process.argv.slice(2));
+  main(process.argv.slice(2))
+    .then(exitCode => process.exitCode = exitCode)
+    .catch(e => {
+      console.error(e);
+      process.exitCode = 1;
+    });
 }