mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 00:02:13 +00:00
* fix(cli): fall back to esbuild-wasm on native host/binary mismatch Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): guard tarball extraction, extend esbuild-wasm fallback to script bundling Address CI review: prevent tar-slip in esbuild-wasm package extraction, route codebase/script and inline-rawscript bundling through getEsbuild() too, and move the loader to utils. Add a unit test for the tar-slip guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): make esbuild-wasm fallback concurrency-safe Address CI review (P1): memoize getEsbuild() on an in-flight promise so concurrent first callers (parallel wmill sync push) share one probe/download instead of racing, and give each extraction a unique temp dir so concurrent extractions can't clobber each other. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
158 lines
4.7 KiB
TypeScript
158 lines
4.7 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { stat } from "node:fs/promises";
|
|
import { readTextFile } from "./utils.ts";
|
|
import { getEsbuild } from "./esbuild_loader.ts";
|
|
import type { SyncCodebase } from "./codebase.ts";
|
|
import { parseMetadataFileIfExists } from "./metadata.ts";
|
|
import { inferContentTypeFromFilePath } from "./script_common.ts";
|
|
import { findCodebase } from "../commands/sync/sync.ts";
|
|
import type { LocalScriptInfo } from "../../windmill-utils-internal/src/inline-scripts/replacer.ts";
|
|
import type { RawScript } from "../../../gen/types.gen.ts";
|
|
|
|
export class UnsupportedLocalPathScriptPreviewError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = "UnsupportedLocalPathScriptPreviewError";
|
|
}
|
|
}
|
|
|
|
async function readOptionalLock(scriptPath: string): Promise<string | undefined> {
|
|
try {
|
|
return await readTextFile(scriptPath + ".script.lock");
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function normalizeOptionalLock(lock: string | undefined): string | undefined {
|
|
return typeof lock === "string" && lock.trim() === "" ? undefined : lock;
|
|
}
|
|
|
|
async function bundleSingleFileCodebaseScript(
|
|
filePath: string,
|
|
codebase: SyncCodebase
|
|
): Promise<string> {
|
|
if (codebase.customBundler) {
|
|
// Pass the script path as a positional shell argument so existing shell-based
|
|
// custom bundlers still work without interpolating the path into the command.
|
|
return execFileSync(
|
|
"sh",
|
|
["-lc", `${codebase.customBundler} "$1"`, "sh", filePath],
|
|
{
|
|
maxBuffer: 1024 * 1024 * 50,
|
|
}
|
|
).toString();
|
|
}
|
|
|
|
const esbuild = await getEsbuild();
|
|
const out = await esbuild.build({
|
|
entryPoints: [filePath],
|
|
// Inline rawscripts are executed through the standard module wrapper,
|
|
// so the bundle must expose `main` as an ESM export.
|
|
format: "esm",
|
|
bundle: true,
|
|
write: false,
|
|
external: codebase.external,
|
|
inject: codebase.inject,
|
|
define: codebase.define,
|
|
loader: codebase.loader ?? { ".node": "file" },
|
|
outdir: "/",
|
|
platform: "node",
|
|
packages: "bundle",
|
|
target: "esnext",
|
|
banner: codebase.banner,
|
|
});
|
|
|
|
if (out.outputFiles.length === 0) {
|
|
throw new Error(`No output files found for ${filePath}`);
|
|
}
|
|
if (out.outputFiles.length > 1) {
|
|
throw new UnsupportedLocalPathScriptPreviewError(
|
|
`Local PathScript ${filePath} requires a multi-file bundle, which flow preview/dev cannot inline yet`
|
|
);
|
|
}
|
|
if (Array.isArray(codebase.assets) && codebase.assets.length > 0) {
|
|
throw new UnsupportedLocalPathScriptPreviewError(
|
|
`Local PathScript ${filePath} requires codebase assets, which flow preview/dev cannot inline yet`
|
|
);
|
|
}
|
|
|
|
return out.outputFiles[0].text;
|
|
}
|
|
|
|
export function createPreviewLocalScriptReader(opts: {
|
|
exts: string[];
|
|
defaultTs?: "bun" | "deno";
|
|
codebases: SyncCodebase[];
|
|
}): (scriptPath: string) => Promise<LocalScriptInfo | undefined> {
|
|
return async (scriptPath) => {
|
|
const localScript = await resolvePreviewLocalScriptState(scriptPath, opts);
|
|
if (!localScript) {
|
|
return undefined;
|
|
}
|
|
|
|
const content = localScript.codebase
|
|
? await bundleSingleFileCodebaseScript(localScript.filePath, localScript.codebase)
|
|
: localScript.content;
|
|
|
|
return {
|
|
content,
|
|
language: localScript.language,
|
|
lock: localScript.lock,
|
|
tag: localScript.tag,
|
|
};
|
|
};
|
|
}
|
|
|
|
export type PreviewLocalScriptState = {
|
|
filePath: string;
|
|
content: string;
|
|
language: RawScript["language"];
|
|
lock?: string;
|
|
tag?: string;
|
|
codebase?: SyncCodebase;
|
|
codebaseDigest?: string;
|
|
};
|
|
|
|
export async function resolvePreviewLocalScriptState(
|
|
scriptPath: string,
|
|
opts: {
|
|
exts: string[];
|
|
defaultTs?: "bun" | "deno";
|
|
codebases: SyncCodebase[];
|
|
}
|
|
): Promise<PreviewLocalScriptState | undefined> {
|
|
for (const ext of opts.exts) {
|
|
const filePath = scriptPath + ext;
|
|
let fileStat;
|
|
try {
|
|
fileStat = await stat(filePath);
|
|
} catch {
|
|
continue;
|
|
}
|
|
if (!fileStat.isFile()) continue;
|
|
|
|
const language = inferContentTypeFromFilePath(filePath, opts.defaultTs);
|
|
const metadata = await parseMetadataFileIfExists(scriptPath);
|
|
const rawLock = metadata?.payload?.lock ?? (await readOptionalLock(scriptPath));
|
|
const codebase =
|
|
language === "bun" ? findCodebase(filePath, opts.codebases) : undefined;
|
|
|
|
return {
|
|
filePath,
|
|
content: await readTextFile(filePath),
|
|
language,
|
|
lock: normalizeOptionalLock(rawLock),
|
|
tag: metadata?.payload?.tag,
|
|
codebase,
|
|
codebaseDigest: codebase
|
|
? await codebase.getDigest(
|
|
Array.isArray(codebase.assets) && codebase.assets.length > 0
|
|
)
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|