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;
+ });
}