diff --git a/core/build.js b/core/build.js index 99d01f9d7..c015ac3f6 100644 --- a/core/build.js +++ b/core/build.js @@ -69,7 +69,10 @@ function build({ buildOptions.outdir = `./dist/${targetDir}`; } - if (format === 'esm') { + // The createRequire preamble lets Node ESM output reach CommonJS-only + // dependencies. It is meaningless in a browser, where 'module' is an + // unresolvable bare specifier, so restrict it to the Node targets. + if (platform === 'node' && format === 'esm') { buildOptions.banner = { js: (buildOptions.banner?.js || '') + diff --git a/integrations/build.js b/integrations/build.js index d07ba5194..cf307e1bc 100644 --- a/integrations/build.js +++ b/integrations/build.js @@ -63,7 +63,10 @@ function build({ buildOptions.outdir = `./dist/${targetDir}`; } - if (format === 'esm') { + // The createRequire preamble lets Node ESM output reach CommonJS-only + // dependencies. It is meaningless in a browser, where 'module' is an + // unresolvable bare specifier, so restrict it to the Node targets. + if (platform === 'node' && format === 'esm') { buildOptions.banner = { js: (buildOptions.banner?.js || '') + diff --git a/tests/integration/build_output/web_output_test.ts b/tests/integration/build_output/web_output_test.ts new file mode 100644 index 000000000..24dd03eec --- /dev/null +++ b/tests/integration/build_output/web_output_test.ts @@ -0,0 +1,66 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import {beforeAll, describe, expect, it} from 'vitest'; + +const repoRoot = process.cwd(); + +/** + * Matches an import of the Node `module` builtin, with or without the `node:` + * protocol, so the check holds whichever specifier the build banner uses. + */ +const MODULE_BUILTIN_IMPORT = /from\s*['"](?:node:)?module['"]/; + +/** Lists the emitted `.js` files under `dir`, relative to `dir`. */ +async function listEmittedJs(dir: string): Promise { + const entries = await fs.readdir(dir, {recursive: true}); + return entries.filter((entry) => entry.endsWith('.js')); +} + +describe.each(['core', 'integrations'])('%s browser build', (pkg: string) => { + const webDir = path.join(repoRoot, pkg, 'dist', 'web'); + let emittedJs: string[]; + + beforeAll(async () => { + const stats = await fs.stat(webDir).catch(() => undefined); + if (!stats?.isDirectory()) { + expect.fail(`${webDir} is missing. Run \`npm run build\` first.`); + } + + emittedJs = await listEmittedJs(webDir); + if (emittedJs.length === 0) { + expect.fail(`${webDir} holds no .js files. Run \`npm run build\` first.`); + } + }); + + it('emits the entry point the package browser field resolves to', () => { + expect(emittedJs).toContain('index_web.js'); + }); + + it('never imports the Node module builtin', async () => { + const offenders: string[] = []; + for (const file of emittedJs) { + const contents = await fs.readFile(path.join(webDir, file), 'utf8'); + if (MODULE_BUILTIN_IMPORT.test(contents)) { + offenders.push(file); + } + } + + expect(offenders).toEqual([]); + }); +}); + +describe('core Node ESM build', () => { + it('keeps the createRequire preamble', async () => { + const contents = await fs.readFile( + path.join(repoRoot, 'core', 'dist', 'esm', 'index.js'), + 'utf8', + ); + + expect(contents).toMatch(MODULE_BUILTIN_IMPORT); + }); +});