mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
fix: codebase preview in standalone mode
- Fix standalone bundle path lookup in worker to not add redundant file extension (the path already contains .tar/.esm suffixes from the API) - Fix CLI preview tar bundle handling to preserve binary data correctly (was using btoa(blob.text()) which corrupted binary tar data) - Add integration tests for script/flow preview commands covering: - Regular scripts (non-codebase) - Codebase scripts (CJS and ESM formats) - Codebase scripts with assets (tar bundles) - Flow preview Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -635,10 +635,9 @@ async fn pull_codebase(w_id: &str, id: &str, job_dir: &str) -> Result<PulledCode
|
||||
&& object_store.is_none()
|
||||
{
|
||||
let bun_cache_path = format!(
|
||||
"{}/{}.{}",
|
||||
"{}/{}",
|
||||
*windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR,
|
||||
path,
|
||||
if is_tar { "tar" } else { "js" }
|
||||
path
|
||||
);
|
||||
if std::fs::metadata(&bun_cache_path).is_ok() {
|
||||
tracing::info!("loading {bun_cache_path} from standalone bundle cache");
|
||||
|
||||
@@ -1125,7 +1125,7 @@ async function preview(
|
||||
const codebase =
|
||||
language == "bun" ? findCodebase(filePath, codebases) : undefined;
|
||||
|
||||
let bundledContent: string | undefined = undefined;
|
||||
let bundledContent: string | Blob | undefined = undefined;
|
||||
let isTar = false;
|
||||
|
||||
if (codebase) {
|
||||
@@ -1179,8 +1179,7 @@ async function preview(
|
||||
const fil = new File([file.contents as any], file.path.substring(1));
|
||||
tarball.append(fil);
|
||||
}
|
||||
const blob = await streamToBlob(tarball.stream());
|
||||
bundledContent = btoa(await blob.text());
|
||||
bundledContent = await streamToBlob(tarball.stream());
|
||||
isTar = true;
|
||||
} else if (Array.isArray(codebase.assets) && codebase.assets.length > 0) {
|
||||
// Handle assets
|
||||
@@ -1196,14 +1195,14 @@ async function preview(
|
||||
const file = new File([blob], asset.to);
|
||||
tarball.append(file);
|
||||
}
|
||||
const blob = await streamToBlob(tarball.stream());
|
||||
bundledContent = btoa(await blob.text());
|
||||
bundledContent = await streamToBlob(tarball.stream());
|
||||
isTar = true;
|
||||
}
|
||||
|
||||
if (!opts.silent) {
|
||||
const size = typeof bundledContent === "string" ? bundledContent.length : bundledContent.size;
|
||||
log.info(
|
||||
`Bundled ${filePath}: ${(bundledContent.length / 1024).toFixed(0)}kB (${(
|
||||
`Bundled ${filePath}: ${(size / 1024).toFixed(0)}kB (${(
|
||||
endTime - startTime
|
||||
).toFixed(0)}ms)`
|
||||
);
|
||||
@@ -1229,7 +1228,9 @@ async function preview(
|
||||
form.append("preview", JSON.stringify(previewPayload));
|
||||
form.append(
|
||||
"file",
|
||||
new Blob([bundledContent], { type: "application/javascript" })
|
||||
typeof bundledContent === "string"
|
||||
? new Blob([bundledContent], { type: "application/javascript" })
|
||||
: bundledContent
|
||||
);
|
||||
|
||||
const url =
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { withTestBackend, cleanupTestBackend } from "./test_backend.ts";
|
||||
|
||||
// =============================================================================
|
||||
// PREVIEW COMMAND INTEGRATION TESTS
|
||||
// Tests for `wmill script preview` and `wmill flow preview` commands
|
||||
//
|
||||
// These tests require a backend to run. Run with:
|
||||
// deno test --allow-all test/preview.test.ts
|
||||
//
|
||||
// The tests cover:
|
||||
// - Regular script preview (non-codebase)
|
||||
// - Codebase script preview (CJS format)
|
||||
// - Codebase script preview (ESM format)
|
||||
// - Codebase script preview with assets (tar bundle)
|
||||
// - Codebase script preview with ESM + tar
|
||||
// - Flow preview
|
||||
// =============================================================================
|
||||
|
||||
// Helper to create a wmill.yaml config file
|
||||
async function createWmillConfig(
|
||||
tempDir: string,
|
||||
config: {
|
||||
defaultTs?: "bun" | "deno";
|
||||
codebases?: Array<{
|
||||
relative_path: string;
|
||||
includes?: string[];
|
||||
format?: "cjs" | "esm";
|
||||
assets?: Array<{ from: string; to: string }>;
|
||||
}>;
|
||||
}
|
||||
): Promise<void> {
|
||||
let yamlContent = `defaultTs: ${config.defaultTs ?? "bun"}\n`;
|
||||
|
||||
if (config.codebases && config.codebases.length > 0) {
|
||||
yamlContent += "codebases:\n";
|
||||
for (const cb of config.codebases) {
|
||||
yamlContent += ` - relative_path: ${cb.relative_path}\n`;
|
||||
yamlContent += " includes:\n";
|
||||
for (const inc of cb.includes ?? ["**"]) {
|
||||
yamlContent += ` - "${inc}"\n`;
|
||||
}
|
||||
if (cb.format) {
|
||||
yamlContent += ` format: ${cb.format}\n`;
|
||||
}
|
||||
if (cb.assets && cb.assets.length > 0) {
|
||||
yamlContent += " assets:\n";
|
||||
for (const asset of cb.assets) {
|
||||
yamlContent += ` - from: ${asset.from}\n`;
|
||||
yamlContent += ` to: ${asset.to}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, yamlContent);
|
||||
}
|
||||
|
||||
// Helper to create a script file with metadata
|
||||
async function createScript(
|
||||
tempDir: string,
|
||||
path: string,
|
||||
content: string,
|
||||
metadata?: {
|
||||
summary?: string;
|
||||
description?: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const dir = `${tempDir}/${path.substring(0, path.lastIndexOf("/"))}`;
|
||||
await Deno.mkdir(dir, { recursive: true });
|
||||
await Deno.writeTextFile(`${tempDir}/${path}`, content);
|
||||
|
||||
// Create metadata file
|
||||
const metaPath = path.replace(/\.[^.]+$/, ".script.yaml");
|
||||
const metaContent = `summary: "${metadata?.summary ?? "Test script"}"
|
||||
description: "${metadata?.description ?? "Test script description"}"
|
||||
lock: ""
|
||||
schema:
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema"
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
default: "World"
|
||||
required: []
|
||||
`;
|
||||
await Deno.writeTextFile(`${tempDir}/${metaPath}`, metaContent);
|
||||
}
|
||||
|
||||
// Helper to create a flow directory with flow.yaml
|
||||
async function createFlow(
|
||||
tempDir: string,
|
||||
flowPath: string,
|
||||
options: {
|
||||
summary: string;
|
||||
scriptContent: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const dir = `${tempDir}/${flowPath}`;
|
||||
await Deno.mkdir(dir, { recursive: true });
|
||||
|
||||
const flowYaml = `summary: "${options.summary}"
|
||||
description: "Test flow"
|
||||
value:
|
||||
modules:
|
||||
- id: "a"
|
||||
value:
|
||||
type: "rawscript"
|
||||
language: "bun"
|
||||
content: |
|
||||
${options.scriptContent.split("\n").map(line => ` ${line}`).join("\n")}
|
||||
schema:
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema"
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
default: "World"
|
||||
required: []
|
||||
`;
|
||||
await Deno.writeTextFile(`${dir}/flow.yaml`, flowYaml);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SCRIPT PREVIEW TESTS
|
||||
// =============================================================================
|
||||
|
||||
Deno.test({
|
||||
name: "script preview: regular script (non-codebase)",
|
||||
async fn() {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, { defaultTs: "bun" });
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/test/simple_script.ts",
|
||||
`export function main(name: string = "World") {
|
||||
return \`Hello, \${name}!\`;
|
||||
}`
|
||||
);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "preview", "f/test/simple_script.ts"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`);
|
||||
assertStringIncludes(result.stdout + result.stderr, "Hello, World!");
|
||||
});
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "script preview: codebase script (CJS)",
|
||||
async fn() {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, {
|
||||
defaultTs: "bun",
|
||||
codebases: [{ relative_path: "f/codebase", includes: ["**"] }],
|
||||
});
|
||||
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/codebase/cjs_script.ts",
|
||||
`export function main(name: string = "World") {
|
||||
console.log("CJS codebase script running");
|
||||
return \`Hello from CJS codebase, \${name}!\`;
|
||||
}`
|
||||
);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "preview", "f/codebase/cjs_script.ts"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`);
|
||||
assertStringIncludes(result.stdout + result.stderr, "Hello from CJS codebase, World!");
|
||||
});
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "script preview: codebase script (ESM)",
|
||||
async fn() {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, {
|
||||
defaultTs: "bun",
|
||||
codebases: [{ relative_path: "f/codebase_esm", includes: ["**"], format: "esm" }],
|
||||
});
|
||||
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/codebase_esm/esm_script.ts",
|
||||
`export function main(name: string = "World") {
|
||||
console.log("ESM codebase script running");
|
||||
return \`Hello from ESM codebase, \${name}!\`;
|
||||
}`
|
||||
);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "preview", "f/codebase_esm/esm_script.ts"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`);
|
||||
assertStringIncludes(result.stdout + result.stderr, "Hello from ESM codebase, World!");
|
||||
});
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "script preview: codebase script with assets (tar)",
|
||||
async fn() {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, {
|
||||
defaultTs: "bun",
|
||||
codebases: [{
|
||||
relative_path: "f/codebase_tar",
|
||||
includes: ["**"],
|
||||
assets: [{ from: "f/codebase_tar/data.json", to: "data.json" }],
|
||||
}],
|
||||
});
|
||||
|
||||
// Create asset file
|
||||
await Deno.mkdir(`${tempDir}/f/codebase_tar`, { recursive: true });
|
||||
await Deno.writeTextFile(
|
||||
`${tempDir}/f/codebase_tar/data.json`,
|
||||
JSON.stringify({ message: "Hello from asset!" })
|
||||
);
|
||||
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/codebase_tar/tar_script.ts",
|
||||
`import * as fs from "fs";
|
||||
|
||||
export function main(name: string = "World") {
|
||||
console.log("Tar codebase script running");
|
||||
const data = fs.readFileSync("data.json", "utf-8");
|
||||
const parsed = JSON.parse(data);
|
||||
return \`Hello \${name}! Asset says: \${parsed.message}\`;
|
||||
}`
|
||||
);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "preview", "f/codebase_tar/tar_script.ts"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`);
|
||||
assertStringIncludes(result.stdout + result.stderr, "Hello World! Asset says: Hello from asset!");
|
||||
});
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "script preview: codebase script ESM + tar (assets)",
|
||||
async fn() {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, {
|
||||
defaultTs: "bun",
|
||||
codebases: [{
|
||||
relative_path: "f/codebase_esm_tar",
|
||||
includes: ["**"],
|
||||
format: "esm",
|
||||
assets: [{ from: "f/codebase_esm_tar/config.json", to: "config.json" }],
|
||||
}],
|
||||
});
|
||||
|
||||
// Create asset file
|
||||
await Deno.mkdir(`${tempDir}/f/codebase_esm_tar`, { recursive: true });
|
||||
await Deno.writeTextFile(
|
||||
`${tempDir}/f/codebase_esm_tar/config.json`,
|
||||
JSON.stringify({ setting: "esm_tar_value" })
|
||||
);
|
||||
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/codebase_esm_tar/esm_tar_script.ts",
|
||||
`import * as fs from "fs";
|
||||
|
||||
export function main(name: string = "World") {
|
||||
console.log("ESM + tar codebase script running");
|
||||
const config = fs.readFileSync("config.json", "utf-8");
|
||||
const parsed = JSON.parse(config);
|
||||
return \`Hello \${name}! Config setting: \${parsed.setting}\`;
|
||||
}`
|
||||
);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "preview", "f/codebase_esm_tar/esm_tar_script.ts"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`);
|
||||
assertStringIncludes(result.stdout + result.stderr, "Hello World! Config setting: esm_tar_value");
|
||||
});
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// FLOW PREVIEW TESTS
|
||||
// =============================================================================
|
||||
|
||||
Deno.test({
|
||||
name: "flow preview: simple flow",
|
||||
async fn() {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, { defaultTs: "bun" });
|
||||
await createFlow(tempDir, "f/test/simple_flow.flow", {
|
||||
summary: "Test flow",
|
||||
scriptContent: `export function main(name: string = "World") { return \`Flow says: Hello, \${name}!\`; }`,
|
||||
});
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "preview", "f/test/simple_flow.flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
assertEquals(result.code, 0, `Flow preview failed: ${result.stderr}\n${result.stdout}`);
|
||||
assertStringIncludes(result.stdout + result.stderr, "Flow says: Hello, World!");
|
||||
});
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// CLEANUP
|
||||
// =============================================================================
|
||||
|
||||
Deno.test({
|
||||
name: "cleanup test backend",
|
||||
async fn() {
|
||||
await cleanupTestBackend();
|
||||
},
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
});
|
||||
Reference in New Issue
Block a user