Files
windmill/cli/src/utils/codebase.ts
T
hugocasaandClaude Opus 5.5 9b46b21f51 feat(cli): include extra paths in codebase digest for monorepo imports (#11327)
* feat(cli): hash codebase extra_digest_paths and warn on uncovered bundle inputs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): cache codebase digest only once complete, skip entry in warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): accept string extra_digest_paths, always print uncovered-input warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): clarify extra_digest_paths resolution and missing-path error

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 14:54:13 +02:00

96 lines
3.1 KiB
TypeScript

import { Codebase, SyncOptions } from "../core/conf.ts";
import * as log from "../core/log.ts";
import { digestDir, generateHash, generateHashFromBuffer } from "./utils.ts";
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
// A bare string is accepted, like `includes`/`excludes` in findCodebase.
function extraDigestPaths(codebase: Codebase): string[] {
return ([] as string[]).concat(codebase.extra_digest_paths ?? []);
}
async function digestPath(p: string): Promise<string> {
let s;
try {
s = await stat(p);
} catch {
throw new Error(
`Codebase extra_digest_paths entry not found: ${p} (resolved to ${path.resolve(p)})`
);
}
return s.isDirectory()
? await digestDir(p, "")
: await generateHashFromBuffer(await readFile(p));
}
/** Bundle inputs that no codebase digest covers, so edits to them would not trigger a re-push. */
export function uncoveredBundleInputs(
codebase: Codebase,
entry: string,
inputs: string[]
): string[] {
const entryAbs = path.resolve(entry);
const roots = [codebase.relative_path, ...extraDigestPaths(codebase)]
.map((r) => path.resolve(r));
return inputs.filter((i) => {
// non-file namespaces ("<define:x>", "(disabled):x", "ns:path") and installed packages
if (/^[<(]|^[a-z-]{2,}:/i.test(i) || i.includes("node_modules")) return false;
const abs = path.resolve(i);
return abs != entryAbs && !roots.some((r) => abs == r || abs.startsWith(r + path.sep));
});
}
export type SyncCodebase = Codebase & {
getDigest: (forceTar?: boolean) => Promise<string>;
};
export function listSyncCodebases(options: SyncOptions): SyncCodebase[] {
const res: SyncCodebase[] = [];
const nb_codebase = options?.codebases?.length ?? 0;
if (nb_codebase > 0) {
log.info(
`Found ${nb_codebase} codebases: ${options?.codebases
?.map((c) => c.relative_path)
.join(", ")}`
);
}
for (const codebase of options?.codebases ?? []) {
let _digest: string | undefined = undefined;
let alreadyPrinted = false;
let hasAssets = false;
const getDigest: (forceTar?: boolean) => Promise<string> = async (
forceTar?: boolean
) => {
if (_digest == undefined) {
// Concurrent callers read `_digest` across these awaits: assign it only once complete.
let digest = await digestDir(
codebase.relative_path,
JSON.stringify(codebase)
);
const extra = extraDigestPaths(codebase);
if (extra.length > 0) {
const hashes = await Promise.all(extra.map(digestPath));
digest = await generateHash(digest + hashes.join(""));
}
if (codebase.format == "esm") {
digest += ".esm";
}
_digest = digest;
if (!alreadyPrinted) {
alreadyPrinted = true;
log.info(`Codebase ${codebase.relative_path}, digest: ${_digest}`);
}
hasAssets =
Array.isArray(codebase.assets) && codebase.assets.length > 0;
}
if (forceTar || hasAssets) {
return _digest + ".tar";
} else {
return _digest;
}
};
res.push({ ...codebase, getDigest });
}
return res;
}