Skip to content

Commit f2c6b2b

Browse files
committedJan 24, 2022
fix(@angular-devkit/architect): correctly handle ESM builders
Previoiusly, we didn't correctly handle ESM builders as the `import` was always downlevelled to `require` by TypeScript. (cherry picked from commit 5def2de)
1 parent 820ff2a commit f2c6b2b

File tree

1 file changed

+47
-1
lines changed

1 file changed

+47
-1
lines changed
 

‎packages/angular_devkit/architect/node/node-modules-architect-host.ts

+47-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { json, workspaces } from '@angular-devkit/core';
1010
import * as path from 'path';
11+
import { URL, pathToFileURL } from 'url';
1112
import { deserialize, serialize } from 'v8';
1213
import { BuilderInfo } from '../src';
1314
import { Schema as BuilderSchema } from '../src/builders-schema';
@@ -197,7 +198,8 @@ export class WorkspaceNodeModulesArchitectHost implements ArchitectHost<NodeModu
197198
}
198199

199200
async loadBuilder(info: NodeModulesBuilderInfo): Promise<Builder> {
200-
const builder = (await import(info.import)).default;
201+
const builder = await getBuilder(info.import);
202+
201203
if (builder[BuilderSymbol]) {
202204
return builder;
203205
}
@@ -210,3 +212,47 @@ export class WorkspaceNodeModulesArchitectHost implements ArchitectHost<NodeModu
210212
throw new Error('Builder is not a builder');
211213
}
212214
}
215+
216+
/**
217+
* This uses a dynamic import to load a module which may be ESM.
218+
* CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
219+
* will currently, unconditionally downlevel dynamic import into a require call.
220+
* require calls cannot load ESM code and will result in a runtime error. To workaround
221+
* this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
222+
* Once TypeScript provides support for keeping the dynamic import this workaround can
223+
* be dropped.
224+
*
225+
* @param modulePath The path of the module to load.
226+
* @returns A Promise that resolves to the dynamically imported module.
227+
*/
228+
function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
229+
return new Function('modulePath', `return import(modulePath);`)(modulePath) as Promise<T>;
230+
}
231+
232+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
233+
async function getBuilder(builderPath: string): Promise<any> {
234+
switch (path.extname(builderPath)) {
235+
case '.mjs':
236+
// Load the ESM configuration file using the TypeScript dynamic import workaround.
237+
// Once TypeScript provides support for keeping the dynamic import this workaround can be
238+
// changed to a direct dynamic import.
239+
return (await loadEsmModule<{ default: unknown }>(pathToFileURL(builderPath))).default;
240+
case '.cjs':
241+
return require(builderPath);
242+
default:
243+
// The file could be either CommonJS or ESM.
244+
// CommonJS is tried first then ESM if loading fails.
245+
try {
246+
return require(builderPath);
247+
} catch (e) {
248+
if (e.code === 'ERR_REQUIRE_ESM') {
249+
// Load the ESM configuration file using the TypeScript dynamic import workaround.
250+
// Once TypeScript provides support for keeping the dynamic import this workaround can be
251+
// changed to a direct dynamic import.
252+
return (await loadEsmModule<{ default: unknown }>(pathToFileURL(builderPath))).default;
253+
}
254+
255+
throw e;
256+
}
257+
}
258+
}

0 commit comments

Comments
 (0)
Please sign in to comment.