diff --git a/cli/src/commands/flow/flow_metadata.ts b/cli/src/commands/flow/flow_metadata.ts index 908e3c63dc..6a9040094f 100644 --- a/cli/src/commands/flow/flow_metadata.ts +++ b/cli/src/commands/flow/flow_metadata.ts @@ -29,7 +29,10 @@ import { FlowFile } from "./flow.ts"; import { FlowValue } from "../../../gen/types.gen.ts"; import { replaceInlineScripts } from "../../../windmill-utils-internal/src/inline-scripts/replacer.ts"; import { workspaceDependenciesLanguages } from "../../utils/script_common.ts"; -import { extractNameFromFolder, getFolderSuffix } from "../../utils/resource_folders.ts"; +import { + extractNameFromFolder, + getNonDottedPaths, +} from "../../utils/resource_folders.ts"; const TOP_HASH = "__flow_hash"; async function generateFlowHash( @@ -157,7 +160,9 @@ export async function generateFlowLockInternal( filteredDeps ); - const lockAssigner = newPathAssigner(opts.defaultTs ?? "bun"); + const lockAssigner = newPathAssigner(opts.defaultTs ?? "bun", { + skipInlineScriptSuffix: getNonDottedPaths(), + }); const inlineScripts = extractInlineScriptsForFlows( flowValue.value.modules, {}, diff --git a/cli/test/sync_pull_push.test.ts b/cli/test/sync_pull_push.test.ts index 613de2ef82..d98b412c39 100644 --- a/cli/test/sync_pull_push.test.ts +++ b/cli/test/sync_pull_push.test.ts @@ -1768,6 +1768,66 @@ excludes: [] }); }); +test("Integration: Sync pull with nonDottedPaths uses non-dotted inline script filenames", async () => { + await withTestBackend(async (backend, tempDir) => { + // Push a flow using default dotted paths + await writeFile( + `${tempDir}/wmill.yaml`, + `defaultTs: bun +includes: + - "**" +excludes: [] +`, + "utf-8", + ); + + const uniqueId = Date.now(); + const flowName = `f/test/nondot_pull_inline_${uniqueId}`; + const flowFixture = createFlowFixture(flowName); + await mkdir(`${tempDir}/f/test/nondot_pull_inline_${uniqueId}${getFolderSuffix("flow")}`, { recursive: true }); + for (const file of Object.values(flowFixture)) { + await writeFile(`${tempDir}/${file.path}`, file.content, "utf-8"); + } + + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--includes", `f/test/nondot_pull_inline_${uniqueId}*/**`], + tempDir, + ); + expect(pushResult.code).toEqual(0); + + // Pull into a fresh directory with nonDottedPaths enabled + const tempDir2 = await mkdtemp(join(tmpdir(), "wmill_nondot_inline_")); + try { + await writeFile( + `${tempDir2}/wmill.yaml`, + `defaultTs: bun +nonDottedPaths: true +includes: + - "**" +excludes: [] +`, + "utf-8", + ); + + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir2); + expect(pullResult.code).toEqual(0); + + // Verify pulled files use non-dotted inline script naming + const files = await listFilesRecursive(tempDir2); + const flowFiles = files.filter((f) => f.includes(`nondot_pull_inline_${uniqueId}`)); + + expect(flowFiles.length > 0).toBeTruthy(); + // Should use __flow folder, not .flow + expect(flowFiles.some((f) => f.includes("__flow/"))).toBeTruthy(); + // No files should have .inline_script. in their name + const dottedInlineFiles = flowFiles.filter((f) => f.includes(".inline_script.")); + expect(dottedInlineFiles.length).toEqual(0); + } finally { + await cleanupTempDir(tempDir2); + } + }); + }); + // ============================================================================= // ws_error_handler_muted Persistence Tests // ============================================================================= diff --git a/cli/test/unified_generate_metadata.test.ts b/cli/test/unified_generate_metadata.test.ts index 5cd240dce9..0e93ae5cea 100644 --- a/cli/test/unified_generate_metadata.test.ts +++ b/cli/test/unified_generate_metadata.test.ts @@ -8,7 +8,7 @@ import { expect, test, describe } from "bun:test"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; -import { writeFile } from "node:fs/promises"; +import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; import { createLocalScript, createLocalFlow, @@ -19,7 +19,12 @@ import { /** * Helper to set up a workspace with wmill.yaml */ -async function setupWorkspace(backend: any, tempDir: string, workspaceName: string) { +async function setupWorkspace( + backend: any, + tempDir: string, + workspaceName: string, + nonDottedPaths = false +) { const testWorkspace = { remote: backend.baseUrl, workspaceId: backend.workspace, @@ -29,11 +34,88 @@ async function setupWorkspace(backend: any, tempDir: string, workspaceName: stri await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun -includes: +${nonDottedPaths ? "nonDottedPaths: true\n" : ""}includes: - "**" excludes: []`, "utf-8"); } +async function createLocalNonDottedFlow(tempDir: string, name: string) { + const flowDir = `${tempDir}/f/test/${name}__flow`; + await mkdir(flowDir, { recursive: true }); + + await writeFile( + `${flowDir}/a.ts`, + `export async function main() {\n return "Hello from flow ${name}";\n}`, + "utf-8" + ); + + await writeFile( + `${flowDir}/flow.yaml`, + `summary: "${name} flow" +description: "A flow for testing" +value: + modules: + - id: a + value: + type: rawscript + content: "!inline a.ts" + language: bun + input_transforms: {} +schema: + $schema: "https://json-schema.org/draft/2020-12/schema" + type: object + properties: {} + required: [] +`, + "utf-8" + ); +} + +async function createLocalNonDottedApp(tempDir: string, name: string) { + const appDir = `${tempDir}/f/test/${name}__app`; + await mkdir(appDir, { recursive: true }); + + await writeFile( + `${appDir}/app.yaml`, + `summary: "${name} app" +value: + type: app + grid: + - id: button1 + data: + type: buttoncomponent + componentInput: + type: runnable + runnable: + type: runnableByName + inlineScript: + content: | + export async function main() { + return "hello from app"; + } + language: bun + hiddenInlineScripts: [] + css: {} + norefreshbar: false +policy: + on_behalf_of: null + on_behalf_of_email: null + triggerables: {} + execution_mode: viewer +`, + "utf-8" + ); +} + +async function fileExists(filePath: string): Promise { + try { + await stat(filePath); + return true; + } catch { + return false; + } +} + // ============================================================================= // Main test: processes scripts, flows, and apps together // ============================================================================= @@ -156,6 +238,87 @@ describe("generate-metadata flags", () => { }); }); + test("--lock-only preserves non-dotted flow filenames", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "lock_only_non_dotted_test", true); + + await createLocalNonDottedFlow(tempDir, "my_flow"); + + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "--lock-only"], + tempDir, + "lock_only_non_dotted_test" + ); + + expect(result.code).toEqual(0); + + const flowDir = `${tempDir}/f/test/my_flow__flow`; + const flowYaml = await readFile(`${flowDir}/flow.yaml`, "utf-8"); + + expect(flowYaml).toContain("!inline a.ts"); + expect(flowYaml).toContain("!inline a.lock"); + expect(flowYaml).not.toContain(".inline_script."); + expect(await fileExists(`${flowDir}/a.lock`)).toEqual(true); + expect(await fileExists(`${flowDir}/a.inline_script.ts`)).toEqual(false); + expect(await fileExists(`${flowDir}/a.inline_script.lock`)).toEqual(false); + }); + }); + + test("generate-metadata preserves non-dotted flow inline script filenames", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "full_gen_non_dotted_flow_test", true); + + await createLocalNonDottedFlow(tempDir, "my_flow"); + + const result = await backend.runCLICommand( + ["generate-metadata", "--yes"], + tempDir, + "full_gen_non_dotted_flow_test" + ); + + expect(result.code).toEqual(0); + + const flowDir = `${tempDir}/f/test/my_flow__flow`; + const flowYaml = await readFile(`${flowDir}/flow.yaml`, "utf-8"); + + // Inline script references should use non-dotted naming + expect(flowYaml).toContain("!inline a.ts"); + expect(flowYaml).toContain("!inline a.lock"); + expect(flowYaml).not.toContain(".inline_script."); + expect(await fileExists(`${flowDir}/a.ts`)).toEqual(true); + expect(await fileExists(`${flowDir}/a.lock`)).toEqual(true); + expect(await fileExists(`${flowDir}/a.inline_script.ts`)).toEqual(false); + expect(await fileExists(`${flowDir}/a.inline_script.lock`)).toEqual(false); + }); + }); + + test("generate-metadata uses non-dotted app inline script filenames", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "non_dotted_app_gen_test", true); + + await createLocalNonDottedApp(tempDir, "my_app"); + + const result = await backend.runCLICommand( + ["generate-metadata", "--yes"], + tempDir, + "non_dotted_app_gen_test" + ); + + expect(result.code).toEqual(0); + + const appDir = `${tempDir}/f/test/my_app__app`; + const appYaml = await readFile(`${appDir}/app.yaml`, "utf-8"); + + // Inline script references should use non-dotted naming + expect(appYaml).not.toContain(".inline_script."); + // Verify no dotted inline script files were created + const { readdir: readdirAsync } = await import("node:fs/promises"); + const files = await readdirAsync(appDir); + const dottedFiles = files.filter((f: string) => f.includes(".inline_script.")); + expect(dottedFiles.length).toEqual(0); + }); + }); + test("--schema-only only processes scripts (skips flows and apps)", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspace(backend, tempDir, "schema_only_test");