From de8fde93de88d7a5d4d1622ebb966b55279ece8e Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Fri, 7 Aug 2026 10:45:38 -0700 Subject: [PATCH 1/3] Fix: derive the bundled outfile from the entry in integrations/build.js The bundled branch hardcoded the output filename to index.js, so the web target compiled src/index_web.ts into dist/web/index.js. The path integrations/package.json advertises as "browser", dist/web/index_web.js, was never emitted by npm run build:bundle. Derive the filename from the entry instead. The esm and cjs targets are unchanged because their entry is already index.ts. --- integrations/build.js | 4 +- .../build_setup/integrations_build_test.ts | 86 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/integration/build_setup/integrations_build_test.ts diff --git a/integrations/build.js b/integrations/build.js index d07ba5194..aa21e488d 100644 --- a/integrations/build.js +++ b/integrations/build.js @@ -57,7 +57,9 @@ function build({ if (bundle) { buildOptions.entryPoints = [`./src/${entry}`]; - buildOptions.outfile = `./dist/${targetDir}/index.js`; + // Keep the emitted filename aligned with the entry so package.json's + // "browser" field keeps resolving to dist/web/index_web.js. + buildOptions.outfile = `./dist/${targetDir}/${entry.replace(/\.ts$/, '.js')}`; } else { buildOptions.entryPoints = ['./src/**/*.ts']; buildOptions.outdir = `./dist/${targetDir}`; diff --git a/tests/integration/build_setup/integrations_build_test.ts b/tests/integration/build_setup/integrations_build_test.ts new file mode 100644 index 000000000..bd4cfd518 --- /dev/null +++ b/tests/integration/build_setup/integrations_build_test.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import {exec} from 'node:child_process'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import {promisify} from 'node:util'; +import {describe, expect, it} from 'vitest'; + +const execAsync = promisify(exec); + +const repoRoot = process.cwd(); +const packageDir = path.join(repoRoot, 'integrations'); +const distDir = path.join(packageDir, 'dist'); + +/** + * Budget (ms) for one `rm -rf dist` plus one full workspace build. A build is + * `tsc --emitDeclarationOnly` followed by three esbuild passes and takes ~2s on + * a warm checkout; 60s absorbs a cold, loaded CI runner. + */ +const BUILD_TIMEOUT_MS = 60000; + +interface PackageManifest { + browser: string; + main: string; + module: string; +} + +async function readManifest(): Promise { + const raw = await fs.readFile(path.join(packageDir, 'package.json'), 'utf8'); + return JSON.parse(raw) as PackageManifest; +} + +/** + * Removes `integrations/dist` and reruns `script`, so that what the assertions + * see was emitted by this build and not left behind by an earlier one. + */ +async function buildFromClean(script: string): Promise { + await fs.rm(distDir, {recursive: true, force: true}); + await execAsync(`npm run ${script} --workspace integrations`, { + cwd: repoRoot, + }); +} + +async function expectEmitted(entryPointField: string, relativePath: string) { + const absolute = path.join(packageDir, relativePath); + await expect( + fs.access(absolute), + `"${entryPointField}": "${relativePath}" was not emitted`, + ).resolves.toBeUndefined(); + const {size} = await fs.stat(absolute); + expect(size, `${relativePath} is empty`).toBeGreaterThan(0); +} + +// Both cases rebuild the real `integrations/dist`. The bundled build runs +// first so the tree is left holding the plain build's output, which is what +// the rest of the repo expects. Nothing else reads `integrations/dist`. +describe('integrations build output', () => { + it( + 'emits every entry point the manifest names, bundled', + async () => { + await buildFromClean('build:bundle'); + + const manifest = await readManifest(); + await expectEmitted('browser', manifest.browser); + await expectEmitted('main', manifest.main); + await expectEmitted('module', manifest.module); + }, + BUILD_TIMEOUT_MS, + ); + + it( + 'emits every entry point the manifest names, unbundled', + async () => { + await buildFromClean('build'); + + const manifest = await readManifest(); + await expectEmitted('browser', manifest.browser); + await expectEmitted('main', manifest.main); + await expectEmitted('module', manifest.module); + }, + BUILD_TIMEOUT_MS, + ); +}); From 0583dd122669c7883b83da42cdacb141dd8348d0 Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Fri, 7 Aug 2026 11:20:24 -0700 Subject: [PATCH 2/3] Fix: share one outdir between the bundled and unbundled builds esbuild derives each output filename from its entry when outdir is set, so the bundled branch does not need to name the file itself. Giving both modes the same outdir collapses the branch to two lines and removes the hand-rolled extension swap. Output is byte-identical to the outfile-based fix in both modes, and the unbundled output is byte-identical to main. The bundled test case now also asserts the browser target holds exactly one .js file. The entry list is the only remaining difference between the two modes, so without it a build:bundle that stopped bundling would pass. --- integrations/build.js | 11 ++--------- .../build_setup/integrations_build_test.ts | 7 +++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/integrations/build.js b/integrations/build.js index aa21e488d..92d99e5fa 100644 --- a/integrations/build.js +++ b/integrations/build.js @@ -55,15 +55,8 @@ function build({ buildOptions.banner = {js: licenseHeaderText}; } - if (bundle) { - buildOptions.entryPoints = [`./src/${entry}`]; - // Keep the emitted filename aligned with the entry so package.json's - // "browser" field keeps resolving to dist/web/index_web.js. - buildOptions.outfile = `./dist/${targetDir}/${entry.replace(/\.ts$/, '.js')}`; - } else { - buildOptions.entryPoints = ['./src/**/*.ts']; - buildOptions.outdir = `./dist/${targetDir}`; - } + buildOptions.entryPoints = bundle ? [`./src/${entry}`] : ['./src/**/*.ts']; + buildOptions.outdir = `./dist/${targetDir}`; if (format === 'esm') { buildOptions.banner = { diff --git a/tests/integration/build_setup/integrations_build_test.ts b/tests/integration/build_setup/integrations_build_test.ts index bd4cfd518..a81bcc8bd 100644 --- a/tests/integration/build_setup/integrations_build_test.ts +++ b/tests/integration/build_setup/integrations_build_test.ts @@ -67,6 +67,13 @@ describe('integrations build output', () => { await expectEmitted('browser', manifest.browser); await expectEmitted('main', manifest.main); await expectEmitted('module', manifest.module); + + const webDir = path.join(packageDir, path.dirname(manifest.browser)); + const emitted = await fs.readdir(webDir); + expect( + emitted.filter((name) => name.endsWith('.js')), + 'the bundled build must emit one bundle, not one file per source', + ).toEqual([path.basename(manifest.browser)]); }, BUILD_TIMEOUT_MS, ); From 2ef32664cbdaaac0c7dfc3965c9e34e76044af03 Mon Sep 17 00:00:00 2001 From: Amaad Martin Date: Fri, 7 Aug 2026 12:18:41 -0700 Subject: [PATCH 3/3] Fix: set entryPoints and outdir in the buildOptions literal Both fields are unconditional once the bundle branch is gone, so assigning them after construction buys nothing and separates the two banner conditionals. Post-construction mutation is now reserved for banner, which is the only genuinely conditional field. The emitted dist/ is byte-identical in both modes. --- integrations/build.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/integrations/build.js b/integrations/build.js index 92d99e5fa..20aef339e 100644 --- a/integrations/build.js +++ b/integrations/build.js @@ -40,6 +40,8 @@ function build({ entry = 'index.ts', }) { const buildOptions = { + entryPoints: bundle ? [`./src/${entry}`] : ['./src/**/*.ts'], + outdir: `./dist/${targetDir}`, target: platformBuildTargets[platform], platform, format, @@ -55,9 +57,6 @@ function build({ buildOptions.banner = {js: licenseHeaderText}; } - buildOptions.entryPoints = bundle ? [`./src/${entry}`] : ['./src/**/*.ts']; - buildOptions.outdir = `./dist/${targetDir}`; - if (format === 'esm') { buildOptions.banner = { js: