From 91690d5a82070e8f6800eafe29b8b782526c8502 Mon Sep 17 00:00:00 2001 From: maoshuorz Date: Sat, 7 Mar 2026 11:57:54 +0800 Subject: [PATCH] fix: update import paths when .flyde files are moved or renamed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a .flyde or .flyde.ts file is moved/renamed in VS Code, all other .flyde files that reference it via source.type "file" now have their relative paths automatically updated to point to the new location. This handles three scenarios: 1. An imported file is moved — all referencing flows are updated 2. A flow file itself is moved — its internal file references are recalculated relative to the new location 3. Both imported and importing files move simultaneously — paths are resolved correctly using the new locations Fixes #112 --- vscode/src/extension.ts | 3 + vscode/src/updateImportsOnFileMove.ts | 227 ++++++++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 vscode/src/updateImportsOnFileMove.ts diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index ea022727..69a7a2a1 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -15,6 +15,7 @@ import { activateReporter, reportEvent, analytics } from "./analytics"; import { showFirstRunPrivacyNotice, showPrivacySettings } from "./privacyNotice"; import { Template, getTemplates, scaffoldTemplate } from "./templateUtils"; +import { registerImportUpdater } from "./updateImportsOnFileMove"; // the application insights key (also known as instrumentation key) @@ -52,6 +53,8 @@ export function activate(context: vscode.ExtensionContext) { } }); + registerImportUpdater(context); + const mainOutputChannel = vscode.window.createOutputChannel("Flyde"); const debugOutputChannel = vscode.window.createOutputChannel("Flyde (Debug)"); diff --git a/vscode/src/updateImportsOnFileMove.ts b/vscode/src/updateImportsOnFileMove.ts new file mode 100644 index 00000000..1058efbe --- /dev/null +++ b/vscode/src/updateImportsOnFileMove.ts @@ -0,0 +1,227 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import * as fs from "fs"; +import * as yaml from "yaml"; + +/** + * When a .flyde or .flyde.ts file is moved/renamed, update all .flyde files + * that reference it via source.type === "file" so their relative paths stay valid. + */ +export function registerImportUpdater( + context: vscode.ExtensionContext +): void { + context.subscriptions.push( + vscode.workspace.onDidRenameFiles(async (event) => { + const relevantRenames = event.files.filter((f) => { + const oldPath = f.oldUri.fsPath; + return oldPath.endsWith(".flyde") || oldPath.endsWith(".flyde.ts"); + }); + + if (relevantRenames.length === 0) { + return; + } + + const flydeFiles = await vscode.workspace.findFiles( + "**/*.flyde", + "**/node_modules/**" + ); + + let totalUpdated = 0; + + for (const flydeFileUri of flydeFiles) { + const flydeFilePath = flydeFileUri.fsPath; + + // Skip files that were themselves moved (they don't need internal updates) + if ( + relevantRenames.some( + (r) => + r.oldUri.fsPath === flydeFilePath || + r.newUri.fsPath === flydeFilePath + ) + ) { + continue; + } + + let raw: string; + try { + raw = fs.readFileSync(flydeFilePath, "utf-8"); + } catch { + continue; + } + + let parsed: any; + try { + parsed = yaml.parse(raw); + } catch { + continue; + } + + if (!parsed?.node?.instances) { + continue; + } + + let changed = false; + + for (const instance of parsed.node.instances) { + if ( + instance.source?.type === "file" && + typeof instance.source.data === "string" + ) { + const oldAbsolute = path.resolve( + path.dirname(flydeFilePath), + instance.source.data + ); + + for (const rename of relevantRenames) { + if (normalizePath(oldAbsolute) === normalizePath(rename.oldUri.fsPath)) { + const newRelative = toRelativePosix( + flydeFilePath, + rename.newUri.fsPath + ); + instance.source.data = newRelative; + changed = true; + } + } + } + + // Also check inline visual nodes recursively + if (instance.source?.type === "inline" && instance.source.data?.instances) { + if (updateInstancesRecursive(instance.source.data.instances, flydeFilePath, relevantRenames)) { + changed = true; + } + } + } + + if (changed) { + const updated = yaml.stringify(parsed, { + aliasDuplicateObjects: false, + }); + fs.writeFileSync(flydeFilePath, updated, "utf-8"); + totalUpdated++; + } + } + + // Also update .flyde files that were themselves moved — their internal + // relative references to OTHER files need recalculating. + for (const rename of relevantRenames) { + if (!rename.newUri.fsPath.endsWith(".flyde")) { + continue; + } + + const flydeFilePath = rename.newUri.fsPath; + const oldDir = path.dirname(rename.oldUri.fsPath); + const newDir = path.dirname(rename.newUri.fsPath); + + // If it only changed name but not directory, internal relative paths are still valid + if (normalizePath(oldDir) === normalizePath(newDir)) { + continue; + } + + let raw: string; + try { + raw = fs.readFileSync(flydeFilePath, "utf-8"); + } catch { + continue; + } + + let parsed: any; + try { + parsed = yaml.parse(raw); + } catch { + continue; + } + + if (!parsed?.node?.instances) { + continue; + } + + let changed = false; + + for (const instance of parsed.node.instances) { + if ( + instance.source?.type === "file" && + typeof instance.source.data === "string" + ) { + // Resolve relative to OLD location + const targetAbsolute = path.resolve( + oldDir, + instance.source.data + ); + + // Check if target still exists at old path (wasn't also moved) + const wasAlsoMoved = relevantRenames.find( + (r) => normalizePath(r.oldUri.fsPath) === normalizePath(targetAbsolute) + ); + + const actualTarget = wasAlsoMoved + ? wasAlsoMoved.newUri.fsPath + : targetAbsolute; + + const newRelative = toRelativePosix(flydeFilePath, actualTarget); + if (newRelative !== instance.source.data) { + instance.source.data = newRelative; + changed = true; + } + } + } + + if (changed) { + const updated = yaml.stringify(parsed, { + aliasDuplicateObjects: false, + }); + fs.writeFileSync(flydeFilePath, updated, "utf-8"); + totalUpdated++; + } + } + + if (totalUpdated > 0) { + vscode.window.showInformationMessage( + `Flyde: Updated import paths in ${totalUpdated} flow file${totalUpdated > 1 ? "s" : ""}.` + ); + } + }) + ); +} + +function updateInstancesRecursive( + instances: any[], + flydeFilePath: string, + renames: ReadonlyArray<{ oldUri: vscode.Uri; newUri: vscode.Uri }> +): boolean { + let changed = false; + for (const instance of instances) { + if ( + instance.source?.type === "file" && + typeof instance.source.data === "string" + ) { + const oldAbsolute = path.resolve( + path.dirname(flydeFilePath), + instance.source.data + ); + for (const rename of renames) { + if (normalizePath(oldAbsolute) === normalizePath(rename.oldUri.fsPath)) { + instance.source.data = toRelativePosix(flydeFilePath, rename.newUri.fsPath); + changed = true; + } + } + } + if (instance.source?.type === "inline" && instance.source.data?.instances) { + if (updateInstancesRecursive(instance.source.data.instances, flydeFilePath, renames)) { + changed = true; + } + } + } + return changed; +} + +function toRelativePosix(from: string, to: string): string { + let rel = path.relative(path.dirname(from), to).replace(/\\/g, "/"); + if (!rel.startsWith(".")) { + rel = "./" + rel; + } + return rel; +} + +function normalizePath(p: string): string { + return path.resolve(p).replace(/\\/g, "/").toLowerCase(); +}