diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/load-proxy-config.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/load-proxy-config.ts index ec84679fd9fe..14f7c7140f4b 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/load-proxy-config.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/load-proxy-config.ts @@ -15,7 +15,11 @@ import { parse as parseGlob } from 'picomatch'; import { assertIsError } from '../../utils/error'; import { loadEsmModule } from '../../utils/load-esm'; -export async function loadProxyConfiguration(root: string, proxyConfig: string | undefined) { +export async function loadProxyConfiguration( + root: string, + proxyConfig: string | undefined, + normalize = false, +) { if (!proxyConfig) { return undefined; } @@ -26,13 +30,14 @@ export async function loadProxyConfiguration(root: string, proxyConfig: string | throw new Error(`Proxy configuration file ${proxyPath} does not exist.`); } + let proxyConfiguration; switch (extname(proxyPath)) { case '.json': { const content = await readFile(proxyPath, 'utf-8'); const { parse, printParseErrorCode } = await import('jsonc-parser'); const parseErrors: import('jsonc-parser').ParseError[] = []; - const proxyConfiguration = parse(content, parseErrors, { allowTrailingComma: true }); + proxyConfiguration = parse(content, parseErrors, { allowTrailingComma: true }); if (parseErrors.length > 0) { let errorMessage = `Proxy configuration file ${proxyPath} contains parse errors:`; @@ -43,47 +48,94 @@ export async function loadProxyConfiguration(root: string, proxyConfig: string | throw new Error(errorMessage); } - return proxyConfiguration; + break; } case '.mjs': // Load the ESM configuration file using the TypeScript dynamic import workaround. // Once TypeScript provides support for keeping the dynamic import this workaround can be // changed to a direct dynamic import. - return (await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath))).default; + proxyConfiguration = (await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath))) + .default; + break; case '.cjs': - return require(proxyPath); + proxyConfiguration = require(proxyPath); + break; default: // The file could be either CommonJS or ESM. // CommonJS is tried first then ESM if loading fails. try { - return require(proxyPath); + proxyConfiguration = require(proxyPath); + break; } catch (e) { assertIsError(e); if (e.code === 'ERR_REQUIRE_ESM') { // Load the ESM configuration file using the TypeScript dynamic import workaround. // Once TypeScript provides support for keeping the dynamic import this workaround can be // changed to a direct dynamic import. - return (await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath))).default; + proxyConfiguration = (await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath))) + .default; + break; } throw e; } } + + if (normalize) { + proxyConfiguration = normalizeProxyConfiguration(proxyConfiguration); + } + + return proxyConfiguration; } /** * Converts glob patterns to regular expressions to support Vite's proxy option. + * Also converts the Webpack supported array form to an object form supported by both. + * * @param proxy A proxy configuration object. */ -export function normalizeProxyConfiguration(proxy: Record) { +function normalizeProxyConfiguration( + proxy: Record | object[], +): Record { + let normalizedProxy: Record | undefined; + + if (Array.isArray(proxy)) { + // Construct an object-form proxy configuration from the array + normalizedProxy = {}; + for (const proxyEntry of proxy) { + if (!('context' in proxyEntry)) { + continue; + } + if (!Array.isArray(proxyEntry.context)) { + continue; + } + + // Array-form entries contain a context string array with the path(s) + // to use for the configuration entry. + const context = proxyEntry.context; + delete proxyEntry.context; + for (const contextEntry of context) { + if (typeof contextEntry !== 'string') { + continue; + } + + normalizedProxy[contextEntry] = proxyEntry; + } + } + } else { + normalizedProxy = proxy; + } + // TODO: Consider upstreaming glob support - for (const key of Object.keys(proxy)) { + for (const key of Object.keys(normalizedProxy)) { if (isDynamicPattern(key)) { const { output } = parseGlob(key); - proxy[`^${output}$`] = proxy[key]; - delete proxy[key]; + normalizedProxy[`^${output}$`] = normalizedProxy[key]; + delete normalizedProxy[key]; } } + + return normalizedProxy; } /** diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/proxy-config_spec.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/proxy-config_spec.ts index 549b79f24a38..ed4152430a51 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/proxy-config_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/proxy-config_spec.ts @@ -173,6 +173,30 @@ describeBuilder(serveWebpackBrowser, DEV_SERVER_BUILDER_INFO, (harness) => { } }); + it('supports the Webpack array form of the configuration file', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + proxyConfig: 'proxy.config.json', + }); + + const proxyServer = createProxyServer(); + try { + await new Promise((resolve) => proxyServer.listen(0, '127.0.0.1', resolve)); + const proxyAddress = proxyServer.address() as import('net').AddressInfo; + + await harness.writeFiles({ + 'proxy.config.json': `[ { "context": ["/api", "/abc"], "target": "http://127.0.0.1:${proxyAddress.port}" } ]`, + }); + + const { result, response } = await executeOnceAndFetch(harness, '/api/test'); + + expect(result?.success).toBeTrue(); + expect(await response?.text()).toContain('TEST_API_RETURN'); + } finally { + await new Promise((resolve) => proxyServer.close(() => resolve())); + } + }); + it('throws an error when proxy configuration file cannot be found', async () => { harness.useTarget('serve', { ...BASE_OPTIONS, diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts index 4a5a7c320c6c..4de5f3bc1069 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts @@ -20,7 +20,7 @@ import { buildEsbuildBrowserInternal } from '../browser-esbuild'; import { JavaScriptTransformer } from '../browser-esbuild/javascript-transformer'; import { BrowserEsbuildOptions } from '../browser-esbuild/options'; import type { Schema as BrowserBuilderOptions } from '../browser-esbuild/schema'; -import { loadProxyConfiguration, normalizeProxyConfiguration } from './load-proxy-config'; +import { loadProxyConfiguration } from './load-proxy-config'; import type { NormalizedDevServerOptions } from './options'; import type { DevServerBuilderOutput } from './webpack-server'; @@ -196,10 +196,8 @@ export async function setupServer( const proxy = await loadProxyConfiguration( serverOptions.workspaceRoot, serverOptions.proxyConfig, + true, ); - if (proxy) { - normalizeProxyConfiguration(proxy); - } const configuration: InlineConfig = { configFile: false,