From 081b62539b2562bff130343558bf4baafed7c36d 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 f27f388df570..26f3c26c7ebe 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 412d692ce82f..9363c6391cd0 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 @@ -18,7 +18,7 @@ import path from 'node:path'; import { InlineConfig, ViteDevServer, createServer, normalizePath } from 'vite'; import { buildEsbuildBrowser } from '../browser-esbuild'; 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'; @@ -181,10 +181,8 @@ export async function setupServer( const proxy = await loadProxyConfiguration( serverOptions.workspaceRoot, serverOptions.proxyConfig, + true, ); - if (proxy) { - normalizeProxyConfiguration(proxy); - } const configuration: InlineConfig = { configFile: false,