Skip to content

Commit

Permalink
fix(typescript): typescript downleveling breaking ESM variant of Angu…
Browse files Browse the repository at this point in the history
…lar v13 compiler (#2987)

Unfortunately my initial refactor to allow for the ESM variant of the
compiler-cli did not contain a workaround for an issue that is caused
by TypeScript. Currently the import is rewritten to a dynamic require,
preventing the ESM variant of the compiler-cli to work. This commit
fixes this in a similar way we have made `@angular/cli` compatible with
the ESM-variant of Angular v13 packages.
  • Loading branch information
devversion authored Oct 1, 2021
1 parent 8ca234b commit 5e4d17d
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,38 @@
import type {NgTscPlugin} from '@angular/compiler-cli';

type CompilerCliModule = typeof import('@angular/compiler-cli');
type CompilerInteropExports = Partial<CompilerCliModule> & {default?: CompilerCliModule};


/**
* Gets the constructor for instantiating the Angular `ngtsc`
* emit plugin supported by `tsc_wrapped`.
* @throws An error when the Angular emit plugin could not be retrieved.
*/
export async function getAngularEmitPlugin(): Promise<typeof NgTscPlugin|null> {
export async function getAngularEmitPluginOrThrow(): Promise<typeof NgTscPlugin> {
// 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.
// Note: TypeScript downlevels the dynamic `import` to a `require` that is
// not compatible with ESM. We create a function to workaround this issue.
const exports = await loadEsmOrFallbackToRequire<CompilerInteropExports>(
'@angular/compiler-cli');
const plugin = exports.NgTscPlugin ?? exports.default?.NgTscPlugin;

if (plugin === undefined) {
throw new Error('Could not find `NgTscPlugin` export in `@angular/compiler-cli`.');
}

return plugin;
}

async function loadEsmOrFallbackToRequire<T>(moduleName: string): Promise<T> {
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;
return await new Function('m', `return import(m);`)(moduleName);
} catch {
return null;
// If the dynamic import failed, we still re-try with `require` because
// some NodeJS versions do not even support the dynamic import expression.
return require(moduleName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ 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';
import {getAngularEmitPluginOrThrow} from './angular_plugin';

/**
* Top-level entry point for tsc_wrapped.
Expand Down Expand Up @@ -325,27 +325,28 @@ export async function createProgramAndEmit(

let angularPlugin: EmitPlugin&DiagnosticPlugin|undefined;
if (bazelOpts.angularCompilerOptions) {
// 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) {
try {
// 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 getAngularEmitPluginOrThrow();
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);
} catch (e) {
return {
diagnostics: [errorDiag(
'when using `ts_library(use_angular_plugin=True)`, ' +
`you must install @angular/compiler-cli.`)]
`you must install "@angular/compiler-cli". Error: ${e}`)]
};
}

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)
Expand Down

0 comments on commit 5e4d17d

Please sign in to comment.