diff --git a/esbuild.config.js b/esbuild.config.js index 85e2402961f..c18f288a36c 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -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 = { @@ -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', diff --git a/packages/cli/src/patches/http-proxy-agent.ts b/packages/cli/src/patches/http-proxy-agent.ts index 1f13f9bb98e..c15049289c0 100644 --- a/packages/cli/src/patches/http-proxy-agent.ts +++ b/packages/cli/src/patches/http-proxy-agent.ts @@ -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); + }, +}); + +export { HttpProxyAgent }; +// eslint-disable-next-line import/no-default-export +export default HttpProxyAgent; diff --git a/packages/cli/src/patches/https-proxy-agent.ts b/packages/cli/src/patches/https-proxy-agent.ts index a7c16557979..77468729f7a 100644 --- a/packages/cli/src/patches/https-proxy-agent.ts +++ b/packages/cli/src/patches/https-proxy-agent.ts @@ -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); + }, +}); + +export { HttpsProxyAgent }; +// eslint-disable-next-line import/no-default-export +export default HttpsProxyAgent; diff --git a/scripts/tests/proxy-agent-bundle.test.ts b/scripts/tests/proxy-agent-bundle.test.ts index 605c45275b0..7ff8976f13f 100644 --- a/scripts/tests/proxy-agent-bundle.test.ts +++ b/scripts/tests/proxy-agent-bundle.test.ts @@ -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'; @@ -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, }; } `, @@ -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/'); }); });