Skip to content

Commit b5edaa0

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular-devkit/build-angular): add experimental extension points to Vite-based dev server
When using the experimental programmatic API for the development server with an esbuild-based builder (`application`/`browser-esbuild`), express compatible middleware can now be added. Also, the index HTML transformer that previously only worked with the Webpack-based development server is also now enabled. However, usage of these options may result in unexpected application output and/or build failures. They are also not officially supported and SemVer guarantees are not present. Stable and supported methods for build process extension are being evaluated for a future release. (cherry picked from commit bcd3a86)
1 parent cb090d6 commit b5edaa0

File tree

3 files changed

+57
-9
lines changed

3 files changed

+57
-9
lines changed

goldens/public-api/angular_devkit/build_angular/index.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
55
```ts
66

7+
/// <reference types="node" />
8+
79
import { BuilderContext } from '@angular-devkit/architect';
810
import { BuilderOutput } from '@angular-devkit/architect';
911
import type { ConfigOptions } from 'karma';
1012
import { Configuration } from 'webpack';
1113
import { DevServerBuildOutput } from '@angular-devkit/build-webpack';
14+
import type http from 'node:http';
1215
import { json } from '@angular-devkit/core';
1316
import { Observable } from 'rxjs';
1417
import { OutputFile } from 'esbuild';
@@ -204,7 +207,10 @@ export function executeDevServerBuilder(options: DevServerBuilderOptions, contex
204207
webpackConfiguration?: ExecutionTransformer<Configuration>;
205208
logging?: WebpackLoggingCallback;
206209
indexHtml?: IndexHtmlTransform;
207-
}, plugins?: Plugin_2[]): Observable<DevServerBuilderOutput>;
210+
}, extensions?: {
211+
buildPlugins?: Plugin_2[];
212+
middleware?: ((req: http.IncomingMessage, res: http.ServerResponse, next: (err?: unknown) => void) => void)[];
213+
}): Observable<DevServerBuilderOutput>;
208214

209215
// @public
210216
export function executeExtractI18nBuilder(options: ExtractI18nBuilderOptions, context: BuilderContext, transforms?: {

packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import type { BuilderContext } from '@angular-devkit/architect';
1010
import type { Plugin } from 'esbuild';
11+
import type http from 'node:http';
1112
import { EMPTY, Observable, defer, switchMap } from 'rxjs';
1213
import type { ExecutionTransformer } from '../../transforms';
1314
import { checkPort } from '../../utils/check-port';
@@ -19,10 +20,16 @@ import type { DevServerBuilderOutput } from './webpack-server';
1920

2021
/**
2122
* A Builder that executes a development server based on the provided browser target option.
23+
*
24+
* Usage of the `transforms` and/or `extensions` parameters is NOT supported and may cause
25+
* unexpected build output or build failures.
26+
*
2227
* @param options Dev Server options.
2328
* @param context The build context.
2429
* @param transforms A map of transforms that can be used to hook into some logic (such as
2530
* transforming webpack configuration before passing it to webpack).
31+
* @param extensions An optional object containing an array of build plugins (esbuild-based)
32+
* and/or HTTP request middleware.
2633
*
2734
* @experimental Direct usage of this function is considered experimental.
2835
*/
@@ -34,7 +41,14 @@ export function execute(
3441
logging?: import('@angular-devkit/build-webpack').WebpackLoggingCallback;
3542
indexHtml?: IndexHtmlTransform;
3643
} = {},
37-
plugins?: Plugin[],
44+
extensions?: {
45+
buildPlugins?: Plugin[];
46+
middleware?: ((
47+
req: http.IncomingMessage,
48+
res: http.ServerResponse,
49+
next: (err?: unknown) => void,
50+
) => void)[];
51+
},
3852
): Observable<DevServerBuilderOutput> {
3953
// Determine project name from builder context target
4054
const projectName = context.target?.project;
@@ -52,22 +66,27 @@ export function execute(
5266
builderName === '@angular-devkit/build-angular:browser-esbuild' ||
5367
normalizedOptions.forceEsbuild
5468
) {
55-
if (Object.keys(transforms).length > 0) {
69+
if (transforms?.logging || transforms?.webpackConfiguration) {
5670
throw new Error(
5771
'The `application` and `browser-esbuild` builders do not support Webpack transforms.',
5872
);
5973
}
6074

6175
return defer(() => import('./vite-server')).pipe(
6276
switchMap(({ serveWithVite }) =>
63-
serveWithVite(normalizedOptions, builderName, context, plugins),
77+
serveWithVite(normalizedOptions, builderName, context, transforms, extensions),
6478
),
6579
);
6680
}
6781

68-
if (plugins?.length) {
82+
if (extensions?.buildPlugins?.length) {
6983
throw new Error('Only the `application` and `browser-esbuild` builders support plugins.');
7084
}
85+
if (extensions?.middleware?.length) {
86+
throw new Error(
87+
'Only the `application` and `browser-esbuild` builders support middleware.',
88+
);
89+
}
7190

7291
// Use Webpack for all other browser targets
7392
return defer(() => import('./webpack-server')).pipe(

packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,18 @@ interface OutputFileRecord {
4040
servable: boolean;
4141
}
4242

43+
// eslint-disable-next-line max-lines-per-function
4344
export async function* serveWithVite(
4445
serverOptions: NormalizedDevServerOptions,
4546
builderName: string,
4647
context: BuilderContext,
47-
plugins?: Plugin[],
48+
transformers?: {
49+
indexHtml?: (content: string) => Promise<string>;
50+
},
51+
extensions?: {
52+
middleware?: Connect.NextHandleFunction[];
53+
buildPlugins?: Plugin[];
54+
},
4855
): AsyncIterableIterator<DevServerBuilderOutput> {
4956
// Get the browser configuration from the target name.
5057
const rawBrowserOptions = (await context.getTargetOptions(
@@ -133,7 +140,7 @@ export async function* serveWithVite(
133140
{
134141
write: false,
135142
},
136-
plugins,
143+
extensions?.buildPlugins,
137144
)) {
138145
assert(result.outputFiles, 'Builder did not provide result files.');
139146

@@ -207,6 +214,8 @@ export async function* serveWithVite(
207214
!!browserOptions.ssr,
208215
prebundleTransformer,
209216
target,
217+
extensions?.middleware,
218+
transformers?.indexHtml,
210219
);
211220

212221
server = await createServer(serverConfiguration);
@@ -366,6 +375,8 @@ export async function setupServer(
366375
ssr: boolean,
367376
prebundleTransformer: JavaScriptTransformer,
368377
target: string[],
378+
extensionMiddleware?: Connect.NextHandleFunction[],
379+
indexHtmlTransformer?: (content: string) => Promise<string>,
369380
): Promise<InlineConfig> {
370381
const proxy = await loadProxyConfiguration(
371382
serverOptions.workspaceRoot,
@@ -557,6 +568,10 @@ export async function setupServer(
557568
next();
558569
});
559570

571+
if (extensionMiddleware?.length) {
572+
extensionMiddleware.forEach((middleware) => server.middlewares.use(middleware));
573+
}
574+
560575
// Returning a function, installs middleware after the main transform middleware but
561576
// before the built-in HTML middleware
562577
return () => {
@@ -595,7 +610,9 @@ export async function setupServer(
595610
inlineCriticalCss: false,
596611
});
597612

598-
return content;
613+
return indexHtmlTransformer && content
614+
? await indexHtmlTransformer(content)
615+
: content;
599616
});
600617
}
601618

@@ -617,7 +634,13 @@ export async function setupServer(
617634
if (pathname === '/' || pathname === `/index.html`) {
618635
const rawHtml = outputFiles.get('/index.html')?.contents;
619636
if (rawHtml) {
620-
transformIndexHtmlAndAddHeaders(req.url, rawHtml, res, next);
637+
transformIndexHtmlAndAddHeaders(
638+
req.url,
639+
rawHtml,
640+
res,
641+
next,
642+
indexHtmlTransformer,
643+
);
621644

622645
return;
623646
}

0 commit comments

Comments
 (0)