Skip to content

Commit fb1ad7c

Browse files
committed
feat(@angular-devkit/build-angular): support ESM proxy configuration files for the dev server
The `proxyConfig` option now supports loading ESM configuration files in addition to JSON and CommonJS files. ESM files (such as those ending with `.mjs`) must provide a default export with the configuration object. For example, a `proxy.config.mjs` containing the follow is now possible: ``` export default { "/api/*": { "target": "http://127.0.0.1:5001" } }; ``` Closes #21623
1 parent 13cceab commit fb1ad7c

File tree

2 files changed

+85
-15
lines changed

2 files changed

+85
-15
lines changed

packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/proxy-config_spec.ts

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,67 @@ describeBuilder(serveWebpackBrowser, DEV_SERVER_BUILDER_INFO, (harness) => {
2525
await harness.writeFile('src/main.ts', '');
2626
});
2727

28-
it('proxies requests based on the proxy configuration file provided in the option', async () => {
28+
it('proxies requests based on the JSON proxy configuration file provided in the option', async () => {
2929
harness.useTarget('serve', {
3030
...BASE_OPTIONS,
3131
proxyConfig: 'proxy.config.json',
3232
});
3333

34-
const proxyServer = http.createServer((request, response) => {
35-
if (request.url?.endsWith('/test')) {
36-
response.writeHead(200);
37-
response.end('TEST_API_RETURN');
38-
} else {
39-
response.writeHead(404);
40-
response.end();
41-
}
34+
const proxyServer = createProxyServer();
35+
try {
36+
await new Promise<void>((resolve) => proxyServer.listen(0, '127.0.0.1', resolve));
37+
const proxyAddress = proxyServer.address() as import('net').AddressInfo;
38+
39+
await harness.writeFiles({
40+
'proxy.config.json': `{ "/api/*": { "target": "http://127.0.0.1:${proxyAddress.port}" } }`,
41+
});
42+
43+
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
44+
45+
expect(result?.success).toBeTrue();
46+
expect(await response?.text()).toContain('TEST_API_RETURN');
47+
} finally {
48+
await new Promise<void>((resolve) => proxyServer.close(() => resolve()));
49+
}
50+
});
51+
52+
it('proxies requests based on the JS proxy configuration file provided in the option', async () => {
53+
harness.useTarget('serve', {
54+
...BASE_OPTIONS,
55+
proxyConfig: 'proxy.config.js',
4256
});
4357

58+
const proxyServer = createProxyServer();
4459
try {
4560
await new Promise<void>((resolve) => proxyServer.listen(0, '127.0.0.1', resolve));
4661
const proxyAddress = proxyServer.address() as import('net').AddressInfo;
4762

4863
await harness.writeFiles({
49-
'proxy.config.json': `{ "/api/*": { "target": "http://127.0.0.1:${proxyAddress.port}" } }`,
64+
'proxy.config.js': `module.exports = { "/api/*": { "target": "http://127.0.0.1:${proxyAddress.port}" } }`,
65+
});
66+
67+
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
68+
69+
expect(result?.success).toBeTrue();
70+
expect(await response?.text()).toContain('TEST_API_RETURN');
71+
} finally {
72+
await new Promise<void>((resolve) => proxyServer.close(() => resolve()));
73+
}
74+
});
75+
76+
it('proxies requests based on the MJS proxy configuration file provided in the option', async () => {
77+
harness.useTarget('serve', {
78+
...BASE_OPTIONS,
79+
proxyConfig: 'proxy.config.mjs',
80+
});
81+
82+
const proxyServer = createProxyServer();
83+
try {
84+
await new Promise<void>((resolve) => proxyServer.listen(0, '127.0.0.1', resolve));
85+
const proxyAddress = proxyServer.address() as import('net').AddressInfo;
86+
87+
await harness.writeFiles({
88+
'proxy.config.mjs': `export default { "/api/*": { "target": "http://127.0.0.1:${proxyAddress.port}" } }`,
5089
});
5190

5291
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
@@ -75,3 +114,22 @@ describeBuilder(serveWebpackBrowser, DEV_SERVER_BUILDER_INFO, (harness) => {
75114
});
76115
});
77116
});
117+
118+
/**
119+
* Creates an HTTP Server used for proxy testing that provides a `/test` endpoint
120+
* that returns a 200 response with a body of `TEST_API_RETURN`. All other requests
121+
* will return a 404 response.
122+
*
123+
* @returns An HTTP Server instance.
124+
*/
125+
function createProxyServer() {
126+
return http.createServer((request, response) => {
127+
if (request.url?.endsWith('/test')) {
128+
response.writeHead(200);
129+
response.end('TEST_API_RETURN');
130+
} else {
131+
response.writeHead(404);
132+
response.end();
133+
}
134+
});
135+
}

packages/angular_devkit/build_angular/src/webpack/configs/dev-server.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import * as url from 'url';
1313
import * as webpack from 'webpack';
1414
import { Configuration } from 'webpack-dev-server';
1515
import { WebpackConfigOptions, WebpackDevServerOptions } from '../../utils/build-options';
16+
import { loadEsmModule } from '../../utils/load-esm';
1617
import { getIndexOutputFile } from '../../utils/webpack-browser-config';
1718
import { HmrLoader } from '../plugins/hmr/hmr-loader';
1819

19-
export function getDevServerConfig(
20+
export async function getDevServerConfig(
2021
wco: WebpackConfigOptions<WebpackDevServerOptions>,
21-
): webpack.Configuration {
22+
): Promise<webpack.Configuration> {
2223
const {
2324
buildOptions: { host, port, index, headers, watch, hmr, main, liveReload, proxyConfig },
2425
logger,
@@ -90,7 +91,7 @@ export function getDevServerConfig(
9091
},
9192
liveReload,
9293
hot: hmr && !liveReload ? 'only' : hmr,
93-
proxy: addProxyConfig(root, proxyConfig),
94+
proxy: await addProxyConfig(root, proxyConfig),
9495
client: {
9596
logging: 'info',
9697
webSocketURL: getPublicHostOptions(wco.buildOptions, webSocketPath),
@@ -154,14 +155,25 @@ function getSslConfig(root: string, options: WebpackDevServerOptions): Configura
154155
* Private method to enhance a webpack config with Proxy configuration.
155156
* @private
156157
*/
157-
function addProxyConfig(root: string, proxyConfig: string | undefined) {
158+
async function addProxyConfig(root: string, proxyConfig: string | undefined) {
158159
if (!proxyConfig) {
159160
return undefined;
160161
}
161162

162163
const proxyPath = resolve(root, proxyConfig);
163164
if (existsSync(proxyPath)) {
164-
return require(proxyPath);
165+
try {
166+
return require(proxyPath);
167+
} catch (e) {
168+
if (e.code === 'ERR_REQUIRE_ESM') {
169+
// Load the ESM configuration file using the TypeScript dynamic import workaround.
170+
// Once TypeScript provides support for keeping the dynamic import this workaround can be
171+
// changed to a direct dynamic import.
172+
return (await loadEsmModule<{ default: unknown }>(url.pathToFileURL(proxyPath))).default;
173+
}
174+
175+
throw e;
176+
}
165177
}
166178

167179
throw new Error('Proxy config file ' + proxyPath + ' does not exist.');

0 commit comments

Comments
 (0)