Skip to content

Commit

Permalink
feat(typescript): support for ESM variant of the Angular compiler plu…
Browse files Browse the repository at this point in the history
…gin (#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.
  • Loading branch information
devversion authored Sep 27, 2021
1 parent 5b6ac1e commit 6f97a7c
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ export function wrap<T>(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<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
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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<boolean> {
if (args.length !== 1) {
console.error('Expected one argument: path to tsconfig.json');
return false;
Expand Down Expand Up @@ -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;
});

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
});
}

0 comments on commit 6f97a7c

Please sign in to comment.