Refactor tsc_wrapped's runFromOptions to be more composable.

This allows other tools to reproduce tsc_wrapped's compiler
configuration & plugin setup with fidelity.

The refactoring moves any console printing out of the former
`runFromOptions`, renames it to `createProgramAndEmit()`, and handles
console emit on the caller level. It also required observing
`options.noEmit` in several locations that'd previously write files
unconditionally.

PiperOrigin-RevId: 278914685
diff --git a/internal/tsc_wrapped/tsc_wrapped.ts b/internal/tsc_wrapped/tsc_wrapped.ts
index 280d733..e73ece5 100644
--- a/internal/tsc_wrapped/tsc_wrapped.ts
+++ b/internal/tsc_wrapped/tsc_wrapped.ts
@@ -118,26 +118,26 @@
  * TODO: Call sites of getDiagnostics should initialize plugins themselves,
  *   including these, and the arguments to getDiagnostics should be simplified.
  */
-export function*
-    getCommonPlugins(
-        options: ts.CompilerOptions, bazelOpts: BazelOptions,
-        program: ts.Program,
-        disabledTsetseRules: string[]): Iterable<DiagnosticPlugin> {
+export function getCommonPlugins(
+    options: ts.CompilerOptions, bazelOpts: BazelOptions, program: ts.Program,
+    disabledTsetseRules: string[]): DiagnosticPlugin[] {
+  const plugins: DiagnosticPlugin[] = [];
   if (!bazelOpts.disableStrictDeps) {
     if (options.rootDir == null) {
       throw new Error(`StrictDepsPlugin requires that rootDir be specified`);
     }
-    yield new StrictDepsPlugin(program, {
+    plugins.push(new StrictDepsPlugin(program, {
       ...bazelOpts,
       rootDir: options.rootDir,
-    });
+    }));
   }
   if (!bazelOpts.isJsTranspilation) {
     let tsetsePluginConstructor:
         {new (program: ts.Program, disabledRules: string[]): DiagnosticPlugin} =
             BazelConformancePlugin;
-    yield new tsetsePluginConstructor(program, disabledTsetseRules);
+    plugins.push(new tsetsePluginConstructor(program, disabledTsetseRules));
   }
+  return plugins;
 }
 
 /**
@@ -250,17 +250,27 @@
 
   const perfTracePath = bazelOpts.perfTracePath;
   if (!perfTracePath) {
-    return runFromOptions(
+    const {diagnostics} = createProgramAndEmit(
         fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules,
         angularCompilerOptions);
+    if (diagnostics.length > 0) {
+      console.error(bazelDiagnostics.format(bazelOpts.target, diagnostics));
+      return false;
+    }
+    return true;
   }
 
   log('Writing trace to', perfTracePath);
-  const success = perfTrace.wrap(
-      'runOneBuild',
-      () => runFromOptions(
-          fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules,
-          angularCompilerOptions));
+  const success = perfTrace.wrap('runOneBuild', () => {
+    const {diagnostics} = createProgramAndEmit(
+        fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules,
+        angularCompilerOptions);
+    if (diagnostics.length > 0) {
+      console.error(bazelDiagnostics.format(bazelOpts.target, diagnostics));
+      return false;
+    }
+    return true;
+  });
   if (!success) return false;
   // Force a garbage collection pass.  This keeps our memory usage
   // consistent across multiple compilations, and allows the file
@@ -279,10 +289,32 @@
 const expectDiagnosticsWhitelist: string[] = [
 ];
 
-function runFromOptions(
+/** errorDiag produces an error diagnostic not bound to a file or location. */
+function errorDiag(messageText: string) {
+  return {
+    category: ts.DiagnosticCategory.Error,
+    code: 0,
+    file: undefined,
+    start: undefined,
+    length: undefined,
+    messageText,
+  };
+}
+
+/**
+ * createProgramAndEmit creates a ts.Program from the given options and emits it
+ * according to them (e.g. including running various plugins and tsickle). It
+ * returns the program and any diagnostics generated.
+ *
+ * Callers should check and emit diagnostics.
+ */
+export function createProgramAndEmit(
     fileLoader: FileLoader, options: ts.CompilerOptions,
     bazelOpts: BazelOptions, files: string[], disabledTsetseRules: string[],
-    angularCompilerOptions?: {[key: string]: unknown}): boolean {
+    angularCompilerOptions?: {[key: string]: unknown}):
+    {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();
   cache.resetStats();
   cache.traceStats();
@@ -313,10 +345,11 @@
       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');
+      return {
+        diagnostics: [errorDiag(
+            'when using `ts_library(compile_angular_templates=True)`, ' +
+            `you must install @angular/compiler-cli (was: ${e})`)]
+      };
     }
 
     // Wrap host only needed until after Ivy cleanup
@@ -345,17 +378,15 @@
       diagnostics = bazelDiagnostics.filterExpected(
           bazelOpts, diagnostics, bazelDiagnostics.uglyFormat);
     } else if (bazelOpts.expectedDiagnostics.length > 0) {
-      console.error(
+      diagnostics.push(errorDiag(
           `Only targets under ${
               expectDiagnosticsWhitelist.join(', ')} can use ` +
-              'expected_diagnostics, but got',
-          bazelOpts.target);
+          'expected_diagnostics, but got ' + bazelOpts.target));
     }
 
     if (diagnostics.length > 0) {
-      console.error(bazelDiagnostics.format(bazelOpts.target, diagnostics));
       debug('compilation failed at', new Error().stack!);
-      return false;
+      return {program, diagnostics};
     }
   }
 
@@ -383,13 +414,10 @@
   }
 
   if (diagnostics.length > 0) {
-    console.error(bazelDiagnostics.format(bazelOpts.target, diagnostics));
     debug('compilation failed at', new Error().stack!);
-    return false;
   }
-
   cache.printStats();
-  return true;
+  return {program, diagnostics};
 }
 
 function emitWithTypescript(
@@ -465,7 +493,7 @@
         optTsickle.getGeneratedExterns(emitResult.externs, options.rootDir!);
   }
 
-  if (bazelOpts.tsickleExternsPath) {
+  if (!options.noEmit && bazelOpts.tsickleExternsPath) {
     // Note: when tsickleExternsPath is provided, we always write a file as a
     // marker that compilation succeeded, even if it's empty (just containing an
     // @externs).
@@ -493,7 +521,7 @@
     }
   }
 
-  if (bazelOpts.manifest) {
+  if (!options.noEmit && bazelOpts.manifest) {
     perfTrace.wrap('manifest', () => {
       const manifest =
           constructManifest(emitResult.modulesManifest, compilerHost);