From 8424ab0434dcc26fc55b93032c687a145b33284d Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 8 Jun 2023 14:04:25 -0400 Subject: [PATCH] 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 779c969f31cfd9ed6356b91986999ce00f4cb3e9) --- .../builders/dev-server/load-proxy-config.ts | 74 ++++++++++++++++--- .../tests/options/proxy-config_spec.ts | 24 ++++++ .../src/builders/dev-server/vite-server.ts | 6 +- 3 files changed, 89 insertions(+), 15 deletions(-) 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,