From 69f4bb9643a1ae84fa025b39b43ad7062a6f5e2e Mon Sep 17 00:00:00 2001 From: Catnap7 <36249828+Catnap7@users.noreply.github.com> Date: Sat, 23 May 2026 03:34:26 +0900 Subject: [PATCH 1/2] Fix moved Flyde flow imports --- vscode/src/extension.ts | 40 ++++++ vscode/src/update-flyde-import-paths.test.ts | 122 +++++++++++++++++++ vscode/src/update-flyde-import-paths.ts | 105 ++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 vscode/src/update-flyde-import-paths.test.ts create mode 100644 vscode/src/update-flyde-import-paths.ts diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index ea022727..aa97acdc 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -10,11 +10,13 @@ var fp = require("find-free-port"); import { createEmbeddedServer } from "./embedded-server"; import { join } from "path"; +import { deserializeFlow, serializeFlow } from "@flyde/loader/dist/server"; import { activateReporter, reportEvent, analytics } from "./analytics"; import { showFirstRunPrivacyNotice, showPrivacySettings } from "./privacyNotice"; import { Template, getTemplates, scaffoldTemplate } from "./templateUtils"; +import { updateMovedFlydeImportPaths } from "./update-flyde-import-paths"; // the application insights key (also known as instrumentation key) @@ -55,6 +57,44 @@ export function activate(context: vscode.ExtensionContext) { const mainOutputChannel = vscode.window.createOutputChannel("Flyde"); const debugOutputChannel = vscode.window.createOutputChannel("Flyde (Debug)"); + const rewriteMovedFlydeImports = async (event: vscode.FileRenameEvent) => { + const moves = event.files.map((file) => ({ + oldPath: file.oldUri.fsPath, + newPath: file.newUri.fsPath, + })); + + const flowUris = await vscode.workspace.findFiles( + "**/*.flyde", + "**/node_modules/**" + ); + + for (const flowUri of flowUris) { + try { + const contents = Buffer.from(await fs.readFile(flowUri)).toString( + "utf8" + ); + const flow = deserializeFlow(contents, flowUri.fsPath); + + if (!updateMovedFlydeImportPaths(flow, flowUri.fsPath, moves)) { + continue; + } + + await fs.writeFile(flowUri, Buffer.from(serializeFlow(flow), "utf8")); + mainOutputChannel.appendLine( + `Updated moved Flyde imports in ${flowUri.fsPath}` + ); + } catch (error) { + mainOutputChannel.appendLine( + `Failed to update moved Flyde imports in ${flowUri.fsPath}: ${error}` + ); + } + } + }; + + context.subscriptions.push( + vscode.workspace.onDidRenameFiles(rewriteMovedFlydeImports) + ); + let currentTheme = vscode.window.activeColorTheme; let useDarkMode = diff --git a/vscode/src/update-flyde-import-paths.test.ts b/vscode/src/update-flyde-import-paths.test.ts new file mode 100644 index 00000000..1723284a --- /dev/null +++ b/vscode/src/update-flyde-import-paths.test.ts @@ -0,0 +1,122 @@ +import * as assert from "assert"; +import * as path from "path"; +import { FlydeFlow } from "@flyde/core"; +import { updateMovedFlydeImportPaths } from "./update-flyde-import-paths"; + +const makeFlow = (sourcePath: string): FlydeFlow => ({ + imports: {}, + node: { + id: "ParentFlow", + inputs: {}, + outputs: {}, + inputsPosition: {}, + outputsPosition: {}, + instances: [ + { + id: "child", + pos: { x: 0, y: 0 }, + inputConfig: {}, + nodeId: "ChildFlow", + type: "visual", + source: { + type: "file", + data: sourcePath, + }, + }, + ], + connections: [], + }, +}); + +suite("updateMovedFlydeImportPaths", () => { + test("rewrites file source paths when an imported flow is moved", () => { + const root = path.resolve("workspace"); + const flowPath = path.join(root, "flows", "Parent.flyde"); + const flow = makeFlow("./Child.flyde"); + + const changed = updateMovedFlydeImportPaths(flow, flowPath, [ + { + oldPath: path.join(root, "flows", "Child.flyde"), + newPath: path.join(root, "shared", "Child.flyde"), + }, + ]); + + assert.strictEqual(changed, true); + assert.strictEqual( + (flow.node.instances[0].source as { data: string }).data, + "../shared/Child.flyde" + ); + }); + + test("rewrites imports when a containing folder is moved", () => { + const root = path.resolve("workspace"); + const flowPath = path.join(root, "flows", "Parent.flyde"); + const flow = makeFlow("./nested/Child.flyde"); + + flow.imports = { + "./nested/Child.flyde": ["ChildFlow"], + }; + + const changed = updateMovedFlydeImportPaths(flow, flowPath, [ + { + oldPath: path.join(root, "flows", "nested"), + newPath: path.join(root, "shared", "nested"), + }, + ]); + + assert.strictEqual(changed, true); + assert.strictEqual( + (flow.node.instances[0].source as { data: string }).data, + "../shared/nested/Child.flyde" + ); + assert.deepStrictEqual(flow.imports, { + "../shared/nested/Child.flyde": ["ChildFlow"], + }); + }); + + test("rewrites file source paths when the parent flow moves", () => { + const root = path.resolve("workspace"); + const flow = makeFlow("./Child.flyde"); + + const changed = updateMovedFlydeImportPaths( + flow, + path.join(root, "archive", "Parent.flyde"), + [ + { + oldPath: path.join(root, "flows", "Parent.flyde"), + newPath: path.join(root, "archive", "Parent.flyde"), + }, + ] + ); + + assert.strictEqual(changed, true); + assert.strictEqual( + (flow.node.instances[0].source as { data: string }).data, + "../flows/Child.flyde" + ); + }); + + test("leaves package and custom sources unchanged", () => { + const root = path.resolve("workspace"); + const flowPath = path.join(root, "flows", "Parent.flyde"); + const flow = makeFlow("@flyde/nodes"); + + flow.node.instances[0].source = { + type: "package", + data: "@flyde/nodes", + }; + + const changed = updateMovedFlydeImportPaths(flow, flowPath, [ + { + oldPath: path.join(root, "flows", "Child.flyde"), + newPath: path.join(root, "shared", "Child.flyde"), + }, + ]); + + assert.strictEqual(changed, false); + assert.strictEqual( + (flow.node.instances[0].source as { data: string }).data, + "@flyde/nodes" + ); + }); +}); diff --git a/vscode/src/update-flyde-import-paths.ts b/vscode/src/update-flyde-import-paths.ts new file mode 100644 index 00000000..3e603b5e --- /dev/null +++ b/vscode/src/update-flyde-import-paths.ts @@ -0,0 +1,105 @@ +import * as path from "path"; +import { FlydeFlow } from "@flyde/core"; + +export type FileMove = { + oldPath: string; + newPath: string; +}; + +const normalizePath = (filePath: string) => path.resolve(filePath); + +const isSameOrInside = (candidate: string, target: string) => { + const normalizedCandidate = normalizePath(candidate); + const normalizedTarget = normalizePath(target); + const relative = path.relative(normalizedTarget, normalizedCandidate); + + return ( + relative === "" || + (!!relative && + !relative.startsWith("..") && + !path.isAbsolute(relative)) + ); +}; + +const getMovedPath = (candidate: string, moves: FileMove[]) => { + for (const move of moves) { + if (isSameOrInside(candidate, move.oldPath)) { + const relativeToMoveRoot = path.relative(move.oldPath, candidate); + return path.join(move.newPath, relativeToMoveRoot); + } + } +}; + +const getPreviousPath = (candidate: string, moves: FileMove[]) => { + for (const move of moves) { + if (isSameOrInside(candidate, move.newPath)) { + const relativeToMoveRoot = path.relative(move.newPath, candidate); + return path.join(move.oldPath, relativeToMoveRoot); + } + } +}; + +const toFlowImportPath = (fromDir: string, targetPath: string) => { + let relativePath = path.relative(fromDir, targetPath).replace(/\\/g, "/"); + + if (!relativePath.startsWith(".")) { + relativePath = `./${relativePath}`; + } + + return relativePath; +}; + +export function updateMovedFlydeImportPaths( + flow: FlydeFlow, + flowPath: string, + moves: FileMove[] +) { + const previousFlowPath = getPreviousPath(flowPath, moves) ?? flowPath; + const previousFlowDir = path.dirname(previousFlowPath); + const flowDir = path.dirname(flowPath); + let changed = false; + + for (const instance of flow.node.instances) { + if (instance.source?.type !== "file") { + continue; + } + + const sourcePath = instance.source.data; + if (typeof sourcePath !== "string") { + continue; + } + + const absoluteSourcePath = path.resolve(previousFlowDir, sourcePath); + const movedPath = getMovedPath(absoluteSourcePath, moves); + const nextSourcePath = movedPath ?? absoluteSourcePath; + const nextImportPath = toFlowImportPath(flowDir, nextSourcePath); + + if (nextImportPath === sourcePath) { + continue; + } + + instance.source.data = nextImportPath; + changed = true; + } + + if (flow.imports) { + for (const [importPath, importedIds] of Object.entries(flow.imports)) { + const absoluteImportPath = path.resolve(previousFlowDir, importPath); + const movedPath = getMovedPath(absoluteImportPath, moves); + const nextImportPath = toFlowImportPath( + flowDir, + movedPath ?? absoluteImportPath + ); + + if (nextImportPath === importPath) { + continue; + } + + delete flow.imports[importPath]; + flow.imports[nextImportPath] = importedIds; + changed = true; + } + } + + return changed; +} From 9d759c030b8270ef60504b700f6f1fe195ab9f3e Mon Sep 17 00:00:00 2001 From: Catnap7 <36249828+Catnap7@users.noreply.github.com> Date: Sat, 23 May 2026 03:50:29 +0900 Subject: [PATCH 2/2] Make disabled visual workflow valid --- .github/workflows/visual-tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/visual-tests.yml b/.github/workflows/visual-tests.yml index 437ec932..0d0ca1dd 100644 --- a/.github/workflows/visual-tests.yml +++ b/.github/workflows/visual-tests.yml @@ -3,6 +3,9 @@ name: Visual Regression Tests # DISABLED: Managing visual test baselines across different machines is too much of a hassle # Visual tests can still be run locally using: pnpm run test:visual +on: + workflow_dispatch: + # on: # pull_request: # branches: [main] @@ -135,4 +138,4 @@ jobs: repo: context.repo.repo, body: comment }); - } \ No newline at end of file + }