Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): support proxy configuration array…
Browse files Browse the repository at this point in the history
…-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)
  • Loading branch information
clydin authored and alan-agius4 committed Jun 9, 2023
1 parent abe3d73 commit 081b625
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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:`;
Expand All @@ -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<string, unknown>) {
function normalizeProxyConfiguration(
proxy: Record<string, unknown> | object[],
): Record<string, unknown> {
let normalizedProxy: Record<string, unknown> | 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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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<void>((resolve) => proxyServer.close(() => resolve()));
}
});

it('throws an error when proxy configuration file cannot be found', async () => {
harness.useTarget('serve', {
...BASE_OPTIONS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 081b625

Please sign in to comment.