Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions esbuild.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ const baseConfig = {

const commonAliases = {
punycode: 'punycode/',
'https-proxy-agent': path.resolve(
__dirname,
'packages/cli/src/patches/https-proxy-agent.ts',
),
'http-proxy-agent': path.resolve(
__dirname,
'packages/cli/src/patches/http-proxy-agent.ts',
),
};

const cliConfig = {
Expand All @@ -101,14 +109,6 @@ const cliConfig = {
plugins: createWasmPlugins(),
alias: {
'is-in-ci': path.resolve(__dirname, 'packages/cli/src/patches/is-in-ci.ts'),
'https-proxy-agent': path.resolve(
__dirname,
'packages/cli/src/patches/https-proxy-agent.ts',
),
'http-proxy-agent': path.resolve(
__dirname,
'packages/cli/src/patches/http-proxy-agent.ts',
),
'@google/gemini-cli-devtools': path.resolve(
__dirname,
'packages/devtools/src/index.ts',
Expand Down
53 changes: 51 additions & 2 deletions packages/cli/src/patches/http-proxy-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,54 @@
* SPDX-License-Identifier: Apache-2.0
*/

// eslint-disable-next-line import/no-relative-packages
export { HttpProxyAgent } from '../../../../node_modules/http-proxy-agent/dist/index.js';
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
/* eslint-disable import/no-relative-packages */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable no-restricted-syntax */

import * as rawProxyAgent from '../../../../node_modules/http-proxy-agent/dist/index.js';

type HttpProxyAgentCtor = typeof rawProxyAgent.HttpProxyAgent;

interface InteropShape {
HttpProxyAgent?: HttpProxyAgentCtor;
default?: HttpProxyAgentCtor | InteropShape;
}

const mod = rawProxyAgent as unknown as InteropShape;
const defaultMod = mod.default as InteropShape | undefined;
const defaultNamedCtor = defaultMod?.HttpProxyAgent;

let resolvedCtor: HttpProxyAgentCtor | undefined;
if (typeof mod.HttpProxyAgent === 'function') {
resolvedCtor = mod.HttpProxyAgent;
} else if (typeof mod.default === 'function') {
resolvedCtor = mod.default;
} else if (typeof defaultNamedCtor === 'function') {
resolvedCtor = defaultNamedCtor;
} else if (typeof rawProxyAgent === 'function') {
resolvedCtor = rawProxyAgent as unknown as HttpProxyAgentCtor;
}

const baseCtor =
resolvedCtor ??
(class {
constructor() {
throw new Error(
'HttpProxyAgent constructor could not be resolved from http-proxy-agent',
);
}
} as unknown as HttpProxyAgentCtor);

const HttpProxyAgent = new Proxy(baseCtor, {
get(target, prop, receiver) {
if (prop === 'HttpProxyAgent' || prop === 'default') {
return receiver;
}
return Reflect.get(target, prop, receiver);
},
});
Comment thread
diegogodinezr marked this conversation as resolved.
Comment thread
diegogodinezr marked this conversation as resolved.
Comment thread
diegogodinezr marked this conversation as resolved.

export { HttpProxyAgent };
// eslint-disable-next-line import/no-default-export
export default HttpProxyAgent;
53 changes: 51 additions & 2 deletions packages/cli/src/patches/https-proxy-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,54 @@
* SPDX-License-Identifier: Apache-2.0
*/

// eslint-disable-next-line import/no-relative-packages
export { HttpsProxyAgent } from '../../../../node_modules/https-proxy-agent/dist/index.js';
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
/* eslint-disable import/no-relative-packages */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable no-restricted-syntax */

import * as rawProxyAgent from '../../../../node_modules/https-proxy-agent/dist/index.js';

type HttpsProxyAgentCtor = typeof rawProxyAgent.HttpsProxyAgent;

interface InteropShape {
HttpsProxyAgent?: HttpsProxyAgentCtor;
default?: HttpsProxyAgentCtor | InteropShape;
}

const mod = rawProxyAgent as unknown as InteropShape;
const defaultMod = mod.default as InteropShape | undefined;
const defaultNamedCtor = defaultMod?.HttpsProxyAgent;

let resolvedCtor: HttpsProxyAgentCtor | undefined;
if (typeof mod.HttpsProxyAgent === 'function') {
resolvedCtor = mod.HttpsProxyAgent;
} else if (typeof mod.default === 'function') {
resolvedCtor = mod.default;
} else if (typeof defaultNamedCtor === 'function') {
resolvedCtor = defaultNamedCtor;
} else if (typeof rawProxyAgent === 'function') {
resolvedCtor = rawProxyAgent as unknown as HttpsProxyAgentCtor;
}

const baseCtor =
resolvedCtor ??
(class {
constructor() {
throw new Error(
'HttpsProxyAgent constructor could not be resolved from https-proxy-agent',
);
}
} as unknown as HttpsProxyAgentCtor);

const HttpsProxyAgent = new Proxy(baseCtor, {
get(target, prop, receiver) {
if (prop === 'HttpsProxyAgent' || prop === 'default') {
return receiver;
}
return Reflect.get(target, prop, receiver);
},
});
Comment thread
diegogodinezr marked this conversation as resolved.
Comment thread
diegogodinezr marked this conversation as resolved.
Comment thread
diegogodinezr marked this conversation as resolved.

export { HttpsProxyAgent };
// eslint-disable-next-line import/no-default-export
export default HttpsProxyAgent;
134 changes: 122 additions & 12 deletions scripts/tests/proxy-agent-bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.unmock('fs');
vi.unmock('node:fs');
import * as esbuild from 'esbuild';
Expand All @@ -17,21 +17,42 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '../../');

describe('proxy-agent bundle shape', () => {
it('preserves named constructors after ESM splitting', async () => {
const tmpDir = mkdtempSync(path.join(tmpdir(), 'gemini-proxy-test-'));
describe('proxy-agent bundle shape and interop', () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = mkdtempSync(path.join(tmpdir(), 'gemini-proxy-test-'));
});

afterEach(() => {
vi.unstubAllEnvs();
if (tmpDir) {
rmSync(tmpDir, { recursive: true, force: true });
}
});

it('preserves named and default constructors after ESM splitting', async () => {
const entryFile = path.join(tmpDir, 'entry.ts');

// Create a minimal entry file that dynamically imports the proxy agents
// Create an entry file that tests both static and dynamic imports
writeFileSync(
entryFile,
`
import StaticHttpsDefault from 'https-proxy-agent';
import { HttpsProxyAgent as StaticHttpsNamed } from 'https-proxy-agent';
import StaticHttpDefault from 'http-proxy-agent';
import { HttpProxyAgent as StaticHttpNamed } from 'http-proxy-agent';

export async function getAgents() {
const httpsMod = await import('https-proxy-agent');
const httpMod = await import('http-proxy-agent');
return {
https: httpsMod,
http: httpMod,
staticHttpsDefault: StaticHttpsDefault,
staticHttpsNamed: StaticHttpsNamed,
staticHttpDefault: StaticHttpDefault,
staticHttpNamed: StaticHttpNamed,
};
}
`,
Expand Down Expand Up @@ -64,17 +85,106 @@ describe('proxy-agent bundle shape', () => {
).href;
const { getAgents } = await import(bundledEntryUrl);

const { https, http } = await getAgents();
const {
https,
http,
staticHttpsDefault,
staticHttpsNamed,
staticHttpDefault,
staticHttpNamed,
} = await getAgents();

// Verify named exports exist
// Verify named exports exist and are functions
expect(typeof https.HttpsProxyAgent).toBe('function');
expect(typeof http.HttpProxyAgent).toBe('function');

// Verify they are constructable
expect(() => new https.HttpsProxyAgent('http://127.0.0.1:9')).not.toThrow();
expect(() => new http.HttpProxyAgent('http://127.0.0.1:9')).not.toThrow();
// Verify default exports exist and are functions
expect(typeof https.default).toBe('function');
expect(typeof http.default).toBe('function');

// Verify static imports work
expect(typeof staticHttpsDefault).toBe('function');
expect(typeof staticHttpsNamed).toBe('function');
expect(typeof staticHttpDefault).toBe('function');
expect(typeof staticHttpNamed).toBe('function');

// Verify self-referential properties for CJS/ESM interop fallback
expect(https.HttpsProxyAgent.HttpsProxyAgent).toBe(https.HttpsProxyAgent);
expect(https.HttpsProxyAgent.default).toBe(https.HttpsProxyAgent);
expect(http.HttpProxyAgent.HttpProxyAgent).toBe(http.HttpProxyAgent);
expect(http.HttpProxyAgent.default).toBe(http.HttpProxyAgent);

// Verify they are constructable with proxy URLs
expect(
() => new https.HttpsProxyAgent('http://127.0.0.1:7897'),
).not.toThrow();
expect(() => new https.default('http://127.0.0.1:7897')).not.toThrow();
expect(() => new staticHttpsDefault('http://127.0.0.1:7897')).not.toThrow();
expect(() => new staticHttpsNamed('http://127.0.0.1:7897')).not.toThrow();

expect(
() => new http.HttpProxyAgent('http://127.0.0.1:7897'),
).not.toThrow();
expect(() => new http.default('http://127.0.0.1:7897')).not.toThrow();
expect(() => new staticHttpDefault('http://127.0.0.1:7897')).not.toThrow();
expect(() => new staticHttpNamed('http://127.0.0.1:7897')).not.toThrow();
});

it('instantiates proxy agents when HTTP_PROXY and HTTPS_PROXY are set', async () => {
vi.stubEnv('HTTP_PROXY', 'http://127.0.0.1:7897');
vi.stubEnv('HTTPS_PROXY', 'http://127.0.0.1:7897');

const entryFile = path.join(tmpDir, 'proxy-env-entry.ts');
writeFileSync(
entryFile,
`
export async function testEnvProxy() {
const httpsMod = await import('https-proxy-agent');
const httpMod = await import('http-proxy-agent');

const httpsProxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy;
const httpProxyUrl = process.env.HTTP_PROXY || process.env.http_proxy;

const httpsConstructor = httpsMod.HttpsProxyAgent || httpsMod.default || httpsMod;
const httpConstructor = httpMod.HttpProxyAgent || httpMod.default || httpMod;

const httpsAgent = new httpsConstructor(httpsProxyUrl);
const httpAgent = new httpConstructor(httpProxyUrl);

return { httpsAgent, httpAgent };
}
`,
);

await esbuild.build({
entryPoints: { gemini: entryFile },
outdir: path.join(tmpDir, 'bundle'),
bundle: true,
splitting: true,
format: 'esm',
platform: 'node',
outExtension: { '.js': '.mjs' },
alias: {
'https-proxy-agent': path.resolve(
projectRoot,
'packages/cli/src/patches/https-proxy-agent.ts',
),
'http-proxy-agent': path.resolve(
projectRoot,
'packages/cli/src/patches/http-proxy-agent.ts',
),
},
});

const bundledEntryUrl = pathToFileURL(
path.join(tmpDir, 'bundle/gemini.mjs'),
).href;
const { testEnvProxy } = await import(bundledEntryUrl);

// Cleanup
rmSync(tmpDir, { recursive: true, force: true });
const { httpsAgent, httpAgent } = await testEnvProxy();
expect(httpsAgent).toBeDefined();
expect(httpAgent).toBeDefined();
expect(httpsAgent.proxy?.href).toBe('http://127.0.0.1:7897/');
expect(httpAgent.proxy?.href).toBe('http://127.0.0.1:7897/');
});
});
Loading