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",
|
"type": "boolean",
|
||||||
"description": "Read buildable libraries from source instead of building them separately.",
|
"description": "Read buildable libraries from source instead of building them separately.",
|
||||||
"default": true
|
"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,
|
"additionalProperties": false,
|
||||||
|
|||||||
@ -21,15 +21,22 @@ import { join, normalize } from 'path';
|
|||||||
describe('Angular Projects', () => {
|
describe('Angular Projects', () => {
|
||||||
let proj: string;
|
let proj: string;
|
||||||
const app1 = uniq('app1');
|
const app1 = uniq('app1');
|
||||||
|
const esbuildApp = uniq('esbuild-app');
|
||||||
const lib1 = uniq('lib1');
|
const lib1 = uniq('lib1');
|
||||||
let app1DefaultModule: string;
|
let app1DefaultModule: string;
|
||||||
let app1DefaultComponentTemplate: string;
|
let app1DefaultComponentTemplate: string;
|
||||||
|
let esbuildAppDefaultModule: string;
|
||||||
|
let esbuildAppDefaultComponentTemplate: string;
|
||||||
|
let esbuildAppDefaultProjectConfig: string;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
proj = newProject();
|
proj = newProject();
|
||||||
runCLI(
|
runCLI(
|
||||||
`generate @nx/angular:app ${app1} --no-standalone --bundler=webpack --project-name-and-root-format=as-provided --no-interactive`
|
`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(
|
runCLI(
|
||||||
`generate @nx/angular:lib ${lib1} --no-standalone --add-module-spec --project-name-and-root-format=as-provided --no-interactive`
|
`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(
|
app1DefaultComponentTemplate = readFile(
|
||||||
`${app1}/src/app/app.component.html`
|
`${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(() => {
|
afterEach(() => {
|
||||||
@ -45,6 +57,12 @@ describe('Angular Projects', () => {
|
|||||||
`${app1}/src/app/app.component.html`,
|
`${app1}/src/app/app.component.html`,
|
||||||
app1DefaultComponentTemplate
|
app1DefaultComponentTemplate
|
||||||
);
|
);
|
||||||
|
updateFile(`${esbuildApp}/src/app/app.module.ts`, esbuildAppDefaultModule);
|
||||||
|
updateFile(
|
||||||
|
`${esbuildAppDefaultComponentTemplate}/src/app/app.component.html`,
|
||||||
|
esbuildAppDefaultComponentTemplate
|
||||||
|
);
|
||||||
|
updateFile(`${esbuildApp}/project.json`, esbuildAppDefaultProjectConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => cleanupProject());
|
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`
|
`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(
|
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(
|
updateFile(
|
||||||
@ -86,11 +104,12 @@ describe('Angular Projects', () => {
|
|||||||
|
|
||||||
// check build
|
// check build
|
||||||
runCLI(
|
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/${app1}/main.js`);
|
||||||
|
checkFilesExist(`dist/${esbuildApp}/browser/main.js`);
|
||||||
checkFilesExist(`dist/my-dir/${standaloneApp}/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
|
// This is a loose requirement because there are a lot of
|
||||||
// influences external from this project that affect this.
|
// influences external from this project that affect this.
|
||||||
const es2015BundleSize = getSize(tmpProjPath(`dist/${app1}/main.js`));
|
const es2015BundleSize = getSize(tmpProjPath(`dist/${app1}/main.js`));
|
||||||
@ -101,7 +120,7 @@ describe('Angular Projects', () => {
|
|||||||
|
|
||||||
// check unit tests
|
// check unit tests
|
||||||
runCLI(
|
runCLI(
|
||||||
`run-many --target test --projects=${app1},${standaloneApp},${esbuildApp},${lib1} --parallel`
|
`run-many --target test --projects=${app1},${standaloneApp},${esbuildStandaloneApp},${lib1} --parallel`
|
||||||
);
|
);
|
||||||
|
|
||||||
// check e2e tests
|
// check e2e tests
|
||||||
@ -121,7 +140,7 @@ describe('Angular Projects', () => {
|
|||||||
await killProcessAndPorts(process.pid, appPort);
|
await killProcessAndPorts(process.pid, appPort);
|
||||||
|
|
||||||
const esbProcess = await runCommandUntil(
|
const esbProcess = await runCommandUntil(
|
||||||
`serve ${esbuildApp} -- --port=${appPort}`,
|
`serve ${esbuildStandaloneApp} -- --port=${appPort}`,
|
||||||
(output) =>
|
(output) =>
|
||||||
output.includes(`Application bundle generation complete`) &&
|
output.includes(`Application bundle generation complete`) &&
|
||||||
output.includes(`localhost:${appPort}`)
|
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 () => {
|
it('should build the dependent buildable lib and its child lib, as well as the app', async () => {
|
||||||
// ARRANGE
|
// 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 buildableLib = uniq('buildlib1');
|
||||||
const buildableChildLib = uniq('buildlib2');
|
const buildableChildLib = uniq('buildlib2');
|
||||||
|
|
||||||
@ -328,6 +342,56 @@ describe('Angular Projects', () => {
|
|||||||
expect(mainEsBuildBundle).toContain(`dist/${buildableLib}`);
|
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', () => {
|
it('should build publishable libs successfully', () => {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
const lib = uniq('lib');
|
const lib = uniq('lib');
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import type { BuilderContext } from '@angular-devkit/architect';
|
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 {
|
import {
|
||||||
joinPathFragments,
|
joinPathFragments,
|
||||||
normalizePath,
|
normalizePath,
|
||||||
parseTargetString,
|
parseTargetString,
|
||||||
readCachedProjectGraph,
|
readCachedProjectGraph,
|
||||||
|
type Target,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { getRootTsConfigPath } from '@nx/js';
|
import { getRootTsConfigPath } from '@nx/js';
|
||||||
import type { DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils';
|
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 { isNpmProject } from 'nx/src/project-graph/operators';
|
||||||
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
|
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
|
||||||
import { relative } from 'path';
|
import { relative } from 'path';
|
||||||
import { from } from 'rxjs';
|
import { combineLatest, from } from 'rxjs';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
import { getInstalledAngularVersionInfo } from '../../executors/utilities/angular-version-utils';
|
import { getInstalledAngularVersionInfo } from '../../executors/utilities/angular-version-utils';
|
||||||
|
import {
|
||||||
|
loadPlugins,
|
||||||
|
type PluginSpec,
|
||||||
|
} from '../../executors/utilities/esbuild-extensions';
|
||||||
import { createTmpTsConfigForBuildableLibs } from '../utilities/buildable-libs';
|
import { createTmpTsConfigForBuildableLibs } from '../utilities/buildable-libs';
|
||||||
import {
|
import {
|
||||||
mergeCustomWebpackConfig,
|
mergeCustomWebpackConfig,
|
||||||
@ -33,6 +43,7 @@ type BuildTargetOptions = {
|
|||||||
buildLibsFromSource?: boolean;
|
buildLibsFromSource?: boolean;
|
||||||
customWebpackConfig?: { path?: string };
|
customWebpackConfig?: { path?: string };
|
||||||
indexFileTransformer?: string;
|
indexFileTransformer?: string;
|
||||||
|
plugins?: string[] | PluginSpec[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function executeDevServerBuilder(
|
export function executeDevServerBuilder(
|
||||||
@ -143,56 +154,66 @@ export function executeDevServerBuilder(
|
|||||||
* builders. Since we are using a custom builder, we patch the context to
|
* builders. Since we are using a custom builder, we patch the context to
|
||||||
* handle `@nx/angular:*` executors.
|
* handle `@nx/angular:*` executors.
|
||||||
*/
|
*/
|
||||||
patchBuilderContext(context);
|
patchBuilderContext(context, !isUsingWebpackBuilder, parsedBuildTarget);
|
||||||
|
|
||||||
return from(import('@angular-devkit/build-angular')).pipe(
|
return combineLatest([
|
||||||
switchMap(({ executeDevServerBuilder }) =>
|
from(import('@angular-devkit/build-angular')),
|
||||||
executeDevServerBuilder(delegateBuilderOptions, context, {
|
from(loadPlugins(buildTargetOptions.plugins, buildTargetOptions.tsConfig)),
|
||||||
webpackConfiguration: isUsingWebpackBuilder
|
]).pipe(
|
||||||
? async (baseWebpackConfig) => {
|
switchMap(([{ executeDevServerBuilder }, plugins]) =>
|
||||||
if (!buildLibsFromSource) {
|
executeDevServerBuilder(
|
||||||
const workspaceDependencies = dependencies
|
delegateBuilderOptions,
|
||||||
.filter((dep) => !isNpmProject(dep.node))
|
context,
|
||||||
.map((dep) => dep.node.name);
|
{
|
||||||
// default for `nx run-many` is --all projects
|
webpackConfiguration: isUsingWebpackBuilder
|
||||||
// by passing an empty string for --projects, run-many will default to
|
? async (baseWebpackConfig) => {
|
||||||
// run the target for all projects.
|
if (!buildLibsFromSource) {
|
||||||
// This will occur when workspaceDependencies = []
|
const workspaceDependencies = dependencies
|
||||||
if (workspaceDependencies.length > 0) {
|
.filter((dep) => !isNpmProject(dep.node))
|
||||||
baseWebpackConfig.plugins.push(
|
.map((dep) => dep.node.name);
|
||||||
// @ts-expect-error - difference between angular and webpack plugin definitions bc of webpack versions
|
// default for `nx run-many` is --all projects
|
||||||
new WebpackNxBuildCoordinationPlugin(
|
// by passing an empty string for --projects, run-many will default to
|
||||||
`nx run-many --target=${
|
// run the target for all projects.
|
||||||
parsedBuildTarget.target
|
// This will occur when workspaceDependencies = []
|
||||||
} --projects=${workspaceDependencies.join(',')}`
|
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) {
|
...(pathToIndexFileTransformer
|
||||||
return baseWebpackConfig;
|
? {
|
||||||
|
indexHtml: resolveIndexHtmlTransformer(
|
||||||
|
pathToIndexFileTransformer,
|
||||||
|
buildTargetOptions.tsConfig,
|
||||||
|
context.target
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
: {}),
|
||||||
return mergeCustomWebpackConfig(
|
},
|
||||||
baseWebpackConfig,
|
{
|
||||||
pathToWebpackConfig,
|
buildPlugins: plugins,
|
||||||
buildTargetOptions,
|
}
|
||||||
context.target
|
)
|
||||||
);
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
|
|
||||||
...(pathToIndexFileTransformer
|
|
||||||
? {
|
|
||||||
indexHtml: resolveIndexHtmlTransformer(
|
|
||||||
pathToIndexFileTransformer,
|
|
||||||
buildTargetOptions.tsConfig,
|
|
||||||
context.target
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -228,7 +249,11 @@ const executorToBuilderMap = new Map<string, string>([
|
|||||||
'@angular-devkit/build-angular:browser-esbuild',
|
'@angular-devkit/build-angular:browser-esbuild',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
function patchBuilderContext(context: BuilderContext): void {
|
function patchBuilderContext(
|
||||||
|
context: BuilderContext,
|
||||||
|
isUsingEsbuildBuilder: boolean,
|
||||||
|
buildTarget: Target
|
||||||
|
): void {
|
||||||
const originalGetBuilderNameForTarget = context.getBuilderNameForTarget;
|
const originalGetBuilderNameForTarget = context.getBuilderNameForTarget;
|
||||||
context.getBuilderNameForTarget = async (target) => {
|
context.getBuilderNameForTarget = async (target) => {
|
||||||
const builderName = await originalGetBuilderNameForTarget(target);
|
const builderName = await originalGetBuilderNameForTarget(target);
|
||||||
@ -239,4 +264,35 @@ function patchBuilderContext(context: BuilderContext): void {
|
|||||||
|
|
||||||
return builderName;
|
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 { BuilderOutput } from '@angular-devkit/architect';
|
||||||
import { type DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils';
|
import { readCachedProjectGraph, type ExecutorContext } from '@nx/devkit';
|
||||||
import { type ExecutorContext, readCachedProjectGraph } from '@nx/devkit';
|
import type { DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils';
|
||||||
import { createTmpTsConfigForBuildableLibs } from './lib/buildable-libs';
|
import type { OutputFile } from 'esbuild';
|
||||||
import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter';
|
import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter';
|
||||||
import { type BuilderOutput } from '@angular-devkit/architect';
|
import { loadPlugins } from '../utilities/esbuild-extensions';
|
||||||
import { type OutputFile } from 'esbuild';
|
import { createTmpTsConfigForBuildableLibs } from './lib/buildable-libs';
|
||||||
|
import type { EsBuildSchema } from './schema';
|
||||||
|
|
||||||
export default async function* esbuildExecutor(
|
export default async function* esbuildExecutor(
|
||||||
options: EsBuildSchema,
|
options: EsBuildSchema,
|
||||||
@ -12,7 +13,11 @@ export default async function* esbuildExecutor(
|
|||||||
) {
|
) {
|
||||||
options.buildLibsFromSource ??= true;
|
options.buildLibsFromSource ??= true;
|
||||||
|
|
||||||
const { buildLibsFromSource, ...delegateExecutorOptions } = options;
|
const {
|
||||||
|
buildLibsFromSource,
|
||||||
|
plugins: pluginPaths,
|
||||||
|
...delegateExecutorOptions
|
||||||
|
} = options;
|
||||||
|
|
||||||
let dependencies: DependentBuildableProjectNode[];
|
let dependencies: DependentBuildableProjectNode[];
|
||||||
let projectGraph = context.projectGraph;
|
let projectGraph = context.projectGraph;
|
||||||
@ -28,6 +33,9 @@ export default async function* esbuildExecutor(
|
|||||||
dependencies = foundDependencies;
|
dependencies = foundDependencies;
|
||||||
delegateExecutorOptions.tsConfig = tsConfigPath;
|
delegateExecutorOptions.tsConfig = tsConfigPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const plugins = await loadPlugins(pluginPaths, options.tsConfig);
|
||||||
|
|
||||||
const { buildEsbuildBrowser } = await import(
|
const { buildEsbuildBrowser } = await import(
|
||||||
'@angular-devkit/build-angular/src/builders/browser-esbuild/index'
|
'@angular-devkit/build-angular/src/builders/browser-esbuild/index'
|
||||||
);
|
);
|
||||||
@ -45,7 +53,9 @@ export default async function* esbuildExecutor(
|
|||||||
|
|
||||||
return yield* buildEsbuildBrowser(
|
return yield* buildEsbuildBrowser(
|
||||||
delegateExecutorOptions,
|
delegateExecutorOptions,
|
||||||
builderContext
|
builderContext,
|
||||||
|
/* infrastructureSettings */ undefined,
|
||||||
|
plugins
|
||||||
) as AsyncIterable<
|
) as AsyncIterable<
|
||||||
BuilderOutput & {
|
BuilderOutput & {
|
||||||
outputFiles?: OutputFile[];
|
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 {
|
export interface EsBuildSchema extends Schema {
|
||||||
buildLibsFromSource?: boolean;
|
buildLibsFromSource?: boolean;
|
||||||
|
plugins?: string[] | PluginSpec[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -441,6 +441,35 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Read buildable libraries from source instead of building them separately.",
|
"description": "Read buildable libraries from source instead of building them separately.",
|
||||||
"default": true
|
"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,
|
"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<
|
export function ngCompilerCli(): Promise<
|
||||||
typeof import('@angular/compiler-cli')
|
typeof import('@angular/compiler-cli')
|
||||||
> {
|
> {
|
||||||
return loadEsmModule('@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