From 485cd07e180caebc4ecb008fcf0a6e2327acbc5a Mon Sep 17 00:00:00 2001 From: nexiumbiz-debug Date: Fri, 8 May 2026 21:20:16 -0400 Subject: [PATCH] Handle moved Flyde import references --- vscode/src/extension.ts | 5 ++ vscode/src/flyde-file-rename-utils.ts | 86 ++++++++++++++++++++++ vscode/src/flyde-file-renames.ts | 65 ++++++++++++++++ vscode/src/test/flyde-file-renames.test.ts | 67 +++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 vscode/src/flyde-file-rename-utils.ts create mode 100644 vscode/src/flyde-file-renames.ts create mode 100644 vscode/src/test/flyde-file-renames.test.ts diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index ea022727..feed0052 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 { registerFlydeFileRenameHandler } from "./flyde-file-renames"; // the application insights key (also known as instrumentation key) @@ -55,6 +56,10 @@ export function activate(context: vscode.ExtensionContext) { const mainOutputChannel = vscode.window.createOutputChannel("Flyde"); const debugOutputChannel = vscode.window.createOutputChannel("Flyde (Debug)"); + context.subscriptions.push( + registerFlydeFileRenameHandler(mainOutputChannel) + ); + let currentTheme = vscode.window.activeColorTheme; let useDarkMode = diff --git a/vscode/src/flyde-file-rename-utils.ts b/vscode/src/flyde-file-rename-utils.ts new file mode 100644 index 00000000..2fcc736d --- /dev/null +++ b/vscode/src/flyde-file-rename-utils.ts @@ -0,0 +1,86 @@ +import * as path from "path"; +import { FlydeFlow, NodeInstance, VisualNode } from "@flyde/core"; + +export type RenamePair = { + oldPath: string; + newPath: string; +}; + +export function rewriteRenamedFileReferences( + flow: FlydeFlow, + flowPath: string, + renamePairs: RenamePair[] +): boolean { + return rewriteNodeReferences(flow.node, path.dirname(flowPath), renamePairs); +} + +function rewriteNodeReferences( + node: VisualNode, + flowDir: string, + renamePairs: RenamePair[] +): boolean { + let changed = false; + + for (const instance of node.instances) { + changed = rewriteInstanceReference(instance, flowDir, renamePairs) || changed; + } + + return changed; +} + +function rewriteInstanceReference( + instance: NodeInstance, + flowDir: string, + renamePairs: RenamePair[] +): boolean { + if (!instance.source) { + return false; + } + + if (instance.source.type === "inline" && instance.source.data) { + return rewriteNodeReferences(instance.source.data, flowDir, renamePairs); + } + + if (instance.source.type !== "file" || typeof instance.source.data !== "string") { + return false; + } + + const existingPath = instance.source.data; + const absoluteReference = path.isAbsolute(existingPath) + ? path.normalize(existingPath) + : path.resolve(flowDir, existingPath); + const renamedReference = findRenamedPath(absoluteReference, renamePairs); + + if (!renamedReference) { + return false; + } + + instance.source.data = path.isAbsolute(existingPath) + ? renamedReference + : normalizeImportPath(path.relative(flowDir, renamedReference)); + + return instance.source.data !== existingPath; +} + +function findRenamedPath( + absoluteReference: string, + renamePairs: RenamePair[] +): string | undefined { + for (const renamePair of renamePairs) { + const oldPath = path.normalize(renamePair.oldPath); + const relativeToOld = path.relative(oldPath, absoluteReference); + + if ( + relativeToOld === "" || + (relativeToOld && + !relativeToOld.startsWith("..") && + !path.isAbsolute(relativeToOld)) + ) { + return path.join(path.normalize(renamePair.newPath), relativeToOld); + } + } +} + +function normalizeImportPath(importPath: string): string { + return importPath.split(path.sep).join("/"); +} diff --git a/vscode/src/flyde-file-renames.ts b/vscode/src/flyde-file-renames.ts new file mode 100644 index 00000000..9e949f59 --- /dev/null +++ b/vscode/src/flyde-file-renames.ts @@ -0,0 +1,65 @@ +import * as vscode from "vscode"; +import { deserializeFlow, serializeFlow } from "@flyde/loader/dist/server"; +import { + RenamePair, + rewriteRenamedFileReferences, +} from "./flyde-file-rename-utils"; + +export { rewriteRenamedFileReferences } from "./flyde-file-rename-utils"; + +export function registerFlydeFileRenameHandler( + outputChannel: vscode.OutputChannel +): vscode.Disposable { + return vscode.workspace.onDidRenameFiles(async (event) => { + const renamePairs = event.files.map((file) => ({ + oldPath: file.oldUri.fsPath, + newPath: file.newUri.fsPath, + })); + + const updatedFiles = await updateFlydeReferencesForRenames(renamePairs); + + if (updatedFiles > 0) { + outputChannel.appendLine( + `Updated Flyde import references in ${updatedFiles} file(s).` + ); + } + }); +} + +export async function updateFlydeReferencesForRenames( + renamePairs: RenamePair[] +): Promise { + if (renamePairs.length === 0) { + return 0; + } + + const flowUris = await vscode.workspace.findFiles( + "**/*.flyde", + "**/node_modules/**" + ); + + let updatedFiles = 0; + + for (const flowUri of flowUris) { + try { + const fileBytes = await vscode.workspace.fs.readFile(flowUri); + const flowText = Buffer.from(fileBytes).toString("utf8"); + const flow = deserializeFlow(flowText, flowUri.fsPath); + + if (rewriteRenamedFileReferences(flow, flowUri.fsPath, renamePairs)) { + await vscode.workspace.fs.writeFile( + flowUri, + Buffer.from(serializeFlow(flow), "utf8") + ); + updatedFiles += 1; + } + } catch (error) { + console.warn( + `Failed to update Flyde imports in ${flowUri.fsPath}`, + error + ); + } + } + + return updatedFiles; +} diff --git a/vscode/src/test/flyde-file-renames.test.ts b/vscode/src/test/flyde-file-renames.test.ts new file mode 100644 index 00000000..dfd5cc01 --- /dev/null +++ b/vscode/src/test/flyde-file-renames.test.ts @@ -0,0 +1,67 @@ +import * as path from "path"; +import assert = require("assert"); +import { FlydeFlow } from "@flyde/core"; +import { rewriteRenamedFileReferences } from "../flyde-file-rename-utils"; + +suite("Flyde file rename references", () => { + test("updates relative imports when an imported flow moves", () => { + const root = path.join(path.sep, "workspace", "project"); + const flowPath = path.join(root, "ParentFlow.flyde"); + const flow = createFlowWithImport("ChildFlow.flyde"); + + const changed = rewriteRenamedFileReferences(flow, flowPath, [ + { + oldPath: path.join(root, "ChildFlow.flyde"), + newPath: path.join(root, "nested", "ChildFlow.flyde"), + }, + ]); + + assert.strictEqual(changed, true); + assert.strictEqual(getImportedPath(flow), "nested/ChildFlow.flyde"); + }); + + test("updates imports when a folder containing imported flows moves", () => { + const root = path.join(path.sep, "workspace", "project"); + const flowPath = path.join(root, "ParentFlow.flyde"); + const flow = createFlowWithImport("nodes/ChildFlow.flyde"); + + const changed = rewriteRenamedFileReferences(flow, flowPath, [ + { + oldPath: path.join(root, "nodes"), + newPath: path.join(root, "shared", "nodes"), + }, + ]); + + assert.strictEqual(changed, true); + assert.strictEqual(getImportedPath(flow), "shared/nodes/ChildFlow.flyde"); + }); +}); + +function createFlowWithImport(importPath: string): FlydeFlow { + return { + node: { + id: "ParentFlow", + inputs: {}, + outputs: {}, + inputsPosition: {}, + outputsPosition: {}, + instances: [ + { + id: "child", + nodeId: "ChildFlow", + type: "visual", + source: { type: "file", data: importPath }, + inputConfig: {}, + pos: { x: 0, y: 0 }, + }, + ], + connections: [], + }, + }; +} + +function getImportedPath(flow: FlydeFlow): string { + const source = flow.node.instances[0].source; + assert(source.type === "file"); + return source.data; +}