Skip to content

Commit

Permalink
feat(angular): support providing esbuild plugins to @nx/angular:brows…
Browse files Browse the repository at this point in the history
…er-esbuild
  • Loading branch information
leosvelperez committed Nov 30, 2023
1 parent f2c68a8 commit 5f968ac
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 95 deletions.
29 changes: 29 additions & 0 deletions docs/generated/packages/angular/executors/browser-esbuild.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
86 changes: 75 additions & 11 deletions e2e/angular-core/src/projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,34 @@ 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`
);
app1DefaultModule = readFile(`${app1}/src/app/app.module.ts`);
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(() => {
Expand All @@ -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());
Expand All @@ -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(
Expand Down Expand Up @@ -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`));
Expand All @@ -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
Expand All @@ -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}`)
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -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');
Expand Down
154 changes: 105 additions & 49 deletions packages/angular/src/builders/dev-server/dev-server.impl.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
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,
parseTargetString,
readCachedProjectGraph,
type Target,
} from '@nx/devkit';
import { getRootTsConfigPath } from '@nx/js';
import type { DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils';
import { WebpackNxBuildCoordinationPlugin } from '@nx/webpack/src/plugins/webpack-nx-build-coordination-plugin';
import { existsSync } from 'fs';
import { isNpmProject } from 'nx/src/project-graph/operators';
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
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,
Expand All @@ -31,6 +41,7 @@ type BuildTargetOptions = {
buildLibsFromSource?: boolean;
customWebpackConfig?: { path?: string };
indexFileTransformer?: string;
plugins?: string[] | PluginSpec[];
};

export function executeDevServerBuilder(
Expand Down Expand Up @@ -138,56 +149,66 @@ export function executeDevServerBuilder(
* builders. Since we are using a custom builder, we patch the context to
* handle `@nx/angular:*` executors.
*/
patchBuilderContext(context);

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(',')}`
)
);
patchBuilderContext(context, !isUsingWebpackBuilder, parsedBuildTarget);

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;
}
}

if (!pathToWebpackConfig) {
return baseWebpackConfig;
return mergeCustomWebpackConfig(
baseWebpackConfig,
pathToWebpackConfig,
buildTargetOptions,
context.target
);
}
: undefined,

return mergeCustomWebpackConfig(
baseWebpackConfig,
pathToWebpackConfig,
buildTargetOptions,
context.target
);
}
: undefined,

...(pathToIndexFileTransformer
? {
indexHtml: resolveIndexHtmlTransformer(
pathToIndexFileTransformer,
buildTargetOptions.tsConfig,
context.target
),
}
: {}),
})
...(pathToIndexFileTransformer
? {
indexHtml: resolveIndexHtmlTransformer(
pathToIndexFileTransformer,
buildTargetOptions.tsConfig,
context.target
),
}
: {}),
},
{
buildPlugins: plugins,
}
)
)
);
}
Expand Down Expand Up @@ -223,7 +244,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);
Expand All @@ -234,4 +259,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;
}

0 comments on commit 5f968ac

Please sign in to comment.