Files
codeql-action/build.mjs
T
Mads Navntoft 436aa0401d PoC: switch esbuild bundles to ESM with code splitting + minify
Bundles each entry point as ESM with esbuild's splitting:true so
shared transitive dependencies (Azure storage, undici, octokit, ...)
live in shared chunk files under lib/chunks/ instead of being
duplicated into every entry bundle.

A small banner is injected so bundled CommonJS deps that call
require(), __filename or __dirname at runtime still work. A
lib/package.json with { "type": "module" } is written by the build
so the bundles can keep the .js extension while being loaded as ESM
by Node; this scope avoids touching the tsc output in build/ and the
action.yml entrypoints stay unchanged.

On a measured tar.gz of the working tree this takes the repo from
~9.5 MB to ~2.6 MB compressed (lib/ alone: 8.0 MB -> 0.7 MB).
2026-05-14 14:43:23 +02:00

136 lines
3.8 KiB
JavaScript

import { copyFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import * as esbuild from "esbuild";
import { globSync } from "glob";
import pkg from "./package.json" with { type: "json" };
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const SRC_DIR = join(__dirname, "src");
const OUT_DIR = join(__dirname, "lib");
/**
* Clean the output directory before building.
*
* @type {esbuild.Plugin}
*/
const cleanPlugin = {
name: "clean",
setup(build) {
build.onStart(async () => {
await rm(OUT_DIR, { recursive: true, force: true });
});
},
};
/**
* Copy defaults.json to the output directory since other projects depend on it.
*
* @type {esbuild.Plugin}
*/
const copyDefaultsPlugin = {
name: "copy-defaults",
setup(build) {
build.onEnd(async () => {
await rm(join(OUT_DIR, "defaults.json"), {
force: true,
});
await copyFile(
join(SRC_DIR, "defaults.json"),
join(OUT_DIR, "defaults.json"),
);
});
},
};
/**
* Mark `lib/` as an ESM scope by writing `lib/package.json` with
* `{ "type": "module" }`. This lets the bundles use the regular `.js`
* extension while still being loaded as ESM by Node, without affecting
* the rest of the repo (the root package.json stays CJS so the tsc
* output in `build/` and any other consumers are unchanged).
*
* @type {esbuild.Plugin}
*/
const writeLibPackageJsonPlugin = {
name: "write-lib-package-json",
setup(build) {
build.onEnd(async () => {
await writeFile(
join(OUT_DIR, "package.json"),
JSON.stringify({ type: "module" }) + "\n",
);
});
},
};
/**
* Log when the build ends.
*
* @type {esbuild.Plugin}
*/
const onEndPlugin = {
name: "on-end",
setup(build) {
build.onEnd((result) => {
// eslint-disable-next-line no-console
console.log(`Build ended with ${result.errors.length} errors`);
});
},
};
// Banner injected into every emitted ESM file so that bundled CommonJS
// dependencies which call `require(...)` at runtime (e.g. parts of the
// Azure SDK + undici stack pulled in transitively by `@actions/cache` and
// `@actions/artifact`), or read `__filename` / `__dirname`, keep working.
const esmCompatBanner = [
`import { createRequire as __codeqlCreateRequire } from "module";`,
`import { fileURLToPath as __codeqlFileURLToPath } from "url";`,
`import { dirname as __codeqlDirname } from "path";`,
`var require = __codeqlCreateRequire(import.meta.url);`,
`var __filename = __codeqlFileURLToPath(import.meta.url);`,
`var __dirname = __codeqlDirname(__filename);`,
].join("");
const context = await esbuild.context({
// Include upload-lib.ts as an entry point for use in testing environments.
entryPoints: globSync([
`${SRC_DIR}/*-action.ts`,
`${SRC_DIR}/*-action-post.ts`,
"src/upload-lib.ts",
]),
bundle: true,
// Use ESM with code splitting so shared modules (Azure storage, undici,
// octokit, ...) live in shared chunk files instead of being duplicated
// into every entry bundle. Node treats these `.js` files as ESM because
// `writeLibPackageJsonPlugin` writes `lib/package.json` with
// `"type": "module"`.
format: "esm",
splitting: true,
minify: true,
chunkNames: "chunks/chunk-[hash]",
banner: { js: esmCompatBanner },
outdir: OUT_DIR,
platform: "node",
plugins: [
cleanPlugin,
copyDefaultsPlugin,
writeLibPackageJsonPlugin,
onEndPlugin,
],
target: ["node20"],
define: {
__CODEQL_ACTION_VERSION__: JSON.stringify(pkg.version),
},
metafile: true,
});
const result = await context.rebuild();
await writeFile(join(__dirname, "meta.json"), JSON.stringify(result.metafile));
await context.dispose();