Skip to content

Commit 081b625

Browse files
clydinalan-agius4
authored andcommitted
fix(@angular-devkit/build-angular): support proxy configuration array-form in esbuild builder
When using the Webpack-based browser application builder with the development server, the proxy configuration can be in an array form when using the `proxyConfig` option. This is unfortunately not natively supported by the Vite development server used when building with the esbuild-based browser application builder. However, the array form can be transformed into the object form. This transformation allows for the array form of the proxy configuration to be used by both development server implementations. (cherry picked from commit 779c969)
1 parent abe3d73 commit 081b625

File tree

3 files changed

+89
-15
lines changed

3 files changed

+89
-15
lines changed

packages/angular_devkit/build_angular/src/builders/dev-server/load-proxy-config.ts

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import { parse as parseGlob } from 'picomatch';
1515
import { assertIsError } from '../../utils/error';
1616
import { loadEsmModule } from '../../utils/load-esm';
1717

18-
export async function loadProxyConfiguration(root: string, proxyConfig: string | undefined) {
18+
export async function loadProxyConfiguration(
19+
root: string,
20+
proxyConfig: string | undefined,
21+
normalize = false,
22+
) {
1923
if (!proxyConfig) {
2024
return undefined;
2125
}
@@ -26,13 +30,14 @@ export async function loadProxyConfiguration(root: string, proxyConfig: string |
2630
throw new Error(`Proxy configuration file ${proxyPath} does not exist.`);
2731
}
2832

33+
let proxyConfiguration;
2934
switch (extname(proxyPath)) {
3035
case '.json': {
3136
const content = await readFile(proxyPath, 'utf-8');
3237

3338
const { parse, printParseErrorCode } = await import('jsonc-parser');
3439
const parseErrors: import('jsonc-parser').ParseError[] = [];
35-
const proxyConfiguration = parse(content, parseErrors, { allowTrailingComma: true });
40+
proxyConfiguration = parse(content, parseErrors, { allowTrailingComma: true });
3641

3742
if (parseErrors.length > 0) {
3843
let errorMessage = `Proxy configuration file ${proxyPath} contains parse errors:`;
@@ -43,47 +48,94 @@ export async function loadProxyConfiguration(root: string, proxyConfig: string |
4348
throw new Error(errorMessage);
4449
}
4550

46-
return proxyConfiguration;
51+
break;
4752
}
4853
case '.mjs':
4954
// Load the ESM configuration file using the TypeScript dynamic import workaround.
5055
// Once TypeScript provides support for keeping the dynamic import this workaround can be
5156
// changed to a direct dynamic import.
52-
return (await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath))).default;
57+
proxyConfiguration = (await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath)))
58+
.default;
59+
break;
5360
case '.cjs':
54-
return require(proxyPath);
61+
proxyConfiguration = require(proxyPath);
62+
break;
5563
default:
5664
// The file could be either CommonJS or ESM.
5765
// CommonJS is tried first then ESM if loading fails.
5866
try {
59-
return require(proxyPath);
67+
proxyConfiguration = require(proxyPath);
68+
break;
6069
} catch (e) {
6170
assertIsError(e);
6271
if (e.code === 'ERR_REQUIRE_ESM') {
6372
// Load the ESM configuration file using the TypeScript dynamic import workaround.
6473
// Once TypeScript provides support for keeping the dynamic import this workaround can be
6574
// changed to a direct dynamic import.
66-
return (await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath))).default;
75+
proxyConfiguration = (await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath)))
76+
.default;
77+
break;
6778
}
6879

6980
throw e;
7081
}
7182
}
83+
84+
if (normalize) {
85+
proxyConfiguration = normalizeProxyConfiguration(proxyConfiguration);
86+
}
87+
88+
return proxyConfiguration;
7289
}
7390

7491
/**
7592
* Converts glob patterns to regular expressions to support Vite's proxy option.
93+
* Also converts the Webpack supported array form to an object form supported by both.
94+
*
7695
* @param proxy A proxy configuration object.
7796
*/
78-
export function normalizeProxyConfiguration(proxy: Record<string, unknown>) {
97+
function normalizeProxyConfiguration(
98+
proxy: Record<string, unknown> | object[],
99+
): Record<string, unknown> {
100+
let normalizedProxy: Record<string, unknown> | undefined;
101+
102+
if (Array.isArray(proxy)) {
103+
// Construct an object-form proxy configuration from the array
104+
normalizedProxy = {};
105+
for (const proxyEntry of proxy) {
106+
if (!('context' in proxyEntry)) {
107+
continue;
108+
}
109+
if (!Array.isArray(proxyEntry.context)) {
110+
continue;
111+
}
112+
113+
// Array-form entries contain a context string array with the path(s)
114+
// to use for the configuration entry.
115+
const context = proxyEntry.context;
116+
delete proxyEntry.context;
117+
for (const contextEntry of context) {
118+
if (typeof contextEntry !== 'string') {
119+
continue;
120+
}
121+
122+
normalizedProxy[contextEntry] = proxyEntry;
123+
}
124+
}
125+
} else {
126+
normalizedProxy = proxy;
127+
}
128+
79129
// TODO: Consider upstreaming glob support
80-
for (const key of Object.keys(proxy)) {
130+
for (const key of Object.keys(normalizedProxy)) {
81131
if (isDynamicPattern(key)) {
82132
const { output } = parseGlob(key);
83-
proxy[`^${output}$`] = proxy[key];
84-
delete proxy[key];
133+
normalizedProxy[`^${output}$`] = normalizedProxy[key];
134+
delete normalizedProxy[key];
85135
}
86136
}
137+
138+
return normalizedProxy;
87139
}
88140

89141
/**

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,30 @@ describeBuilder(serveWebpackBrowser, DEV_SERVER_BUILDER_INFO, (harness) => {
173173
}
174174
});
175175

176+
it('supports the Webpack array form of the configuration file', async () => {
177+
harness.useTarget('serve', {
178+
...BASE_OPTIONS,
179+
proxyConfig: 'proxy.config.json',
180+
});
181+
182+
const proxyServer = createProxyServer();
183+
try {
184+
await new Promise<void>((resolve) => proxyServer.listen(0, '127.0.0.1', resolve));
185+
const proxyAddress = proxyServer.address() as import('net').AddressInfo;
186+
187+
await harness.writeFiles({
188+
'proxy.config.json': `[ { "context": ["/api", "/abc"], "target": "http://127.0.0.1:${proxyAddress.port}" } ]`,
189+
});
190+
191+
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
192+
193+
expect(result?.success).toBeTrue();
194+
expect(await response?.text()).toContain('TEST_API_RETURN');
195+
} finally {
196+
await new Promise<void>((resolve) => proxyServer.close(() => resolve()));
197+
}
198+
});
199+
176200
it('throws an error when proxy configuration file cannot be found', async () => {
177201
harness.useTarget('serve', {
178202
...BASE_OPTIONS,

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import path from 'node:path';
1818
import { InlineConfig, ViteDevServer, createServer, normalizePath } from 'vite';
1919
import { buildEsbuildBrowser } from '../browser-esbuild';
2020
import type { Schema as BrowserBuilderOptions } from '../browser-esbuild/schema';
21-
import { loadProxyConfiguration, normalizeProxyConfiguration } from './load-proxy-config';
21+
import { loadProxyConfiguration } from './load-proxy-config';
2222
import type { NormalizedDevServerOptions } from './options';
2323
import type { DevServerBuilderOutput } from './webpack-server';
2424

@@ -181,10 +181,8 @@ export async function setupServer(
181181
const proxy = await loadProxyConfiguration(
182182
serverOptions.workspaceRoot,
183183
serverOptions.proxyConfig,
184+
true,
184185
);
185-
if (proxy) {
186-
normalizeProxyConfiguration(proxy);
187-
}
188186

189187
const configuration: InlineConfig = {
190188
configFile: false,

0 commit comments

Comments
 (0)