Skip to content

Commit 875f654

Browse files
committed
feat(@angular/build): support middlewareConfig
1 parent f98cc82 commit 875f654

File tree

8 files changed

+197
-13
lines changed

8 files changed

+197
-13
lines changed

packages/angular/build/src/builders/dev-server/options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export async function normalizeOptions(
115115
sslKey,
116116
prebundle,
117117
allowedHosts,
118+
middlewareConfig,
118119
} = options;
119120

120121
// Return all the normalized options
@@ -142,5 +143,6 @@ export async function normalizeOptions(
142143
prebundle: cacheOptions.enabled && !optimization.scripts && prebundle,
143144
inspect,
144145
allowedHosts: allowedHosts ? allowedHosts : [],
146+
middlewareConfig,
145147
};
146148
}

packages/angular/build/src/builders/dev-server/schema.json

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,29 +110,43 @@
110110
"type": "string",
111111
"description": "Activate the inspector on host and port in the format of `[[host:]port]`. See the security warning in https://nodejs.org/docs/latest-v22.x/api/cli.html#warning-binding-inspector-to-a-public-ipport-combination-is-insecure regarding the host parameter usage."
112112
},
113-
{ "type": "boolean" }
113+
{
114+
"type": "boolean"
115+
}
114116
]
115117
},
116118
"prebundle": {
117119
"description": "Enable and control the Vite-based development server's prebundling capabilities. To enable prebundling, the Angular CLI cache must also be enabled.",
118120
"default": true,
119121
"oneOf": [
120-
{ "type": "boolean" },
122+
{
123+
"type": "boolean"
124+
},
121125
{
122126
"type": "object",
123127
"properties": {
124128
"exclude": {
125129
"description": "List of package imports that should not be prebundled by the development server. The packages will be bundled into the application code itself. Note: specifying `@foo/bar` marks all paths within the `@foo/bar` package as excluded, including sub-paths like `@foo/bar/baz`.",
126130
"type": "array",
127-
"items": { "type": "string" }
131+
"items": {
132+
"type": "string"
133+
}
128134
}
129135
},
130136
"additionalProperties": false,
131-
"required": ["exclude"]
137+
"required": [
138+
"exclude"
139+
]
132140
}
133141
]
142+
},
143+
"middlewareConfig": {
144+
"type": "string",
145+
"description": "Middleware configuration file."
134146
}
135147
},
136148
"additionalProperties": false,
137-
"required": ["buildTarget"]
138-
}
149+
"required": [
150+
"buildTarget"
151+
]
152+
}

packages/angular/build/src/builders/dev-server/vite/server.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
createRemoveIdPrefixPlugin,
2020
} from '../../../tools/vite/plugins';
2121
import { EsbuildLoaderOption, getDepOptimizationConfig } from '../../../tools/vite/utils';
22-
import { loadProxyConfiguration } from '../../../utils';
22+
import { loadMiddlewareConfiguration, loadProxyConfiguration } from '../../../utils';
2323
import { type ApplicationBuilderInternalOptions, JavaScriptTransformer } from '../internal';
2424
import type { NormalizedDevServerOptions } from '../options';
2525
import { DevServerExternalResultMetadata, OutputAssetRecord, OutputFileRecord } from './utils';
@@ -153,6 +153,19 @@ export async function setupServer(
153153
join(serverOptions.workspaceRoot, `.angular/vite-root`, serverOptions.buildTarget.project),
154154
);
155155

156+
if (serverOptions.middlewareConfig) {
157+
const middleware = await loadMiddlewareConfiguration(
158+
serverOptions.workspaceRoot,
159+
serverOptions.middlewareConfig,
160+
);
161+
162+
if (extensionMiddleware) {
163+
extensionMiddleware.push(...middleware);
164+
} else {
165+
extensionMiddleware = middleware;
166+
}
167+
}
168+
156169
/**
157170
* Required when using `externalDependencies` to prevent Vite load errors.
158171
*

packages/angular/build/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export * from './normalize-asset-patterns';
1010
export * from './normalize-optimization';
1111
export * from './normalize-source-maps';
1212
export * from './load-proxy-config';
13+
export * from './load-middleware-config';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
import { existsSync } from 'node:fs';
9+
import { resolve } from 'node:path';
10+
import { pathToFileURL } from 'node:url';
11+
import type { Connect } from 'vite';
12+
import { assertIsError } from './error';
13+
import { loadEsmModule } from './load-esm';
14+
15+
export async function loadMiddlewareConfiguration(
16+
root: string,
17+
middlewareConfig: string | undefined,
18+
): Promise<Connect.NextHandleFunction[]> {
19+
if (!middlewareConfig) {
20+
return [];
21+
}
22+
23+
const middlewarePath = resolve(root, middlewareConfig);
24+
25+
if (!existsSync(middlewarePath)) {
26+
throw new Error(`Middleware configuration file ${middlewarePath} does not exist.`);
27+
}
28+
29+
let middlewareConfiguration;
30+
31+
try {
32+
middlewareConfiguration = await import(middlewarePath);
33+
} catch (e) {
34+
assertIsError(e);
35+
if (e.code !== 'ERR_REQUIRE_ASYNC_MODULE') {
36+
throw e;
37+
}
38+
39+
middlewareConfiguration = await loadEsmModule<{ default: unknown }>(
40+
pathToFileURL(middlewarePath),
41+
);
42+
}
43+
44+
if ('default' in middlewareConfiguration) {
45+
middlewareConfiguration = middlewareConfiguration.default;
46+
}
47+
48+
return Array.isArray(middlewareConfiguration)
49+
? middlewareConfiguration
50+
: [middlewareConfiguration];
51+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export function execute(
9898
hmr: boolean;
9999
allowedHosts: true | string[];
100100
define: { [key: string]: string } | undefined;
101+
middlewareConfig: string;
101102
},
102103
builderName,
103104
(options, context, codePlugins) => {

packages/angular_devkit/build_angular/src/builders/dev-server/schema.json

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@
103103
"type": "string",
104104
"description": "Activate the inspector on host and port in the format of `[[host:]port]`. See the security warning in https://nodejs.org/docs/latest-v22.x/api/cli.html#warning-binding-inspector-to-a-public-ipport-combination-is-insecure regarding the host parameter usage."
105105
},
106-
{ "type": "boolean" }
106+
{
107+
"type": "boolean"
108+
}
107109
]
108110
},
109111
"forceEsbuild": {
@@ -114,22 +116,34 @@
114116
"prebundle": {
115117
"description": "Enable and control the Vite-based development server's prebundling capabilities. To enable prebundling, the Angular CLI cache must also be enabled. This option has no effect when using the 'browser' or other Webpack-based builders.",
116118
"oneOf": [
117-
{ "type": "boolean" },
119+
{
120+
"type": "boolean"
121+
},
118122
{
119123
"type": "object",
120124
"properties": {
121125
"exclude": {
122126
"description": "List of package imports that should not be prebundled by the development server. The packages will be bundled into the application code itself.",
123127
"type": "array",
124-
"items": { "type": "string" }
128+
"items": {
129+
"type": "string"
130+
}
125131
}
126132
},
127133
"additionalProperties": false,
128-
"required": ["exclude"]
134+
"required": [
135+
"exclude"
136+
]
129137
}
130138
]
139+
},
140+
"middlewareConfig": {
141+
"type": "string",
142+
"description": "Middleware configuration file."
131143
}
132144
},
133145
"additionalProperties": false,
134-
"required": ["buildTarget"]
135-
}
146+
"required": [
147+
"buildTarget"
148+
]
149+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { createServer } from 'node:http';
10+
import { executeDevServer } from '../../index';
11+
import { executeOnceAndFetch } from '../execute-fetch';
12+
import { describeServeBuilder } from '../jasmine-helpers';
13+
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
14+
15+
describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => {
16+
describe('option: "middlewareConfig"', () => {
17+
beforeEach(async () => {
18+
setupTarget(harness);
19+
20+
// Application code is not needed for these tests
21+
await harness.writeFile('src/main.ts', '');
22+
});
23+
24+
it('middleware configuration export single function (CommonJS)', async () => {
25+
harness.useTarget('serve', {
26+
...BASE_OPTIONS,
27+
middlewareConfig: 'middleware.config.js',
28+
});
29+
const proxyServer = await createProxyServer();
30+
try {
31+
await harness.writeFiles({
32+
'middleware.config.js': `module.exports = (req, res, next) => { res.end('TEST_MIDDLEWARE'); next();}`,
33+
});
34+
35+
const { result, response } = await executeOnceAndFetch(harness, '/test');
36+
37+
expect(result?.success).toBeTrue();
38+
expect(await response?.text()).toContain('TEST_MIDDLEWARE');
39+
} finally {
40+
await proxyServer.close();
41+
}
42+
});
43+
44+
it('middleware configuration export an array of multiple functions (CommonJS)', async () => {
45+
harness.useTarget('serve', {
46+
...BASE_OPTIONS,
47+
middlewareConfig: 'middleware.config.js',
48+
});
49+
const proxyServer = await createProxyServer();
50+
try {
51+
await harness.writeFiles({
52+
'middleware.config.js': `module.exports = [(req, res, next) => { next();}, (req, res, next) => { res.end('TEST_MIDDLEWARE'); next();}]`,
53+
});
54+
55+
const { result, response } = await executeOnceAndFetch(harness, '/test');
56+
57+
expect(result?.success).toBeTrue();
58+
expect(await response?.text()).toContain('TEST_MIDDLEWARE');
59+
} finally {
60+
await proxyServer.close();
61+
}
62+
});
63+
});
64+
});
65+
66+
/**
67+
* Creates an HTTP Server used for proxy testing that provides a `/test` endpoint
68+
* that returns a 200 response with a body of `TEST_API_RETURN`. All other requests
69+
* will return a 404 response.
70+
*/
71+
async function createProxyServer() {
72+
const proxyServer = createServer((request, response) => {
73+
if (request.url?.endsWith('/test')) {
74+
response.writeHead(200);
75+
response.end('TEST_API_RETURN');
76+
} else {
77+
response.writeHead(404);
78+
response.end();
79+
}
80+
});
81+
82+
await new Promise<void>((resolve) => proxyServer.listen(0, '127.0.0.1', resolve));
83+
84+
return {
85+
address: proxyServer.address() as import('net').AddressInfo,
86+
close: () => new Promise<void>((resolve) => proxyServer.close(() => resolve())),
87+
};
88+
}

0 commit comments

Comments
 (0)