From 6f97a7cd018b2f39e2341a94145b2c9db80cd4c2 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Mon, 27 Sep 2021 21:41:48 +0200 Subject: [PATCH] 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. --- .../rules_typescript/internal/BUILD.bazel | 1 + .../internal/tsc_wrapped/angular_plugin.ts | 23 +++++++ .../internal/tsc_wrapped/perf_trace.ts | 14 ++++ .../internal/tsc_wrapped/tsc_wrapped.ts | 67 ++++++++++--------- 4 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 third_party/github.com/bazelbuild/rules_typescript/internal/tsc_wrapped/angular_plugin.ts 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 5440894fc6..943d00e736 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 @@ tsc( 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 0000000000..47bc080351 --- /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 { + 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 & {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 3e0399ff60..c1f44e150d 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 @@ -58,6 +58,20 @@ export function wrap(name: string, f: () => T): T { } } +/** + * Records the execution of the given async function by invoking it. Execution + * is recorded until the async function completes. + */ +export async function wrapAsync(name: string, f: () => Promise): Promise { + 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 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 79160f284c..be4931e55a 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 {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'; +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 @@ export function main(args: string[]) { } 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 @@ function expandSourcesFromDirectories(fileList: string[], filePath: string) { * 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 { if (args.length !== 1) { console.error('Expected one argument: path to tsconfig.json'); return false; @@ -242,9 +245,9 @@ function runOneBuild( 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 @@ function errorDiag(messageText: string) { * * 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 @@ export function createProgramAndEmit( 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; - - 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) { + // 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(); + + 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 @@ if (require.main === module) { // 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; + }); }