diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index a61cb2d60f..f139fc683e 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -91,6 +91,7 @@ import { isRawAppFolderMetadataFile, getDeleteSuffix, transformJsonPathToDir, + getFolderSuffix, getFolderSuffixWithSep, } from "../../utils/resource_folders.ts"; @@ -1289,7 +1290,7 @@ export async function elementsToMap( } if (isRawAppFile(path)) { - const suffix = path.split(".raw_app" + SEP).pop(); + const suffix = path.split(getFolderSuffix("raw_app") + SEP).pop(); if ( suffix?.startsWith("dist/") || suffix == "wmill.d.ts" || diff --git a/cli/src/utils/resource_folders.ts b/cli/src/utils/resource_folders.ts index c374ad196d..8f12da2af0 100644 --- a/cli/src/utils/resource_folders.ts +++ b/cli/src/utils/resource_folders.ts @@ -8,7 +8,7 @@ * (.flow, .app, .raw_app) or dunder-prefixed names (__flow, __app, __raw_app). */ -import { SEP } from "../../deps.ts"; +import { log, SEP } from "../../deps.ts"; // Resource types that use folder-based storage export type FolderResourceType = "flow" | "app" | "raw_app"; @@ -28,7 +28,9 @@ const NON_DOTTED_SUFFIXES = { raw_app: "__raw_app", } as const; -export type FolderSuffixes = typeof DOTTED_SUFFIXES | typeof NON_DOTTED_SUFFIXES; +export type FolderSuffixes = + | typeof DOTTED_SUFFIXES + | typeof NON_DOTTED_SUFFIXES; // Global state for nonDottedPaths configuration let _nonDottedPaths = false; @@ -39,6 +41,9 @@ let _nonDottedPaths = false; * This should be called once at startup based on wmill.yaml configuration. */ export function setNonDottedPaths(value: boolean): void { + if (value) { + log.info("Using non-dotted paths (__flow, __app, __raw_app)"); + } _nonDottedPaths = value; } @@ -148,7 +153,7 @@ export function isRawAppBackendPath(filePath: string): boolean { // Normalize path separators for consistent matching const normalizedPath = filePath.replaceAll(SEP, "/"); // Check if path contains pattern: *.[suffix]/backend/ - const escapedSuffix = suffixes.raw_app.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const escapedSuffix = suffixes.raw_app.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const pattern = new RegExp(`${escapedSuffix}/backend/`); return pattern.test(normalizedPath); } @@ -205,7 +210,12 @@ export function buildMetadataPath( type: FolderResourceType, format: "yaml" | "json" ): string { - return resourceName + getFolderSuffixes()[type] + SEP + METADATA_FILES[type][format]; + return ( + resourceName + + getFolderSuffixes()[type] + + SEP + + METADATA_FILES[type][format] + ); } // ============================================================================ @@ -258,27 +268,71 @@ export function extractNameFromFolder( // ============================================================================ /** - * Check if a path ends with a flow metadata file suffix + * Check if a path ends with a flow metadata file suffix. + * Detects BOTH API format (always dotted: .flow.json) and local format (user-configured). + * This is necessary because the API always returns dotted format, but local files + * may use non-dotted format if nonDottedPaths is configured. */ export function isFlowMetadataFile(p: string): boolean { - const suffixes = getFolderSuffixes(); - return p.endsWith(suffixes.flow + ".json") || p.endsWith(suffixes.flow + ".yaml"); + // Always check API format (dotted) + if ( + p.endsWith(DOTTED_SUFFIXES.flow + ".json") || + p.endsWith(DOTTED_SUFFIXES.flow + ".yaml") + ) { + return true; + } + // Also check non-dotted format for local files + if (_nonDottedPaths) { + return ( + p.endsWith(NON_DOTTED_SUFFIXES.flow + ".json") || + p.endsWith(NON_DOTTED_SUFFIXES.flow + ".yaml") + ); + } + return false; } /** - * Check if a path ends with an app metadata file suffix + * Check if a path ends with an app metadata file suffix. + * Detects BOTH API format (always dotted: .app.json) and local format (user-configured). */ export function isAppMetadataFile(p: string): boolean { - const suffixes = getFolderSuffixes(); - return p.endsWith(suffixes.app + ".json") || p.endsWith(suffixes.app + ".yaml"); + // Always check API format (dotted) + if ( + p.endsWith(DOTTED_SUFFIXES.app + ".json") || + p.endsWith(DOTTED_SUFFIXES.app + ".yaml") + ) { + return true; + } + // Also check non-dotted format for local files + if (_nonDottedPaths) { + return ( + p.endsWith(NON_DOTTED_SUFFIXES.app + ".json") || + p.endsWith(NON_DOTTED_SUFFIXES.app + ".yaml") + ); + } + return false; } /** - * Check if a path ends with a raw_app metadata file suffix + * Check if a path ends with a raw_app metadata file suffix. + * Detects BOTH API format (always dotted: .raw_app.json) and local format (user-configured). */ export function isRawAppMetadataFile(p: string): boolean { - const suffixes = getFolderSuffixes(); - return p.endsWith(suffixes.raw_app + ".json") || p.endsWith(suffixes.raw_app + ".yaml"); + // Always check API format (dotted) + if ( + p.endsWith(DOTTED_SUFFIXES.raw_app + ".json") || + p.endsWith(DOTTED_SUFFIXES.raw_app + ".yaml") + ) { + return true; + } + // Also check non-dotted format for local files + if (_nonDottedPaths) { + return ( + p.endsWith(NON_DOTTED_SUFFIXES.raw_app + ".json") || + p.endsWith(NON_DOTTED_SUFFIXES.raw_app + ".yaml") + ); + } + return false; } /** @@ -308,16 +362,26 @@ export function getDeleteSuffix( } /** - * Transform a JSON path to the appropriate directory/file path for sync - * e.g., "f/my_flow.flow.json" -> "f/my_flow.flow" or "f/my_flow__flow.json" -> "f/my_flow__flow" + * Transform a JSON path from API format to local directory path for sync. + * The API always returns dotted format (.flow.json, .app.json, .raw_app.json). + * This function transforms to the user's configured format (dotted or non-dotted). + * e.g., with nonDottedPaths=true: "f/my_flow.flow.json" -> "f/my_flow__flow" + * e.g., with nonDottedPaths=false: "f/my_flow.flow.json" -> "f/my_flow.flow" */ export function transformJsonPathToDir( p: string, type: FolderResourceType ): string { - const suffixes = getFolderSuffixes(); - const fullSuffix = suffixes[type] + ".json"; - if (p.endsWith(fullSuffix)) { + // API always returns dotted format + const apiSuffix = DOTTED_SUFFIXES[type] + ".json"; + if (p.endsWith(apiSuffix)) { + // Remove the API suffix and add user's configured suffix + const basePath = p.substring(0, p.length - apiSuffix.length); + return basePath + getFolderSuffixes()[type]; + } + // Also handle the case where path already has user's configured format + const userSuffix = getFolderSuffixes()[type] + ".json"; + if (p.endsWith(userSuffix)) { return p.substring(0, p.length - 5); // Remove ".json" } // Path doesn't match expected suffix pattern, return unchanged diff --git a/cli/src/utils/utils.ts b/cli/src/utils/utils.ts index 0059fd50c2..b4fa85ba64 100644 --- a/cli/src/utils/utils.ts +++ b/cli/src/utils/utils.ts @@ -2,7 +2,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck This file is copied from a JS project, so it's not type-safe. -import { colors, encodeHex, log } from "../../deps.ts"; +import { colors, encodeHex, log, SEP } from "../../deps.ts"; import crypto from "node:crypto"; import { fetchVersion } from "../core/context.ts"; import { updateGlobalVersions } from "../commands/sync/global.ts"; diff --git a/cli/test/sync_pull_push.test.ts b/cli/test/sync_pull_push.test.ts index 2443f33e6d..e8409db8c7 100644 --- a/cli/test/sync_pull_push.test.ts +++ b/cli/test/sync_pull_push.test.ts @@ -22,6 +22,10 @@ import { hasFolderSuffix, setNonDottedPaths, getNonDottedPaths, + isFlowMetadataFile, + isAppMetadataFile, + isRawAppMetadataFile, + transformJsonPathToDir, } from "../src/utils/resource_folders.ts"; // ============================================================================= @@ -481,6 +485,123 @@ Deno.test("buildFolderPath creates correct paths", () => { assertEquals(buildFolderPath("u/admin/raw_app", "raw_app"), "u/admin/raw_app.raw_app"); }); +// ============================================================================= +// nonDottedPaths Tests - API format detection and transformation +// ============================================================================= + +Deno.test("Metadata file detection works with dotted format (default)", () => { + // Ensure we're in default mode + setNonDottedPaths(false); + + // API always returns dotted format + assert(isFlowMetadataFile("f/my_flow.flow.json"), "Should detect .flow.json"); + assert(isFlowMetadataFile("f/my_flow.flow.yaml"), "Should detect .flow.yaml"); + assert(isAppMetadataFile("f/my_app.app.json"), "Should detect .app.json"); + assert(isAppMetadataFile("f/my_app.app.yaml"), "Should detect .app.yaml"); + assert(isRawAppMetadataFile("f/my_raw.raw_app.json"), "Should detect .raw_app.json"); + assert(isRawAppMetadataFile("f/my_raw.raw_app.yaml"), "Should detect .raw_app.yaml"); + + // Non-matching should return false + assert(!isFlowMetadataFile("f/my_script.ts"), "Should not detect script file"); + assert(!isAppMetadataFile("f/my_script.ts"), "Should not detect script file"); +}); + +Deno.test("Metadata file detection works with nonDottedPaths=true", () => { + // Store original value + const wasNonDotted = getNonDottedPaths(); + + try { + setNonDottedPaths(true); + + // API format (dotted) should still be detected + assert(isFlowMetadataFile("f/my_flow.flow.json"), "Should detect API format .flow.json"); + assert(isAppMetadataFile("f/my_app.app.json"), "Should detect API format .app.json"); + assert(isRawAppMetadataFile("f/my_raw.raw_app.json"), "Should detect API format .raw_app.json"); + + // Local format (non-dotted) should also be detected + assert(isFlowMetadataFile("f/my_flow__flow.json"), "Should detect local format __flow.json"); + assert(isFlowMetadataFile("f/my_flow__flow.yaml"), "Should detect local format __flow.yaml"); + assert(isAppMetadataFile("f/my_app__app.json"), "Should detect local format __app.json"); + assert(isRawAppMetadataFile("f/my_raw__raw_app.json"), "Should detect local format __raw_app.json"); + } finally { + // Restore original value + setNonDottedPaths(wasNonDotted); + } +}); + +Deno.test("transformJsonPathToDir transforms API format to local format", () => { + // Store original value + const wasNonDotted = getNonDottedPaths(); + + try { + // Test with dotted paths (default) + setNonDottedPaths(false); + assertEquals( + transformJsonPathToDir("f/my_flow.flow.json", "flow"), + "f/my_flow.flow", + "Should transform dotted API format to dotted local format" + ); + assertEquals( + transformJsonPathToDir("f/my_app.app.json", "app"), + "f/my_app.app", + "Should transform app correctly" + ); + assertEquals( + transformJsonPathToDir("f/my_raw.raw_app.json", "raw_app"), + "f/my_raw.raw_app", + "Should transform raw_app correctly" + ); + + // Test with non-dotted paths + setNonDottedPaths(true); + assertEquals( + transformJsonPathToDir("f/my_flow.flow.json", "flow"), + "f/my_flow__flow", + "Should transform dotted API format to non-dotted local format" + ); + assertEquals( + transformJsonPathToDir("f/my_app.app.json", "app"), + "f/my_app__app", + "Should transform app to non-dotted format" + ); + assertEquals( + transformJsonPathToDir("f/my_raw.raw_app.json", "raw_app"), + "f/my_raw__raw_app", + "Should transform raw_app to non-dotted format" + ); + + // Non-matching paths should be returned unchanged + assertEquals( + transformJsonPathToDir("f/my_script.ts", "flow"), + "f/my_script.ts", + "Should return non-matching path unchanged" + ); + } finally { + // Restore original value + setNonDottedPaths(wasNonDotted); + } +}); + +Deno.test("getFolderSuffix returns correct suffix based on nonDottedPaths setting", () => { + // Store original value + const wasNonDotted = getNonDottedPaths(); + + try { + setNonDottedPaths(false); + assertEquals(getFolderSuffix("flow"), ".flow"); + assertEquals(getFolderSuffix("app"), ".app"); + assertEquals(getFolderSuffix("raw_app"), ".raw_app"); + + setNonDottedPaths(true); + assertEquals(getFolderSuffix("flow"), "__flow"); + assertEquals(getFolderSuffix("app"), "__app"); + assertEquals(getFolderSuffix("raw_app"), "__raw_app"); + } finally { + // Restore original value + setNonDottedPaths(wasNonDotted); + } +}); + Deno.test("Script fixture creates valid structure", () => { const pythonScript = createScriptFixture("test_script", "python3");