From 7e962debbe01f2147c62eae6005298782c01b2e2 Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Wed, 29 Jul 2026 15:03:40 -0700 Subject: [PATCH 1/2] fix(dev): only pass esbuild "external" when bundling is enabled esbuild rejects the `external` option unless `bundle` is enabled, so `AgentFile.load()` hard-failed with `Cannot use "external" without "bundle"` for every non-truthy `bundle` value -- breaking `adk web|run| api_server|deploy --bundle false` and `new AgentFile(p, {compile: true})`. Hoist the allowlist to a module-scope `EXTERNAL_PACKAGES` constant (contents unchanged) and spread it into the build options only when bundling, so the key is genuinely absent otherwise. `bundle: true`, the default on every code path, produces an identical esbuild invocation. --- dev/src/utils/agent_loader.ts | 48 ++++++++++++--------- dev/test/utils/agent_loader_test.ts | 67 +++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 20 deletions(-) diff --git a/dev/src/utils/agent_loader.ts b/dev/src/utils/agent_loader.ts index d5097c482..15775c498 100644 --- a/dev/src/utils/agent_loader.ts +++ b/dev/src/utils/agent_loader.ts @@ -49,6 +49,31 @@ const FILE_MODULE_TYPE_EXTENSION_MAP = { [FileModuleType.ESM]: '.mjs', }; +/** + * Packages that must never be inlined into the bundled agent file. + * + * See http://mikro-orm.io/docs/deployment#deploy-a-bundle-of-entities-and-dependencies-with-esbuild for more details + */ +const EXTERNAL_PACKAGES = [ + 'sqlite3', + 'better-sqlite3', + 'mysql', + 'mysql2', + // Native addons must remain external so Node can resolve their + // platform-specific assets at runtime. + 'onnxruntime-node', + 'oracledb', + 'pg-native', + 'pg-query-stream', + 'tedious', + 'libsql', + // Optional peer dependencies of vite and eslint that are not + // installed and MUST NOT be bundled. + 'lightningcss', + 'jiti', + 'jiti/package.json', +]; + /** * Metadata for a file. */ @@ -187,26 +212,9 @@ export class AgentFile { bundle: this.options.bundle, minify: this.options.bundle, plugins: [replaceDirnamePlugin(filePath, originalDir), shimPlugin()], - // See http://mikro-orm.io/docs/deployment#deploy-a-bundle-of-entities-and-dependencies-with-esbuild for more details - external: [ - 'sqlite3', - 'better-sqlite3', - 'mysql', - 'mysql2', - // Native addons must remain external so Node can resolve their - // platform-specific assets at runtime. - 'onnxruntime-node', - 'oracledb', - 'pg-native', - 'pg-query-stream', - 'tedious', - 'libsql', - // Optional peer dependencies of vite and eslint that are not - // installed and MUST NOT be bundled. - 'lightningcss', - 'jiti', - 'jiti/package.json', - ], + // esbuild rejects `external` unless `bundle` is enabled, so the + // allowlist is only passed when the agent file is actually bundled. + ...(this.options.bundle ? {external: EXTERNAL_PACKAGES} : {}), }); this.cleanupDirPath = outputDir; diff --git a/dev/test/utils/agent_loader_test.ts b/dev/test/utils/agent_loader_test.ts index 2285df895..4ff7014ca 100644 --- a/dev/test/utils/agent_loader_test.ts +++ b/dev/test/utils/agent_loader_test.ts @@ -286,6 +286,9 @@ describe('AgentLoader', () => { minify: true, external: expect.arrayContaining(['onnxruntime-node']), }); + expect((esbuild.build as Mock).mock.calls[0][0]).toHaveProperty( + 'external', + ); await agentFile.dispose(); await expect(fs.access(compiledAgentPath)).rejects.toThrow(); @@ -500,6 +503,70 @@ describe('AgentLoader', () => { await agentFile.dispose(); }); + it('omits esbuild "external" when bundling is disabled', async () => { + const agentPath = path.join(tempAgentsDir, 'agent2.ts'); + await fs.writeFile(agentPath, agent2TsContent); + + (esbuild.build as Mock).mockImplementation(async () => { + await fs.writeFile(compiledPath('agent2.cjs'), agent2CjsContentMocked); + return Promise.resolve(); + }); + + const agentFile = new AgentFile(agentPath, { + compile: true, + bundle: false, + }); + const agent = await agentFile.load(); + + expect(agent.name).toEqual('agent2'); + const buildOptions = (esbuild.build as Mock).mock.calls[0][0]; + expect(buildOptions).not.toHaveProperty('external'); + expect(buildOptions).toMatchObject({bundle: false, minify: false}); + + await agentFile.dispose(); + }); + + it('omits esbuild "external" when bundle option is not provided', async () => { + const agentPath = path.join(tempAgentsDir, 'agent2.ts'); + await fs.writeFile(agentPath, agent2TsContent); + + (esbuild.build as Mock).mockImplementation(async () => { + await fs.writeFile(compiledPath('agent2.cjs'), agent2CjsContentMocked); + return Promise.resolve(); + }); + + const agentFile = new AgentFile(agentPath, {compile: true}); + const agent = await agentFile.load(); + + expect(agent.name).toEqual('agent2'); + expect((esbuild.build as Mock).mock.calls[0][0]).not.toHaveProperty( + 'external', + ); + + await agentFile.dispose(); + }); + + it('compiles and loads a .ts agent with real esbuild when bundle is disabled', async () => { + const {build: realEsbuildBuild} = + await vi.importActual('esbuild'); + (esbuild.build as Mock).mockImplementation(realEsbuildBuild); + + const agentPath = path.join(tempAgentsDir, 'agent2.ts'); + await fs.writeFile(agentPath, agent2TsContent); + + const agentFile = new AgentFile(agentPath, { + compile: true, + bundle: false, + }); + const agent = await agentFile.load(); + + expect(agent.name).toEqual('agent2'); + const compiled = await fs.readFile(compiledPath('agent2.cjs'), 'utf8'); + expect(compiled).toContain('require("@google/adk")'); + + await agentFile.dispose(); + }); + it('throws specific error if file does not exist', async () => { const agentPath = path.join(tempAgentsDir, 'non_existent.js'); const agentFile = new AgentFile(agentPath); From 155ee790b0b0db090bb6924624b8feb048c94122 Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Wed, 29 Jul 2026 15:22:36 -0700 Subject: [PATCH 2/2] test(dev): tighten the new agent-loader bundle tests Drop a tautological toHaveProperty('external') assertion -- the toMatchObject two lines above already asserts external: expect.arrayContaining(['onnxruntime-node']), which fails when the key is absent. Scope the real-esbuild implementation to the single build the test triggers so it cannot leak into later tests (vi.clearAllMocks clears calls but keeps implementations). --- dev/test/utils/agent_loader_test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dev/test/utils/agent_loader_test.ts b/dev/test/utils/agent_loader_test.ts index 4ff7014ca..fef6f1cc7 100644 --- a/dev/test/utils/agent_loader_test.ts +++ b/dev/test/utils/agent_loader_test.ts @@ -286,9 +286,6 @@ describe('AgentLoader', () => { minify: true, external: expect.arrayContaining(['onnxruntime-node']), }); - expect((esbuild.build as Mock).mock.calls[0][0]).toHaveProperty( - 'external', - ); await agentFile.dispose(); await expect(fs.access(compiledAgentPath)).rejects.toThrow(); @@ -549,7 +546,7 @@ describe('AgentLoader', () => { it('compiles and loads a .ts agent with real esbuild when bundle is disabled', async () => { const {build: realEsbuildBuild} = await vi.importActual('esbuild'); - (esbuild.build as Mock).mockImplementation(realEsbuildBuild); + (esbuild.build as Mock).mockImplementationOnce(realEsbuildBuild); const agentPath = path.join(tempAgentsDir, 'agent2.ts'); await fs.writeFile(agentPath, agent2TsContent);