feat(angular): support providing esbuild plugins to @nx/angular:browser-esbuild (#20504)
This commit is contained in:
parent
7cb8aead12
commit
a831e262dd
@ -513,6 +513,35 @@
|
||||
"type": "boolean",
|
||||
"description": "Read buildable libraries from source instead of building them separately.",
|
||||
"default": true
|
||||
},
|
||||
"plugins": {
|
||||
"description": "A list of ESBuild plugins.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path to the plugin. Relative to the workspace root."
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"description": "The options to provide to the plugin.",
|
||||
"properties": {},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["path"]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The path to the plugin. Relative to the workspace root."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -21,15 +21,22 @@ import { join, normalize } from 'path';
|
||||
describe('Angular Projects', () => {
|
||||
let proj: string;
|
||||
const app1 = uniq('app1');
|
||||
const esbuildApp = uniq('esbuild-app');
|
||||
const lib1 = uniq('lib1');
|
||||
let app1DefaultModule: string;
|
||||
let app1DefaultComponentTemplate: string;
|
||||
let esbuildAppDefaultModule: string;
|
||||
let esbuildAppDefaultComponentTemplate: string;
|
||||
let esbuildAppDefaultProjectConfig: string;
|
||||
|
||||
beforeAll(() => {
|
||||
proj = newProject();
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${app1} --no-standalone --bundler=webpack --project-name-and-root-format=as-provided --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${esbuildApp} --bundler=esbuild --no-standalone --project-name-and-root-format=as-provided --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:lib ${lib1} --no-standalone --add-module-spec --project-name-and-root-format=as-provided --no-interactive`
|
||||
);
|
||||
@ -37,6 +44,11 @@ describe('Angular Projects', () => {
|
||||
app1DefaultComponentTemplate = readFile(
|
||||
`${app1}/src/app/app.component.html`
|
||||
);
|
||||
esbuildAppDefaultModule = readFile(`${app1}/src/app/app.module.ts`);
|
||||
esbuildAppDefaultComponentTemplate = readFile(
|
||||
`${esbuildApp}/src/app/app.component.html`
|
||||
);
|
||||
esbuildAppDefaultProjectConfig = readFile(`${esbuildApp}/project.json`);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -45,6 +57,12 @@ describe('Angular Projects', () => {
|
||||
`${app1}/src/app/app.component.html`,
|
||||
app1DefaultComponentTemplate
|
||||
);
|
||||
updateFile(`${esbuildApp}/src/app/app.module.ts`, esbuildAppDefaultModule);
|
||||
updateFile(
|
||||
`${esbuildAppDefaultComponentTemplate}/src/app/app.component.html`,
|
||||
esbuildAppDefaultComponentTemplate
|
||||
);
|
||||
updateFile(`${esbuildApp}/project.json`, esbuildAppDefaultProjectConfig);
|
||||
});
|
||||
|
||||
afterAll(() => cleanupProject());
|
||||
@ -55,9 +73,9 @@ describe('Angular Projects', () => {
|
||||
`generate @nx/angular:app ${standaloneApp} --directory=my-dir/${standaloneApp} --bundler=webpack --project-name-and-root-format=as-provided --no-interactive`
|
||||
);
|
||||
|
||||
const esbuildApp = uniq('esbuild-app');
|
||||
const esbuildStandaloneApp = uniq('esbuild-app');
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${esbuildApp} --bundler=esbuild --directory=my-dir/${esbuildApp} --project-name-and-root-format=as-provided --no-interactive`
|
||||
`generate @nx/angular:app ${esbuildStandaloneApp} --bundler=esbuild --directory=my-dir/${esbuildStandaloneApp} --project-name-and-root-format=as-provided --no-interactive`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
@ -86,11 +104,12 @@ describe('Angular Projects', () => {
|
||||
|
||||
// check build
|
||||
runCLI(
|
||||
`run-many --target build --projects=${app1},${standaloneApp},${esbuildApp} --parallel --prod --output-hashing none`
|
||||
`run-many --target build --projects=${app1},${esbuildApp},${standaloneApp},${esbuildStandaloneApp} --parallel --prod --output-hashing none`
|
||||
);
|
||||
checkFilesExist(`dist/${app1}/main.js`);
|
||||
checkFilesExist(`dist/${esbuildApp}/browser/main.js`);
|
||||
checkFilesExist(`dist/my-dir/${standaloneApp}/main.js`);
|
||||
checkFilesExist(`dist/my-dir/${esbuildApp}/browser/main.js`);
|
||||
checkFilesExist(`dist/my-dir/${esbuildStandaloneApp}/browser/main.js`);
|
||||
// This is a loose requirement because there are a lot of
|
||||
// influences external from this project that affect this.
|
||||
const es2015BundleSize = getSize(tmpProjPath(`dist/${app1}/main.js`));
|
||||
@ -101,7 +120,7 @@ describe('Angular Projects', () => {
|
||||
|
||||
// check unit tests
|
||||
runCLI(
|
||||
`run-many --target test --projects=${app1},${standaloneApp},${esbuildApp},${lib1} --parallel`
|
||||
`run-many --target test --projects=${app1},${standaloneApp},${esbuildStandaloneApp},${lib1} --parallel`
|
||||
);
|
||||
|
||||
// check e2e tests
|
||||
@ -121,7 +140,7 @@ describe('Angular Projects', () => {
|
||||
await killProcessAndPorts(process.pid, appPort);
|
||||
|
||||
const esbProcess = await runCommandUntil(
|
||||
`serve ${esbuildApp} -- --port=${appPort}`,
|
||||
`serve ${esbuildStandaloneApp} -- --port=${appPort}`,
|
||||
(output) =>
|
||||
output.includes(`Application bundle generation complete`) &&
|
||||
output.includes(`localhost:${appPort}`)
|
||||
@ -199,11 +218,6 @@ describe('Angular Projects', () => {
|
||||
|
||||
it('should build the dependent buildable lib and its child lib, as well as the app', async () => {
|
||||
// ARRANGE
|
||||
const esbuildApp = uniq('esbuild-app');
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${esbuildApp} --bundler=esbuild --no-standalone --project-name-and-root-format=as-provided --no-interactive`
|
||||
);
|
||||
|
||||
const buildableLib = uniq('buildlib1');
|
||||
const buildableChildLib = uniq('buildlib2');
|
||||
|
||||
@ -328,6 +342,56 @@ describe('Angular Projects', () => {
|
||||
expect(mainEsBuildBundle).toContain(`dist/${buildableLib}`);
|
||||
});
|
||||
|
||||
it('should support esbuild plugins', async () => {
|
||||
updateFile(
|
||||
`${esbuildApp}/replace-text.plugin.mjs`,
|
||||
`const replaceTextPlugin = {
|
||||
name: 'replace-text',
|
||||
setup(build) {
|
||||
const options = build.initialOptions;
|
||||
options.define.BUILD_DEFINED = '"Value was provided at build time"';
|
||||
},
|
||||
};
|
||||
|
||||
export default replaceTextPlugin;`
|
||||
);
|
||||
updateJson(join(esbuildApp, 'project.json'), (config) => {
|
||||
config.targets.build.executor = '@nx/angular:browser-esbuild';
|
||||
config.targets.build.options = {
|
||||
...config.targets.build.options,
|
||||
outputPath: `dist/${esbuildApp}`,
|
||||
main: config.targets.build.options.browser,
|
||||
browser: undefined,
|
||||
plugins: [`${esbuildApp}/replace-text.plugin.mjs`],
|
||||
};
|
||||
return config;
|
||||
});
|
||||
updateFile(
|
||||
`${esbuildApp}/src/app/app.component.ts`,
|
||||
`import { Component } from '@angular/core';
|
||||
|
||||
declare const BUILD_DEFINED: string;
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'esbuild-app';
|
||||
buildDefined = BUILD_DEFINED;
|
||||
}`
|
||||
);
|
||||
|
||||
// ACT
|
||||
runCLI(`build ${esbuildApp} --configuration=development`);
|
||||
|
||||
// ASSERT
|
||||
const mainBundle = readFile(`dist/${esbuildApp}/main.js`);
|
||||
expect(mainBundle).toContain(
|
||||
'this.buildDefined = "Value was provided at build time";'
|
||||
);
|
||||
});
|
||||
|
||||
it('should build publishable libs successfully', () => {
|
||||
// ARRANGE
|
||||
const lib = uniq('lib');
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import type { BuilderContext } from '@angular-devkit/architect';
|
||||
import type { DevServerBuilderOptions } from '@angular-devkit/build-angular';
|
||||
import type {
|
||||
ApplicationBuilderOptions,
|
||||
BrowserBuilderOptions,
|
||||
DevServerBuilderOptions,
|
||||
} from '@angular-devkit/build-angular';
|
||||
import type { Schema as BrowserEsbuildBuilderOptions } from '@angular-devkit/build-angular/src/builders/browser-esbuild/schema';
|
||||
import {
|
||||
joinPathFragments,
|
||||
normalizePath,
|
||||
parseTargetString,
|
||||
readCachedProjectGraph,
|
||||
type Target,
|
||||
} from '@nx/devkit';
|
||||
import { getRootTsConfigPath } from '@nx/js';
|
||||
import type { DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils';
|
||||
@ -13,9 +19,13 @@ import { existsSync } from 'fs';
|
||||
import { isNpmProject } from 'nx/src/project-graph/operators';
|
||||
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
|
||||
import { relative } from 'path';
|
||||
import { from } from 'rxjs';
|
||||
import { combineLatest, from } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { getInstalledAngularVersionInfo } from '../../executors/utilities/angular-version-utils';
|
||||
import {
|
||||
loadPlugins,
|
||||
type PluginSpec,
|
||||
} from '../../executors/utilities/esbuild-extensions';
|
||||
import { createTmpTsConfigForBuildableLibs } from '../utilities/buildable-libs';
|
||||
import {
|
||||
mergeCustomWebpackConfig,
|
||||
@ -33,6 +43,7 @@ type BuildTargetOptions = {
|
||||
buildLibsFromSource?: boolean;
|
||||
customWebpackConfig?: { path?: string };
|
||||
indexFileTransformer?: string;
|
||||
plugins?: string[] | PluginSpec[];
|
||||
};
|
||||
|
||||
export function executeDevServerBuilder(
|
||||
@ -143,56 +154,66 @@ export function executeDevServerBuilder(
|
||||
* builders. Since we are using a custom builder, we patch the context to
|
||||
* handle `@nx/angular:*` executors.
|
||||
*/
|
||||
patchBuilderContext(context);
|
||||
patchBuilderContext(context, !isUsingWebpackBuilder, parsedBuildTarget);
|
||||
|
||||
return from(import('@angular-devkit/build-angular')).pipe(
|
||||
switchMap(({ executeDevServerBuilder }) =>
|
||||
executeDevServerBuilder(delegateBuilderOptions, context, {
|
||||
webpackConfiguration: isUsingWebpackBuilder
|
||||
? async (baseWebpackConfig) => {
|
||||
if (!buildLibsFromSource) {
|
||||
const workspaceDependencies = dependencies
|
||||
.filter((dep) => !isNpmProject(dep.node))
|
||||
.map((dep) => dep.node.name);
|
||||
// default for `nx run-many` is --all projects
|
||||
// by passing an empty string for --projects, run-many will default to
|
||||
// run the target for all projects.
|
||||
// This will occur when workspaceDependencies = []
|
||||
if (workspaceDependencies.length > 0) {
|
||||
baseWebpackConfig.plugins.push(
|
||||
// @ts-expect-error - difference between angular and webpack plugin definitions bc of webpack versions
|
||||
new WebpackNxBuildCoordinationPlugin(
|
||||
`nx run-many --target=${
|
||||
parsedBuildTarget.target
|
||||
} --projects=${workspaceDependencies.join(',')}`
|
||||
)
|
||||
);
|
||||
return combineLatest([
|
||||
from(import('@angular-devkit/build-angular')),
|
||||
from(loadPlugins(buildTargetOptions.plugins, buildTargetOptions.tsConfig)),
|
||||
]).pipe(
|
||||
switchMap(([{ executeDevServerBuilder }, plugins]) =>
|
||||
executeDevServerBuilder(
|
||||
delegateBuilderOptions,
|
||||
context,
|
||||
{
|
||||
webpackConfiguration: isUsingWebpackBuilder
|
||||
? async (baseWebpackConfig) => {
|
||||
if (!buildLibsFromSource) {
|
||||
const workspaceDependencies = dependencies
|
||||
.filter((dep) => !isNpmProject(dep.node))
|
||||
.map((dep) => dep.node.name);
|
||||
// default for `nx run-many` is --all projects
|
||||
// by passing an empty string for --projects, run-many will default to
|
||||
// run the target for all projects.
|
||||
// This will occur when workspaceDependencies = []
|
||||
if (workspaceDependencies.length > 0) {
|
||||
baseWebpackConfig.plugins.push(
|
||||
// @ts-expect-error - difference between angular and webpack plugin definitions bc of webpack versions
|
||||
new WebpackNxBuildCoordinationPlugin(
|
||||
`nx run-many --target=${
|
||||
parsedBuildTarget.target
|
||||
} --projects=${workspaceDependencies.join(',')}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pathToWebpackConfig) {
|
||||
return baseWebpackConfig;
|
||||
}
|
||||
|
||||
return mergeCustomWebpackConfig(
|
||||
baseWebpackConfig,
|
||||
pathToWebpackConfig,
|
||||
buildTargetOptions,
|
||||
context.target
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
|
||||
if (!pathToWebpackConfig) {
|
||||
return baseWebpackConfig;
|
||||
...(pathToIndexFileTransformer
|
||||
? {
|
||||
indexHtml: resolveIndexHtmlTransformer(
|
||||
pathToIndexFileTransformer,
|
||||
buildTargetOptions.tsConfig,
|
||||
context.target
|
||||
),
|
||||
}
|
||||
|
||||
return mergeCustomWebpackConfig(
|
||||
baseWebpackConfig,
|
||||
pathToWebpackConfig,
|
||||
buildTargetOptions,
|
||||
context.target
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
|
||||
...(pathToIndexFileTransformer
|
||||
? {
|
||||
indexHtml: resolveIndexHtmlTransformer(
|
||||
pathToIndexFileTransformer,
|
||||
buildTargetOptions.tsConfig,
|
||||
context.target
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
: {}),
|
||||
},
|
||||
{
|
||||
buildPlugins: plugins,
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -228,7 +249,11 @@ const executorToBuilderMap = new Map<string, string>([
|
||||
'@angular-devkit/build-angular:browser-esbuild',
|
||||
],
|
||||
]);
|
||||
function patchBuilderContext(context: BuilderContext): void {
|
||||
function patchBuilderContext(
|
||||
context: BuilderContext,
|
||||
isUsingEsbuildBuilder: boolean,
|
||||
buildTarget: Target
|
||||
): void {
|
||||
const originalGetBuilderNameForTarget = context.getBuilderNameForTarget;
|
||||
context.getBuilderNameForTarget = async (target) => {
|
||||
const builderName = await originalGetBuilderNameForTarget(target);
|
||||
@ -239,4 +264,35 @@ function patchBuilderContext(context: BuilderContext): void {
|
||||
|
||||
return builderName;
|
||||
};
|
||||
|
||||
if (isUsingEsbuildBuilder) {
|
||||
const originalGetTargetOptions = context.getTargetOptions;
|
||||
context.getTargetOptions = async (target) => {
|
||||
const options = await originalGetTargetOptions(target);
|
||||
|
||||
if (
|
||||
target.project === buildTarget.project &&
|
||||
target.target === buildTarget.target &&
|
||||
target.configuration === buildTarget.configuration
|
||||
) {
|
||||
cleanBuildTargetOptions(options);
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function cleanBuildTargetOptions(
|
||||
options: any
|
||||
):
|
||||
| ApplicationBuilderOptions
|
||||
| BrowserBuilderOptions
|
||||
| BrowserEsbuildBuilderOptions {
|
||||
delete options.buildLibsFromSource;
|
||||
delete options.customWebpackConfig;
|
||||
delete options.indexFileTransformer;
|
||||
delete options.plugins;
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { type EsBuildSchema } from './schema';
|
||||
import { type DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils';
|
||||
import { type ExecutorContext, readCachedProjectGraph } from '@nx/devkit';
|
||||
import { createTmpTsConfigForBuildableLibs } from './lib/buildable-libs';
|
||||
import type { BuilderOutput } from '@angular-devkit/architect';
|
||||
import { readCachedProjectGraph, type ExecutorContext } from '@nx/devkit';
|
||||
import type { DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils';
|
||||
import type { OutputFile } from 'esbuild';
|
||||
import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter';
|
||||
import { type BuilderOutput } from '@angular-devkit/architect';
|
||||
import { type OutputFile } from 'esbuild';
|
||||
import { loadPlugins } from '../utilities/esbuild-extensions';
|
||||
import { createTmpTsConfigForBuildableLibs } from './lib/buildable-libs';
|
||||
import type { EsBuildSchema } from './schema';
|
||||
|
||||
export default async function* esbuildExecutor(
|
||||
options: EsBuildSchema,
|
||||
@ -12,7 +13,11 @@ export default async function* esbuildExecutor(
|
||||
) {
|
||||
options.buildLibsFromSource ??= true;
|
||||
|
||||
const { buildLibsFromSource, ...delegateExecutorOptions } = options;
|
||||
const {
|
||||
buildLibsFromSource,
|
||||
plugins: pluginPaths,
|
||||
...delegateExecutorOptions
|
||||
} = options;
|
||||
|
||||
let dependencies: DependentBuildableProjectNode[];
|
||||
let projectGraph = context.projectGraph;
|
||||
@ -28,6 +33,9 @@ export default async function* esbuildExecutor(
|
||||
dependencies = foundDependencies;
|
||||
delegateExecutorOptions.tsConfig = tsConfigPath;
|
||||
}
|
||||
|
||||
const plugins = await loadPlugins(pluginPaths, options.tsConfig);
|
||||
|
||||
const { buildEsbuildBrowser } = await import(
|
||||
'@angular-devkit/build-angular/src/builders/browser-esbuild/index'
|
||||
);
|
||||
@ -45,7 +53,9 @@ export default async function* esbuildExecutor(
|
||||
|
||||
return yield* buildEsbuildBrowser(
|
||||
delegateExecutorOptions,
|
||||
builderContext
|
||||
builderContext,
|
||||
/* infrastructureSettings */ undefined,
|
||||
plugins
|
||||
) as AsyncIterable<
|
||||
BuilderOutput & {
|
||||
outputFiles?: OutputFile[];
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Schema } from '@angular-devkit/build-angular/src/builders/browser-esbuild/schema';
|
||||
import type { Schema } from '@angular-devkit/build-angular/src/builders/browser-esbuild/schema';
|
||||
import type { PluginSpec } from '../utilities/esbuild-extensions';
|
||||
|
||||
export interface EsBuildSchema extends Schema {
|
||||
buildLibsFromSource?: boolean;
|
||||
plugins?: string[] | PluginSpec[];
|
||||
}
|
||||
|
||||
@ -441,6 +441,35 @@
|
||||
"type": "boolean",
|
||||
"description": "Read buildable libraries from source instead of building them separately.",
|
||||
"default": true
|
||||
},
|
||||
"plugins": {
|
||||
"description": "A list of ESBuild plugins.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path to the plugin. Relative to the workspace root."
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"description": "The options to provide to the plugin.",
|
||||
"properties": {},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["path"]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The path to the plugin. Relative to the workspace root."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import { registerTsProject } from '@nx/js/src/internal';
|
||||
import type { Plugin } from 'esbuild';
|
||||
import { loadModule } from './module-loader';
|
||||
|
||||
export type PluginSpec = {
|
||||
path: string;
|
||||
options: any;
|
||||
};
|
||||
|
||||
export async function loadPlugins(
|
||||
plugins: string[] | PluginSpec[] | undefined,
|
||||
tsConfig: string
|
||||
): Promise<Plugin[]> {
|
||||
if (!plugins?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cleanupTranspiler = registerTsProject(tsConfig);
|
||||
|
||||
try {
|
||||
return await Promise.all(
|
||||
plugins.map((plugin: string | PluginSpec) => loadPlugin(plugin))
|
||||
);
|
||||
} finally {
|
||||
cleanupTranspiler();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPlugin(pluginSpec: string | PluginSpec): Promise<Plugin> {
|
||||
const pluginPath =
|
||||
typeof pluginSpec === 'string' ? pluginSpec : pluginSpec.path;
|
||||
|
||||
let plugin = await loadModule(pluginPath);
|
||||
|
||||
if (typeof plugin === 'function') {
|
||||
plugin =
|
||||
typeof pluginSpec === 'object' ? plugin(pluginSpec.options) : plugin();
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}
|
||||
55
packages/angular/src/executors/utilities/module-loader.ts
Normal file
55
packages/angular/src/executors/utilities/module-loader.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { extname } from 'path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
export async function loadModule<T = any>(path: string): Promise<T> {
|
||||
switch (extname(path)) {
|
||||
case '.mjs': {
|
||||
const result = await loadEsmModule(pathToFileURL(path));
|
||||
return (result as { default: T }).default ?? (result as T);
|
||||
}
|
||||
case '.cjs': {
|
||||
const result = require(path);
|
||||
return result.default ?? result;
|
||||
}
|
||||
|
||||
default:
|
||||
// it can be CommonJS or ESM, try both
|
||||
try {
|
||||
const result = require(path);
|
||||
return result.default ?? result;
|
||||
} catch (e: any) {
|
||||
if (e.code === 'ERR_REQUIRE_ESM') {
|
||||
const result = await loadEsmModule(pathToFileURL(path));
|
||||
return (result as { default: T }).default ?? (result as T);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily compiled dynamic import loader function.
|
||||
*/
|
||||
let load: (<T>(modulePath: string | URL) => Promise<T>) | undefined;
|
||||
|
||||
/**
|
||||
* This uses a dynamic import to load a module which may be ESM.
|
||||
* CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
|
||||
* will currently, unconditionally downlevel dynamic import into a require call.
|
||||
* require calls cannot load ESM code and will result in a runtime error. To workaround
|
||||
* this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
|
||||
* Once TypeScript provides support for keeping the dynamic import this workaround can
|
||||
* be dropped.
|
||||
*
|
||||
* @param modulePath The path of the module to load.
|
||||
* @returns A Promise that resolves to the dynamically imported module.
|
||||
*/
|
||||
export function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
|
||||
load ??= new Function('modulePath', `return import(modulePath);`) as Exclude<
|
||||
typeof load,
|
||||
undefined
|
||||
>;
|
||||
|
||||
return load(modulePath);
|
||||
}
|
||||
@ -1,31 +1,7 @@
|
||||
import { loadEsmModule } from './module-loader';
|
||||
|
||||
export function ngCompilerCli(): Promise<
|
||||
typeof import('@angular/compiler-cli')
|
||||
> {
|
||||
return loadEsmModule('@angular/compiler-cli');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily compiled dynamic import loader function.
|
||||
*/
|
||||
let load: (<T>(modulePath: string | URL) => Promise<T>) | undefined;
|
||||
|
||||
/**
|
||||
* This uses a dynamic import to load a module which may be ESM.
|
||||
* CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
|
||||
* will currently, unconditionally downlevel dynamic import into a require call.
|
||||
* require calls cannot load ESM code and will result in a runtime error. To workaround
|
||||
* this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
|
||||
* Once TypeScript provides support for keeping the dynamic import this workaround can
|
||||
* be dropped.
|
||||
*
|
||||
* @param modulePath The path of the module to load.
|
||||
* @returns A Promise that resolves to the dynamically imported module.
|
||||
*/
|
||||
export function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
|
||||
load ??= new Function('modulePath', `return import(modulePath);`) as Exclude<
|
||||
typeof load,
|
||||
undefined
|
||||
>;
|
||||
|
||||
return load(modulePath);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user