From 0820acb8985c0da7e0d9635f8f0c3a0cdbeff946 Mon Sep 17 00:00:00 2001 From: Shivam Shukla Date: Sat, 26 Sep 2026 02:21:17 +0530 Subject: [PATCH] Update Flyde imports when referenced files move --- vscode/src/extension.ts | 3 + .../src/test/updateMovedFlydeImports.test.ts | 37 +++++ vscode/src/updateMovedFlydeImports.ts | 131 ++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 vscode/src/test/updateMovedFlydeImports.test.ts create mode 100644 vscode/src/updateMovedFlydeImports.ts diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index ea022727..ec55df67 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 { registerMovedFlydeImportUpdater } from "./updateMovedFlydeImports"; // the application insights key (also known as instrumentation key) @@ -55,6 +56,8 @@ export function activate(context: vscode.ExtensionContext) { const mainOutputChannel = vscode.window.createOutputChannel("Flyde"); const debugOutputChannel = vscode.window.createOutputChannel("Flyde (Debug)"); + registerMovedFlydeImportUpdater(context, debugOutputChannel); + let currentTheme = vscode.window.activeColorTheme; let useDarkMode = diff --git a/vscode/src/test/updateMovedFlydeImports.test.ts b/vscode/src/test/updateMovedFlydeImports.test.ts new file mode 100644 index 00000000..f33d5191 --- /dev/null +++ b/vscode/src/test/updateMovedFlydeImports.test.ts @@ -0,0 +1,37 @@ +import * as assert from "assert"; +import * as path from "path"; +import { updateMovedFlydeImportsInText } from "../updateMovedFlydeImports"; + +suite("updateMovedFlydeImportsInText", () => { + test("updates local visual node imports when a referenced .flyde file is moved", () => { + const flowPath = path.join("/workspace", "flows", "main.flyde"); + const original = `imports:\n "./nodes/add-one.flyde": AddOne\n "@flyde/nodes": Add\nnode:\n id: Main\n inputs: {}\n outputs: {}\n instances: []\n connections: []\n inputsPosition: {}\n outputsPosition: {}\n`; + + const updated = updateMovedFlydeImportsInText(original, flowPath, [ + { + oldPath: path.join("/workspace", "flows", "nodes", "add-one.flyde"), + newPath: path.join("/workspace", "shared", "add-one.flyde"), + }, + ]); + + assert.ok(updated); + assert.match(updated, /\.\.\/shared\/add-one\.flyde/); + assert.doesNotMatch(updated, /\.\/nodes\/add-one\.flyde/); + assert.match(updated, /@flyde\/nodes/); + }); + + test("returns undefined when no local imports match the moved file", () => { + const flowPath = path.join("/workspace", "flows", "main.flyde"); + const original = `imports:\n "./nodes/add-one.flyde": AddOne\nnode:\n id: Main\n inputs: {}\n outputs: {}\n instances: []\n connections: []\n inputsPosition: {}\n outputsPosition: {}\n`; + + const updated = updateMovedFlydeImportsInText(original, flowPath, [ + { + oldPath: path.join("/workspace", "other", "add-one.flyde"), + newPath: path.join("/workspace", "shared", "add-one.flyde"), + }, + ]); + + assert.equal(updated, undefined); + }); +}); + diff --git a/vscode/src/updateMovedFlydeImports.ts b/vscode/src/updateMovedFlydeImports.ts new file mode 100644 index 00000000..a733d3f4 --- /dev/null +++ b/vscode/src/updateMovedFlydeImports.ts @@ -0,0 +1,131 @@ +import * as path from "path"; +import * as vscode from "vscode"; +import { deserializeFlow, serializeFlow } from "@flyde/loader/dist/server"; + +function normalizeFsPath(filePath: string): string { + return path.resolve(filePath); +} + +function toPosixPath(filePath: string): string { + return filePath.split(path.sep).join(path.posix.sep); +} + +function relativeImportPath(fromFilePath: string, toFilePath: string): string { + const relative = toPosixPath( + path.relative(path.dirname(fromFilePath), toFilePath) + ); + + if (relative.startsWith(".")) { + return relative; + } + + return `./${relative}`; +} + +function importPathMatchesFile( + flowPath: string, + importPath: string, + movedFromPath: string +): boolean { + if (!importPath.startsWith(".")) { + return false; + } + + const resolvedImportPath = normalizeFsPath( + path.resolve(path.dirname(flowPath), importPath) + ); + + return resolvedImportPath === normalizeFsPath(movedFromPath); +} + +export function updateMovedFlydeImportsInText( + flowText: string, + flowPath: string, + renamedFiles: Array<{ oldPath: string; newPath: string }> +): string | undefined { + const flow = deserializeFlow(flowText, flowPath); + const imports = flow.imports ?? {}; + let changed = false; + + for (const [oldImportPath, importedNodeIds] of Object.entries(imports)) { + const movedFile = renamedFiles.find(({ oldPath }) => + importPathMatchesFile(flowPath, oldImportPath, oldPath) + ); + + if (!movedFile) { + continue; + } + + const newImportPath = relativeImportPath(flowPath, movedFile.newPath); + delete imports[oldImportPath]; + imports[newImportPath] = importedNodeIds; + changed = true; + } + + if (!changed) { + return undefined; + } + + flow.imports = imports; + return serializeFlow(flow); +} + +export function registerMovedFlydeImportUpdater( + context: vscode.ExtensionContext, + outputChannel?: vscode.OutputChannel +) { + context.subscriptions.push( + vscode.workspace.onDidRenameFiles(async ({ files }) => { + const renamedFlydeFiles = files + .filter( + ({ oldUri, newUri }) => + oldUri.scheme === "file" && + newUri.scheme === "file" && + oldUri.fsPath.endsWith(".flyde") && + newUri.fsPath.endsWith(".flyde") + ) + .map(({ oldUri, newUri }) => ({ + oldPath: oldUri.fsPath, + newPath: newUri.fsPath, + })); + + if (renamedFlydeFiles.length === 0) { + return; + } + + const flydeFiles = await vscode.workspace.findFiles( + "**/*.flyde", + "**/node_modules/**" + ); + + for (const file of flydeFiles) { + try { + const document = await vscode.workspace.openTextDocument(file); + const updatedText = updateMovedFlydeImportsInText( + document.getText(), + file.fsPath, + renamedFlydeFiles + ); + + if (updatedText === undefined) { + continue; + } + + const edit = new vscode.WorkspaceEdit(); + edit.replace( + file, + new vscode.Range(0, 0, document.lineCount, 0), + updatedText + ); + await vscode.workspace.applyEdit(edit); + await document.save(); + } catch (error) { + outputChannel?.appendLine( + `Failed updating Flyde imports in ${file.fsPath}: ${error}` + ); + } + } + }) + ); +} +