From 343ab8bcd11c01155d2e53345763796df465339a Mon Sep 17 00:00:00 2001 From: Tomasz Nuri Date: Thu, 10 Sep 2026 18:10:26 +0300 Subject: [PATCH] fix(vscode): auto-update imports when files are moved (#112) --- .../server/findReferencedNodeServer.ts | 69 ++++++- pnpm-lock.yaml | 35 +++- vscode/package.json | 3 +- vscode/src/extension.ts | 5 + vscode/src/fileMoveHandler.ts | 173 ++++++++++++++++++ vscode/src/test/fileMoveHandler.test.ts | 124 +++++++++++++ 6 files changed, 391 insertions(+), 18 deletions(-) create mode 100644 vscode/src/fileMoveHandler.ts create mode 100644 vscode/src/test/fileMoveHandler.test.ts diff --git a/loader/src/resolver/server/findReferencedNodeServer.ts b/loader/src/resolver/server/findReferencedNodeServer.ts index ecfc88f6..3e6e923c 100644 --- a/loader/src/resolver/server/findReferencedNodeServer.ts +++ b/loader/src/resolver/server/findReferencedNodeServer.ts @@ -7,8 +7,9 @@ import { } from "@flyde/core"; import * as _Nodes from "@flyde/nodes/dist/all"; -import { join } from "path"; +import { join, dirname } from "path"; import { existsSync, readFileSync } from "fs"; +import * as glob from "glob"; import { resolveImportablePaths } from "./resolveImportablePaths"; import { deserializeFlowByPath } from "../../serdes"; import { ReferencedNodeFinder } from "./../ReferencedNodeFinder"; @@ -205,16 +206,68 @@ function getLocalOrPackagePaths(fullFlowPath: string, importPath: string) { if (existsSync(fullImportPath)) { return [fullImportPath]; - } else { - try { - return resolveImportablePaths(fullFlowPath, importPath); - } catch (e) { - if (importPath !== "@flyde/nodes") { - throw new Error(`Cannot find module ${importPath} in ${fullFlowPath}`); + } + + const extensions = [".flyde", ".flyde.ts", ".ts", ".js"]; + for (const ext of extensions) { + if (existsSync(fullImportPath + ext)) { + return [fullImportPath + ext]; + } + } + + // If a relative import is broken because the file was moved to another folder, search project tree + if (importPath.startsWith(".")) { + const flowDir = dirname(fullFlowPath); + const fileName = importPath.split("/").pop() || ""; + if (fileName) { + const candidates = [fileName]; + for (const ext of extensions) { + if (!fileName.endsWith(ext)) { + candidates.push(fileName + ext); + } + } + + let currentDir = flowDir; + let projectRoot = flowDir; + for (let i = 0; i < 5; i++) { + if ( + existsSync(join(currentDir, "package.json")) || + existsSync(join(currentDir, ".git")) + ) { + projectRoot = currentDir; + break; + } + const parent = dirname(currentDir); + if (parent === currentDir) break; + currentDir = parent; + } + + for (const cand of candidates) { + const matches = glob.sync(`**/${cand}`, { + cwd: projectRoot, + ignore: [ + "**/node_modules/**", + "**/dist/**", + "**/build/**", + "**/.git/**", + ], + absolute: true, + }); + if (matches.length > 0) { + return [matches[0]]; + } } - return []; } } + + try { + return resolveImportablePaths(fullFlowPath, importPath); + } catch (e) { + if (importPath !== "@flyde/nodes") { + throw new Error(`Cannot find module ${importPath} in ${fullFlowPath}`); + } + return []; + } } // Helper function to get the source code of a node from a file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40423ef1..2dad7f8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -563,6 +563,9 @@ importers: walker: specifier: ^1.0.8 version: 1.0.8 + yaml: + specifier: ^2.8.0 + version: 2.8.0 devDependencies: '@playwright/test': specifier: ^1.53.2 @@ -1308,6 +1311,7 @@ packages: '@fortawesome/react-fontawesome@0.2.2': resolution: {integrity: sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==} + deprecated: v0.2.x is no longer supported. Unless you are still using FontAwesome 5, please update to v3.1.1 or greater. peerDependencies: '@fortawesome/fontawesome-svg-core': ~1 || ~6 react: '>=16.3' @@ -2753,6 +2757,9 @@ packages: '@sinonjs/text-encoding@0.7.3': resolution: {integrity: sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==} + deprecated: |- + Deprecated: no longer maintained and no longer used by Sinon packages. See + https://github.com/sinonjs/nise/issues/243 for replacement details. '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -3134,6 +3141,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.10.1': resolution: {integrity: sha512-zohDKXT1Ok0yhbVGff4YAg9HUs5ietG5GpvJBPFSApZnGe7uf2cd26DRhKZbn0Be6xHUZrSzP+RAgMmzyc71EA==} @@ -4507,6 +4515,7 @@ packages: eslint@9.30.1: resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -4871,25 +4880,27 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@6.0.4: resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} @@ -6077,6 +6088,7 @@ packages: next@15.3.3: resolution: {integrity: sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -6573,6 +6585,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.1.2: @@ -7484,6 +7497,7 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me terser-webpack-plugin@5.3.14: resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} @@ -7843,11 +7857,12 @@ packages: uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -8009,10 +8024,12 @@ packages: whatwg-encoding@1.0.5: resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@2.3.0: resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} @@ -11972,7 +11989,7 @@ snapshots: '@typescript-eslint/parser': 7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) eslint: 9.30.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.30.1(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.30.1(jiti@2.4.2)) eslint-plugin-react: 7.37.5(eslint@9.30.1(jiti@2.4.2)) @@ -11992,7 +12009,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.30.1(jiti@2.4.2)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1(supports-color@8.1.1) @@ -12007,14 +12024,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: '@typescript-eslint/parser': 7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) eslint: 9.30.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.30.1(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -12029,7 +12046,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.30.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/vscode/package.json b/vscode/package.json index 3f1678cc..338c3c25 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -254,7 +254,8 @@ "socket.io": "^4.5.1", "ts-node": "^10.9.2", "use-debounce": "^8.0.4", - "walker": "^1.0.8" + "walker": "^1.0.8", + "yaml": "^2.8.0" }, "packageManager": "yarn@3.3.1" } \ No newline at end of file diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index ea022727..3accef8f 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 { handleFileRenames } from "./fileMoveHandler"; // the application insights key (also known as instrumentation key) @@ -229,6 +230,10 @@ export function activate(context: vscode.ExtensionContext) { await showPrivacySettings(); }) ); + + context.subscriptions.push( + vscode.workspace.onDidRenameFiles(handleFileRenames) + ); } // this method is called when your extension is deactivated diff --git a/vscode/src/fileMoveHandler.ts b/vscode/src/fileMoveHandler.ts new file mode 100644 index 00000000..11e65941 --- /dev/null +++ b/vscode/src/fileMoveHandler.ts @@ -0,0 +1,173 @@ +import type * as vscodeTypes from "vscode"; +import * as path from "path"; +import * as yaml from "yaml"; + +export interface FileRename { + oldUri: vscodeTypes.Uri; + newUri: vscodeTypes.Uri; +} + +/** + * Computes updated YAML text for a .flyde flow file when files or folders have been moved/renamed. + * Returns the updated content string if changes were made, or null if no imports were affected. + */ +export function computeUpdatedImports( + flowContent: string, + flowFilePath: string, + renames: { oldPath: string; newPath: string }[] +): string | null { + let doc: yaml.Document.Parsed; + try { + doc = yaml.parseDocument(flowContent); + } catch { + return null; + } + + const importsNode = doc.get("imports") as any; + if (!importsNode || typeof importsNode.toJSON !== "function") { + return null; + } + + const importsObj = importsNode.toJSON(); + if (!importsObj || typeof importsObj !== "object") { + return null; + } + + const flowDir = path.dirname(flowFilePath); + let changed = false; + + // Check if this flow file itself was moved + const selfRename = renames.find( + (r) => path.normalize(r.oldPath) === path.normalize(flowFilePath) + ); + + for (const importKey of Object.keys(importsObj)) { + // Only process relative file imports + if (!importKey.startsWith(".")) { + continue; + } + + const value = importsNode.get(importKey); + + if (selfRename) { + // The flow file itself was moved from oldPath to newPath. + // Its imports were relative to oldDir and now must be relative to newDir. + const oldFlowDir = path.dirname(selfRename.oldPath); + const newFlowDir = path.dirname(selfRename.newPath); + + // Resolve what the import pointed to from the old location + const absTarget = path.resolve(oldFlowDir, importKey); + let newRel = path.relative(newFlowDir, absTarget).split(path.sep).join("/"); + if (!newRel.startsWith(".") && !newRel.startsWith("/")) { + newRel = "./" + newRel; + } + + if (newRel !== importKey) { + importsNode.delete(importKey); + importsNode.set(newRel, value); + changed = true; + } + } else { + // Another file or directory was moved. Check if this import targeted that file/directory. + const absTarget = path.resolve(flowDir, importKey); + + for (const rename of renames) { + const normOld = path.normalize(rename.oldPath); + const normNew = path.normalize(rename.newPath); + const normTarget = path.normalize(absTarget); + + let match = false; + let newTarget = ""; + + // Direct file match + if (normTarget === normOld) { + match = true; + newTarget = normNew; + } else if ( + // Extensionless match (e.g. import was './sub/code' and file was 'sub/code.flyde.ts' or 'sub/code.ts') + normTarget.replace(/\.(flyde|flyde\.ts|ts|js)$/, "") === + normOld.replace(/\.(flyde|flyde\.ts|ts|js)$/, "") + ) { + match = true; + const oldExt = path.extname(normOld); + const hasExt = + importKey.endsWith(oldExt) || + importKey.endsWith(".ts") || + importKey.endsWith(".flyde"); + newTarget = hasExt + ? normNew + : normNew.replace(/\.(flyde|flyde\.ts|ts|js)$/, ""); + } else if ( + // Directory match (a parent folder was moved/renamed) + normTarget.startsWith(normOld + path.sep) + ) { + match = true; + newTarget = normNew + normTarget.slice(normOld.length); + } + + if (match) { + let newRel = path.relative(flowDir, newTarget).split(path.sep).join("/"); + if (!newRel.startsWith(".") && !newRel.startsWith("/")) { + newRel = "./" + newRel; + } + + if (newRel !== importKey) { + importsNode.delete(importKey); + importsNode.set(newRel, value); + changed = true; + break; + } + } + } + } + } + + return changed ? doc.toString() : null; +} + +/** + * Handles VS Code workspace file rename events and updates all affected .flyde imports. + */ +export async function handleFileRenames( + event: vscodeTypes.FileRenameEvent +): Promise { + const vscode = require("vscode"); + const renames = event.files.map((f: vscodeTypes.FileRenameEvent["files"][number]) => ({ + oldPath: f.oldUri.fsPath, + newPath: f.newUri.fsPath, + })); + + // Find all .flyde files in the workspace + const flydeFiles = await vscode.workspace.findFiles("**/*.flyde"); + + const workspaceEdit = new vscode.WorkspaceEdit(); + let editCount = 0; + + for (const fileUri of flydeFiles) { + try { + const document = await vscode.workspace.openTextDocument(fileUri); + const text = document.getText(); + const updatedText = computeUpdatedImports(text, fileUri.fsPath, renames); + + if (updatedText !== null) { + const fullRange = new vscode.Range( + document.positionAt(0), + document.positionAt(text.length) + ); + workspaceEdit.replace(fileUri, fullRange, updatedText); + editCount++; + } + } catch (e) { + console.error(`[Flyde] Error updating imports for ${fileUri.fsPath}:`, e); + } + } + + if (editCount > 0) { + const success = await vscode.workspace.applyEdit(workspaceEdit); + if (success) { + vscode.window.showInformationMessage( + `Flyde: Updated imports across ${editCount} flow(s) after file move.` + ); + } + } +} diff --git a/vscode/src/test/fileMoveHandler.test.ts b/vscode/src/test/fileMoveHandler.test.ts new file mode 100644 index 00000000..d5b400de --- /dev/null +++ b/vscode/src/test/fileMoveHandler.test.ts @@ -0,0 +1,124 @@ +import * as assert from "assert"; +import * as path from "path"; +import { suite, test } from "mocha"; +import { computeUpdatedImports } from "../fileMoveHandler"; + +suite("File Move Handler Test Suite", () => { + test("updates relative import when imported .flyde file is moved", () => { + const flowPath = path.resolve("/workspace/src/Main.flyde"); + const flowContent = `imports: + ./sub/Helper.flyde: + - HelperNode +node: + id: Main + instances: [] + connections: [] +`; + + const renames = [ + { + oldPath: path.resolve("/workspace/src/sub/Helper.flyde"), + newPath: path.resolve("/workspace/src/utils/Helper.flyde"), + }, + ]; + + const result = computeUpdatedImports(flowContent, flowPath, renames); + assert.notStrictEqual(result, null, "Should return updated content"); + assert.ok( + result!.includes("./utils/Helper.flyde"), + `Expected ./utils/Helper.flyde in result, got:\n${result}` + ); + assert.ok( + !result!.includes("./sub/Helper.flyde"), + "Old path should no longer be present" + ); + }); + + test("updates relative import when imported code node (.flyde.ts) is moved", () => { + const flowPath = path.resolve("/workspace/src/Main.flyde"); + const flowContent = `imports: + ./nodes/Add.flyde.ts: + - AddNode +node: + id: Main +`; + + const renames = [ + { + oldPath: path.resolve("/workspace/src/nodes/Add.flyde.ts"), + newPath: path.resolve("/workspace/src/shared/math/Add.flyde.ts"), + }, + ]; + + const result = computeUpdatedImports(flowContent, flowPath, renames); + assert.notStrictEqual(result, null); + assert.ok(result!.includes("./shared/math/Add.flyde.ts")); + }); + + test("updates relative imports when directory is moved or renamed", () => { + const flowPath = path.resolve("/workspace/src/Main.flyde"); + const flowContent = `imports: + ./components/Button.flyde: + - Button +node: + id: Main +`; + + const renames = [ + { + oldPath: path.resolve("/workspace/src/components"), + newPath: path.resolve("/workspace/src/ui/components"), + }, + ]; + + const result = computeUpdatedImports(flowContent, flowPath, renames); + assert.notStrictEqual(result, null); + assert.ok(result!.includes("./ui/components/Button.flyde")); + }); + + test("updates imports when the .flyde file itself is moved to a new folder", () => { + const oldFlowPath = path.resolve("/workspace/src/Main.flyde"); + const newFlowPath = path.resolve("/workspace/src/flows/Main.flyde"); + const flowContent = `imports: + ./Helper.flyde: + - HelperNode +node: + id: Main +`; + + const renames = [ + { + oldPath: oldFlowPath, + newPath: newFlowPath, + }, + ]; + + const result = computeUpdatedImports(flowContent, oldFlowPath, renames); + assert.notStrictEqual(result, null); + // Relative to /workspace/src/flows/, Helper.flyde at /workspace/src/Helper.flyde is ../Helper.flyde + assert.ok( + result!.includes("../Helper.flyde"), + `Expected ../Helper.flyde in result, got:\n${result}` + ); + }); + + test("returns null when no imports are affected", () => { + const flowPath = path.resolve("/workspace/src/Main.flyde"); + const flowContent = `imports: + ./sub/Helper.flyde: + - HelperNode +node: + id: Main +`; + + const renames = [ + { + oldPath: path.resolve("/workspace/src/unrelated/Other.flyde"), + newPath: path.resolve("/workspace/src/unrelated2/Other.flyde"), + }, + ]; + + const result = computeUpdatedImports(flowContent, flowPath, renames); + assert.strictEqual(result, null); + }); +});