mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 16:03:21 +00:00
fix(cli): handle both .flow and __flow suffixes in wmill dev
The flow detection in loadPaths only checked the configured suffix (dotted or non-dotted), so users with nonDottedPaths=true who had .flow folders (or vice versa) would see inline script edits treated as standalone script changes instead of flow changes. Now checks both suffix forms everywhere: type classification, folder path extraction, path stripping, and loadWmPath lookup. Also adds raw_app launch.json generation to init and sync pull. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
d1ee68c376
commit
a4ef96f056
+56
-23
@@ -28,12 +28,9 @@ import { replaceInlineScripts, replaceAllPathScriptsWithLocal } from "../../../w
|
||||
import { extractInlineScripts, extractCurrentMapping } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts";
|
||||
import { parseMetadataFile } from "../../utils/metadata.ts";
|
||||
import {
|
||||
getFolderSuffix,
|
||||
getFolderSuffixWithSep,
|
||||
getMetadataFileName,
|
||||
extractFolderPath,
|
||||
getNonDottedPaths,
|
||||
hasFolderSuffix,
|
||||
loadNonDottedPathsSetting,
|
||||
} from "../../utils/resource_folders.ts";
|
||||
import * as path from "node:path";
|
||||
@@ -122,7 +119,7 @@ export async function dev(opts: GlobalOptions & SyncOptions & DevOpts) {
|
||||
// Need to init nonDottedPaths before checking suffix
|
||||
await loadNonDottedPathsSetting();
|
||||
|
||||
if (hasFolderSuffix(cwdBasename, "flow")) {
|
||||
if (cwdBasename.endsWith(".flow") || cwdBasename.endsWith("__flow")) {
|
||||
GLOBAL_CONFIG_OPT.noCdToRoot = true;
|
||||
|
||||
// Find workspace root
|
||||
@@ -141,10 +138,14 @@ export async function dev(opts: GlobalOptions & SyncOptions & DevOpts) {
|
||||
|
||||
if (workspaceRoot) {
|
||||
const relPath = path.relative(workspaceRoot, cwd).replaceAll("\\", "/");
|
||||
const flowSuffix = getFolderSuffix("flow");
|
||||
opts.path = relPath.endsWith(flowSuffix)
|
||||
? relPath.slice(0, -flowSuffix.length)
|
||||
: relPath;
|
||||
// Strip whichever flow suffix is actually present (dotted or non-dotted)
|
||||
if (relPath.endsWith(".flow")) {
|
||||
opts.path = relPath.slice(0, -".flow".length);
|
||||
} else if (relPath.endsWith("__flow")) {
|
||||
opts.path = relPath.slice(0, -"__flow".length);
|
||||
} else {
|
||||
opts.path = relPath;
|
||||
}
|
||||
opts.proxyPort = opts.proxyPort ?? 3100;
|
||||
log.info(`Detected flow folder, path: ${opts.path}`);
|
||||
process.chdir(workspaceRoot);
|
||||
@@ -189,12 +190,13 @@ export async function dev(opts: GlobalOptions & SyncOptions & DevOpts) {
|
||||
});
|
||||
}
|
||||
|
||||
const flowFolderSuffix = getFolderSuffixWithSep("flow");
|
||||
const flowMetadataFile = getMetadataFileName("flow", "yaml");
|
||||
async function loadPaths(pathsToLoad: string[]) {
|
||||
const paths = pathsToLoad.filter((path) =>
|
||||
exts.some(
|
||||
(ext) => path.endsWith(ext) || path.endsWith(flowFolderSuffix + flowMetadataFile)
|
||||
(ext) => path.endsWith(ext)
|
||||
|| path.endsWith(".flow/" + flowMetadataFile)
|
||||
|| path.endsWith("__flow/" + flowMetadataFile)
|
||||
)
|
||||
);
|
||||
if (paths.length == 0) {
|
||||
@@ -203,10 +205,27 @@ export async function dev(opts: GlobalOptions & SyncOptions & DevOpts) {
|
||||
const nativePath = (await realpath(paths[0])).replace(base + SEP, "");
|
||||
const cpath = nativePath.replaceAll("\\", "/");
|
||||
if (!ignore(nativePath, false)) {
|
||||
const typ = getTypeStrFromPath(cpath);
|
||||
let typ = getTypeStrFromPath(cpath);
|
||||
// If a script file is inside a flow folder, treat it as a flow change
|
||||
// (handles both .flow/ and __flow/ regardless of nonDottedPaths setting)
|
||||
if (typ === "script" && (cpath.includes(".flow/") || cpath.includes("__flow/"))) {
|
||||
typ = "flow";
|
||||
}
|
||||
log.info("Detected change in " + cpath + " (" + typ + ")");
|
||||
if (typ == "flow") {
|
||||
const localPath = extractFolderPath(cpath, "flow")!;
|
||||
// Try extractFolderPath, fallback to manual extraction for mixed suffix cases
|
||||
let localPath = extractFolderPath(cpath, "flow");
|
||||
if (!localPath) {
|
||||
// extractFolderPath only checks the configured suffix; try both manually
|
||||
for (const suffix of [".flow/", "__flow/"]) {
|
||||
const idx = cpath.indexOf(suffix);
|
||||
if (idx !== -1) {
|
||||
localPath = cpath.substring(0, idx) + suffix;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!localPath) return;
|
||||
const localFlow = (await yamlParseFile(
|
||||
localPath + "flow.yaml"
|
||||
)) as FlowFile;
|
||||
@@ -227,10 +246,13 @@ export async function dev(opts: GlobalOptions & SyncOptions & DevOpts) {
|
||||
});
|
||||
await replaceAllPathScriptsWithLocal(localFlow.value, localScriptReader, log);
|
||||
tagReplacedPathScripts(localFlow);
|
||||
const flowSuffix = getFolderSuffix("flow");
|
||||
const wmFlowPath = localPath.endsWith(flowSuffix + "/")
|
||||
? localPath.slice(0, -(flowSuffix.length + 1))
|
||||
: localPath.replace(/\/$/, "");
|
||||
// Strip whichever flow suffix is present (dotted or non-dotted)
|
||||
let wmFlowPath = localPath.replace(/\/$/, "");
|
||||
if (wmFlowPath.endsWith(".flow")) {
|
||||
wmFlowPath = wmFlowPath.slice(0, -".flow".length);
|
||||
} else if (wmFlowPath.endsWith("__flow")) {
|
||||
wmFlowPath = wmFlowPath.slice(0, -"__flow".length);
|
||||
}
|
||||
currentLastEdit = {
|
||||
type: "flow",
|
||||
flow: localFlow,
|
||||
@@ -284,10 +306,12 @@ export async function dev(opts: GlobalOptions & SyncOptions & DevOpts) {
|
||||
|
||||
// Normalize a windmill path by stripping any trailing flow/app suffix
|
||||
function normalizeWmPath(p: string): string {
|
||||
const flowSuffix = getFolderSuffix("flow");
|
||||
let result = p.replace(/\/$/, "");
|
||||
if (result.endsWith(flowSuffix)) {
|
||||
result = result.slice(0, -flowSuffix.length);
|
||||
// Strip whichever flow suffix is present (dotted or non-dotted)
|
||||
if (result.endsWith(".flow")) {
|
||||
result = result.slice(0, -".flow".length);
|
||||
} else if (result.endsWith("__flow")) {
|
||||
result = result.slice(0, -"__flow".length);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -295,11 +319,20 @@ export async function dev(opts: GlobalOptions & SyncOptions & DevOpts) {
|
||||
// Load a resource by its windmill path (e.g., "u/admin/my_script" or "f/my_flow")
|
||||
async function loadWmPath(wmPath: string): Promise<LastEditScript | LastEditFlow | undefined> {
|
||||
wmPath = normalizeWmPath(wmPath);
|
||||
// Try as flow
|
||||
const flowDir = wmPath + getFolderSuffix("flow") + "/";
|
||||
const flowYaml = flowDir + "flow.yaml";
|
||||
// Try as flow — check both dotted and non-dotted suffixes
|
||||
let flowDir: string | undefined;
|
||||
let flowYaml: string | undefined;
|
||||
for (const suffix of [".flow", "__flow"]) {
|
||||
const candidate = wmPath + suffix + "/";
|
||||
try {
|
||||
await access(candidate + "flow.yaml");
|
||||
flowDir = candidate;
|
||||
flowYaml = candidate + "flow.yaml";
|
||||
break;
|
||||
} catch {}
|
||||
}
|
||||
try {
|
||||
await access(flowYaml);
|
||||
if (!flowDir || !flowYaml) throw new Error("not a flow");
|
||||
const localFlow = (await yamlParseFile(flowYaml)) as FlowFile;
|
||||
await replaceInlineScripts(
|
||||
localFlow.value.modules,
|
||||
|
||||
@@ -399,6 +399,49 @@ async function initAction(opts: InitOptions) {
|
||||
);
|
||||
}
|
||||
|
||||
// Generate .claude/launch.json for each raw_app folder
|
||||
try {
|
||||
const rawAppSuffix = nonDottedPaths ? "__raw_app" : ".raw_app";
|
||||
const appLaunchJson = JSON.stringify({
|
||||
version: "0.0.1",
|
||||
configurations: [{
|
||||
name: "windmill",
|
||||
runtimeExecutable: "bash",
|
||||
runtimeArgs: ["-c", "wmill app dev --no-open --port ${PORT:-4000}"],
|
||||
port: 4000,
|
||||
autoPort: true,
|
||||
}],
|
||||
}, null, 2) + "\n";
|
||||
|
||||
let appCount = 0;
|
||||
async function scanForApps(dir: string) {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.name.endsWith(rawAppSuffix)) {
|
||||
const claudeDir = join(fullPath, ".claude");
|
||||
const launchPath = join(claudeDir, "launch.json");
|
||||
mkdirSync(claudeDir, { recursive: true });
|
||||
writeFileSync(launchPath, appLaunchJson, "utf-8");
|
||||
appCount++;
|
||||
} else {
|
||||
await scanForApps(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await scanForApps(".");
|
||||
if (appCount > 0) {
|
||||
log.info(colors.green(`Created .claude/launch.json for ${appCount} raw app folder(s)`));
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
`Could not scan for raw app folders: ${error instanceof Error ? error.message : error}`
|
||||
);
|
||||
}
|
||||
|
||||
// Generate resource type namespace
|
||||
try {
|
||||
await generateRTNamespace(opts as GlobalOptions);
|
||||
|
||||
@@ -2346,6 +2346,46 @@ export async function pull(
|
||||
log.warn(`Could not scan for flow folders: ${error instanceof Error ? error.message : error}`);
|
||||
}
|
||||
|
||||
// Generate .claude/launch.json for all raw_app folders
|
||||
try {
|
||||
const rawAppSuffix = getFolderSuffix("raw_app");
|
||||
const appLaunchJson = JSON.stringify({
|
||||
version: "0.0.1",
|
||||
configurations: [{
|
||||
name: "windmill",
|
||||
runtimeExecutable: "bash",
|
||||
runtimeArgs: ["-c", "wmill app dev --no-open --port ${PORT:-4000}"],
|
||||
port: 4000,
|
||||
autoPort: true,
|
||||
}],
|
||||
}, null, 2) + "\n";
|
||||
|
||||
let appLaunchCount = 0;
|
||||
async function scanForApps(dir: string) {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.name.endsWith(rawAppSuffix)) {
|
||||
const claudeDir = path.join(fullPath, ".claude");
|
||||
mkdirSync(claudeDir, { recursive: true });
|
||||
writeFileSync(path.join(claudeDir, "launch.json"), appLaunchJson, "utf-8");
|
||||
appLaunchCount++;
|
||||
} else {
|
||||
await scanForApps(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await scanForApps(".");
|
||||
if (appLaunchCount > 0) {
|
||||
log.info(colors.green(`Created .claude/launch.json for ${appLaunchCount} raw app folder(s)`));
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn(`Could not scan for raw app folders: ${error instanceof Error ? error.message : error}`);
|
||||
}
|
||||
|
||||
if (tracker.apps.length > 0) {
|
||||
log.info(
|
||||
colors.gray(
|
||||
|
||||
Reference in New Issue
Block a user