Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/angular/build/src/builders/dev-server/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export async function normalizeOptions(
sslKey,
prebundle,
allowedHosts,
middlewareConfig,
} = options;

// Return all the normalized options
Expand Down Expand Up @@ -142,5 +143,6 @@ export async function normalizeOptions(
prebundle: cacheOptions.enabled && !optimization.scripts && prebundle,
inspect,
allowedHosts: allowedHosts ? allowedHosts : [],
middlewareConfig,
};
}
26 changes: 20 additions & 6 deletions packages/angular/build/src/builders/dev-server/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,29 +110,43 @@
"type": "string",
"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."
},
{ "type": "boolean" }
{
"type": "boolean"
}
]
},
"prebundle": {
"description": "Enable and control the Vite-based development server's prebundling capabilities. To enable prebundling, the Angular CLI cache must also be enabled.",
"default": true,
"oneOf": [
{ "type": "boolean" },
{
"type": "boolean"
},
{
"type": "object",
"properties": {
"exclude": {
"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`.",
"type": "array",
"items": { "type": "string" }
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"required": ["exclude"]
"required": [
"exclude"
]
}
]
},
"middlewareConfig": {
"type": "string",
"description": "Middleware configuration file."
}
},
"additionalProperties": false,
"required": ["buildTarget"]
}
"required": [
"buildTarget"
]
}
15 changes: 14 additions & 1 deletion packages/angular/build/src/builders/dev-server/vite/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
createRemoveIdPrefixPlugin,
} from '../../../tools/vite/plugins';
import { EsbuildLoaderOption, getDepOptimizationConfig } from '../../../tools/vite/utils';
import { loadProxyConfiguration } from '../../../utils';
import { loadMiddlewareConfiguration, loadProxyConfiguration } from '../../../utils';
import { type ApplicationBuilderInternalOptions, JavaScriptTransformer } from '../internal';
import type { NormalizedDevServerOptions } from '../options';
import { DevServerExternalResultMetadata, OutputAssetRecord, OutputFileRecord } from './utils';
Expand Down Expand Up @@ -153,6 +153,19 @@ export async function setupServer(
join(serverOptions.workspaceRoot, `.angular/vite-root`, serverOptions.buildTarget.project),
);

if (serverOptions.middlewareConfig) {
const middleware = await loadMiddlewareConfiguration(
serverOptions.workspaceRoot,
serverOptions.middlewareConfig,
);

if (extensionMiddleware) {
extensionMiddleware = [...extensionMiddleware, ...middleware];
} else {
extensionMiddleware = middleware;
}
}

/**
* Required when using `externalDependencies` to prevent Vite load errors.
*
Expand Down
1 change: 1 addition & 0 deletions packages/angular/build/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './normalize-asset-patterns';
export * from './normalize-optimization';
export * from './normalize-source-maps';
export * from './load-proxy-config';
export * from './load-middleware-config';
51 changes: 51 additions & 0 deletions packages/angular/build/src/utils/load-middleware-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import { existsSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import type { Connect } from 'vite';
import { assertIsError } from './error';
import { loadEsmModule } from './load-esm';

export async function loadMiddlewareConfiguration(
root: string,
middlewareConfig: string | undefined,
): Promise<Connect.NextHandleFunction[]> {
if (!middlewareConfig) {
return [];
}

const middlewarePath = resolve(root, middlewareConfig);

if (!existsSync(middlewarePath)) {
throw new Error(`Middleware configuration file ${middlewarePath} does not exist.`);
}

let middlewareConfiguration;

try {
middlewareConfiguration = await import(pathToFileURL(middlewarePath).href);
} catch (e) {
assertIsError(e);
if (e.code !== 'ERR_REQUIRE_ASYNC_MODULE') {
throw e;
}

middlewareConfiguration = await loadEsmModule<{ default: unknown }>(
pathToFileURL(middlewarePath),
);
}

if ('default' in middlewareConfiguration) {
middlewareConfiguration = middlewareConfiguration.default;
}

return Array.isArray(middlewareConfiguration)
? middlewareConfiguration
: [middlewareConfiguration];
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export function execute(
hmr: boolean;
allowedHosts: true | string[];
define: { [key: string]: string } | undefined;
middlewareConfig?: string;
},
builderName,
(options, context, codePlugins) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@
"type": "string",
"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."
},
{ "type": "boolean" }
{
"type": "boolean"
}
]
},
"forceEsbuild": {
Expand All @@ -114,22 +116,34 @@
"prebundle": {
"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.",
"oneOf": [
{ "type": "boolean" },
{
"type": "boolean"
},
{
"type": "object",
"properties": {
"exclude": {
"description": "List of package imports that should not be prebundled by the development server. The packages will be bundled into the application code itself.",
"type": "array",
"items": { "type": "string" }
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"required": ["exclude"]
"required": [
"exclude"
]
}
]
},
"middlewareConfig": {
"type": "string",
"description": "Middleware configuration file."
}
},
"additionalProperties": false,
"required": ["buildTarget"]
}
"required": [
"buildTarget"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { createServer } from 'node:http';
import { executeDevServer } from '../../index';
import { executeOnceAndFetch } from '../execute-fetch';
import { describeServeBuilder } from '../jasmine-helpers';
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';

describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => {
describe('option: "middlewareConfig"', () => {
beforeEach(async () => {
setupTarget(harness);

// Application code is not needed for these tests
await harness.writeFile('src/main.ts', '');
});

it('middleware configuration export single function (CommonJS)', async () => {
harness.useTarget('serve', {
...BASE_OPTIONS,
middlewareConfig: 'middleware.config.js',
});
const proxyServer = await createProxyServer();
try {
await harness.writeFiles({
'middleware.config.js': `module.exports = (req, res, next) => { res.end('TEST_MIDDLEWARE'); next();}`,
});

const { result, response } = await executeOnceAndFetch(harness, '/test');

expect(result?.success).toBeTrue();
expect(await response?.text()).toContain('TEST_MIDDLEWARE');
} finally {
await proxyServer.close();
}
});

it('middleware configuration export an array of multiple functions (CommonJS)', async () => {
harness.useTarget('serve', {
...BASE_OPTIONS,
middlewareConfig: 'middleware.config.js',
});
const proxyServer = await createProxyServer();
try {
await harness.writeFiles({
'middleware.config.js': `module.exports = [(req, res, next) => { next();}, (req, res, next) => { res.end('TEST_MIDDLEWARE'); next();}]`,
});

const { result, response } = await executeOnceAndFetch(harness, '/test');

expect(result?.success).toBeTrue();
expect(await response?.text()).toContain('TEST_MIDDLEWARE');
} finally {
await proxyServer.close();
}
});
});
});

/**
* Creates an HTTP Server used for proxy testing that provides a `/test` endpoint
* that returns a 200 response with a body of `TEST_API_RETURN`. All other requests
* will return a 404 response.
*/
async function createProxyServer() {
const proxyServer = createServer((request, response) => {
if (request.url?.endsWith('/test')) {
response.writeHead(200);
response.end('TEST_API_RETURN');
} else {
response.writeHead(404);
response.end();
}
});

await new Promise<void>((resolve) => proxyServer.listen(0, '127.0.0.1', resolve));

return {
address: proxyServer.address() as import('net').AddressInfo,
close: () => new Promise<void>((resolve) => proxyServer.close(() => resolve())),
};
}
Loading