Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 104 additions & 8 deletions build.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { copyFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { basename, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import * as esbuild from "esbuild";
Expand All @@ -13,6 +13,72 @@ const __dirname = dirname(__filename);
const SRC_DIR = join(__dirname, "src");
const OUT_DIR = join(__dirname, "lib");

/**
* Name of the shared entrypoint file that imports each Action's code. By introducing a single
* entrypoint for all the Actions, we avoid duplicating code across each Action's bundle.
*/
const SHARED_ENTRYPOINT = "actions-entrypoint";

/** The names of all the Action entry points (as referenced by `action.yml`s). */
function findActionNames() {
return globSync([
`${SRC_DIR}/*-action.ts`,
`${SRC_DIR}/*-action-post.ts`,
])
.map((p) => basename(p, ".ts"))
.sort();
}

const ACTION_NAMES = findActionNames();

/**
* Generate the source for the shared entry point. The generated module dispatches at runtime to the
* Action selected by `CODEQL_ACTION_ENTRYPOINT`, using `require()` to incorporate each Action's
* code without executing the top-level side effects.
*/
function generateEntrypointTypescriptSource() {
const cases = ACTION_NAMES
.map(
(name) =>
` case ${JSON.stringify(name)}:\n require("./${name}");\n break;`,
)
.join("\n");
return `const entrypoint = process.env.CODEQL_ACTION_ENTRYPOINT;
switch (entrypoint) {
${cases}
default:
throw new Error(
\`Unknown CodeQL Action entrypoint: \${JSON.stringify(entrypoint)}. \` +
"This file is intended to be invoked via the generated stubs in lib/.",
);
}
`;
}

/**
* Resolve the virtual shared entry point and provide its generated source to esbuild without
* writing it to disk.
*
* @type {esbuild.Plugin}
*/
const virtualEntrypointPlugin = {
name: "virtual-actions-entrypoint",
setup(build) {
const namespace = "actions-entrypoint";
// Ideally, we'd `RegExp.escape` the entrypoint here, but that API isn't supported in Node 20. Since we're dealing with a hardcoded string, this isn't too much of a problem.
build.onResolve({ filter: new RegExp(`^${SHARED_ENTRYPOINT}$`) }, () => ({
path: SHARED_ENTRYPOINT,
namespace,
}));
// Restrict using the namespace. The path filter does not need to discriminate any further.
build.onLoad({ filter: /.*/, namespace }, () => ({
contents: generateEntrypointTypescriptSource(),
resolveDir: SRC_DIR,
loader: "ts",
}));
},
};

/**
* Clean the output directory before building.
*
Expand Down Expand Up @@ -62,18 +128,48 @@ const onEndPlugin = {
},
};

/**
* Emit a tiny stub file for each Action entrypoint. Each stub sets an environment variable
* identifying which action was invoked and then `require()`s the shared bundle, which dispatches to
* the correct Action's code.
*
* @type {esbuild.Plugin}
*/
const emitActionStubsPlugin = {
name: "emit-action-stubs",
setup(build) {
build.onEnd(async () => {
await Promise.all(
ACTION_NAMES.map(async (name) => {
const stub =
`"use strict";\n` +
`process.env.CODEQL_ACTION_ENTRYPOINT = ${JSON.stringify(name)};\n` +
`require("./${SHARED_ENTRYPOINT}.js");\n`;
await writeFile(join(OUT_DIR, `${name}.js`), stub);
}),
);
});
},
};

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 every action together via the shared entry point. We also keep
// `upload-lib.ts` as a separate entry point for use in testing environments.
entryPoints: [
{ in: SHARED_ENTRYPOINT, out: SHARED_ENTRYPOINT },
join(SRC_DIR, "upload-lib.ts"),
],
bundle: true,
format: "cjs",
outdir: OUT_DIR,
platform: "node",
plugins: [cleanPlugin, copyDefaultsPlugin, onEndPlugin],
plugins: [
cleanPlugin,
copyDefaultsPlugin,
virtualEntrypointPlugin,
emitActionStubsPlugin,
onEndPlugin,
],
target: ["node20"],
define: {
__CODEQL_ACTION_VERSION__: JSON.stringify(pkg.version),
Expand Down
Loading
Loading