Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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.push(...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(middlewarePath);
} 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