Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { activateReporter, reportEvent, analytics } from "./analytics";
import { showFirstRunPrivacyNotice, showPrivacySettings } from "./privacyNotice";

import { Template, getTemplates, scaffoldTemplate } from "./templateUtils";
import { registerFileMoveImportUpdater } from "./updateImportsOnMove";

// the application insights key (also known as instrumentation key)

Expand Down Expand Up @@ -229,6 +230,8 @@ export function activate(context: vscode.ExtensionContext) {
await showPrivacySettings();
})
);

registerFileMoveImportUpdater(context);
}

// this method is called when your extension is deactivated
Expand Down
196 changes: 196 additions & 0 deletions vscode/src/flowImportLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import * as path from "path";
import { deserializeFlow, serializeFlow } from "@flyde/loader/dist/server";

export interface RenamedFileEntry {
oldPath: string;
newPath: string;
}

function toPosixPath(p: string): string {
return p.split(path.sep).join("/");
}

function normalizeFsPath(p: string): string {
const resolved = path.resolve(p);
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
}

export function mapPathThroughRenames(
targetPath: string,
renamedFiles: ReadonlyArray<RenamedFileEntry>,
direction: "oldToNew" | "newToOld"
): string {
const resolvedTarget = path.resolve(targetPath);
const normTarget = normalizeFsPath(resolvedTarget);

for (const entry of renamedFiles) {
const fromPath = path.resolve(
direction === "oldToNew" ? entry.oldPath : entry.newPath
);
const toPath = path.resolve(
direction === "oldToNew" ? entry.newPath : entry.oldPath
);
const normFrom = normalizeFsPath(fromPath);

if (normTarget === normFrom) {
return toPath;
}

const fromPrefix = normFrom.endsWith(path.sep)
? normFrom
: normFrom + path.sep;
if (normTarget.startsWith(fromPrefix)) {
const suffix = resolvedTarget.slice(fromPath.length);
return toPath + suffix;
}
}

return resolvedTarget;
}

export function isRelativeImportPath(importPath: string): boolean {
if (!importPath || typeof importPath !== "string") {
return false;
}
if (importPath.includes("://") || importPath.startsWith("@")) {
return false;
}
return (
importPath.startsWith("./") ||
importPath.startsWith("../") ||
importPath.endsWith(".flyde") ||
importPath.endsWith(".flyde.ts") ||
importPath.endsWith(".flyde.js") ||
importPath.endsWith(".flyde.mjs") ||
importPath.endsWith(".flyde.cjs")
);
}

export function computeUpdatedRelativeImport(
importPath: string,
oldFlowPath: string,
newFlowPath: string,
renamedFiles: ReadonlyArray<RenamedFileEntry>
): string {
if (!isRelativeImportPath(importPath)) {
return importPath;
}

const oldFlowDir = path.dirname(path.resolve(oldFlowPath));
const newFlowDir = path.dirname(path.resolve(newFlowPath));

const oldTargetAbs = path.resolve(oldFlowDir, importPath);
const newTargetAbs = mapPathThroughRenames(
oldTargetAbs,
renamedFiles,
"oldToNew"
);

if (
normalizeFsPath(oldFlowDir) === normalizeFsPath(newFlowDir) &&
normalizeFsPath(oldTargetAbs) === normalizeFsPath(newTargetAbs)
) {
return importPath;
}

const relPosix = path.posix.relative(
toPosixPath(newFlowDir),
toPosixPath(newTargetAbs)
);

return relPosix.startsWith(".") ? relPosix : `./${relPosix}`;
}

export function updateFlowObjectImports(
flow: any,
currentFlowPath: string,
renamedFiles: ReadonlyArray<RenamedFileEntry>
): boolean {
if (!flow || typeof flow !== "object") {
return false;
}

const oldFlowPath = mapPathThroughRenames(
currentFlowPath,
renamedFiles,
"newToOld"
);
let modified = false;

if (flow.imports && typeof flow.imports === "object") {
const updatedImports: Record<string, any> = {};
for (const [impKey, impVal] of Object.entries(flow.imports)) {
const newKey = computeUpdatedRelativeImport(
impKey,
oldFlowPath,
currentFlowPath,
renamedFiles
);
if (newKey !== impKey) {
modified = true;
}
updatedImports[newKey] = impVal;
}
flow.imports = updatedImports;
}

function visitVisualNode(node: any) {
if (!node || !Array.isArray(node.instances)) {
return;
}
for (const instance of node.instances) {
if (
instance?.source?.type === "file" &&
typeof instance.source.data === "string"
) {
const updatedPath = computeUpdatedRelativeImport(
instance.source.data,
oldFlowPath,
currentFlowPath,
renamedFiles
);
if (updatedPath !== instance.source.data) {
instance.source.data = updatedPath;
modified = true;
}
} else if (
instance?.source?.type === "inline" &&
instance.source.data &&
typeof instance.source.data === "object"
) {
visitVisualNode(instance.source.data);
}
if (instance?.node && typeof instance.node === "object") {
visitVisualNode(instance.node);
}
}
}

if (flow.node) {
visitVisualNode(flow.node);
}

return modified;
}

export function updateFlowImportsOnRename(
flowContent: string,
currentFlowPath: string,
renamedFiles: ReadonlyArray<RenamedFileEntry>
): string | null {
try {
const flow = deserializeFlow(flowContent, currentFlowPath);
const modified = updateFlowObjectImports(
flow,
currentFlowPath,
renamedFiles
);
if (!modified) {
return null;
}
return serializeFlow(flow);
} catch (err) {
console.error(`Failed to update imports for flow at ${currentFlowPath}:`, err);
return null;
}
}
130 changes: 130 additions & 0 deletions vscode/src/updateImportsOnMove.pure.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as assert from "assert";
import {
computeUpdatedRelativeImport,
updateFlowObjectImports,
} from "./flowImportLogic";

suite("updateImportsOnMove Unit Tests", () => {
test("updates file-sourced instance when an imported node file is moved", () => {
const flow = {
imports: {},
node: {
id: "MainFlow",
instances: [
{
id: "ins1",
nodeId: "Add",
type: "code",
source: {
type: "file",
data: "./Add.flyde.ts",
},
},
],
},
};

const changed = updateFlowObjectImports(flow, "/workspace/flows/MainFlow.flyde", [
{
oldPath: "/workspace/flows/Add.flyde.ts",
newPath: "/workspace/flows/sub/Add.flyde.ts",
},
]);

assert.strictEqual(changed, true);
assert.strictEqual(flow.node.instances[0].source.data, "./sub/Add.flyde.ts");
});

test("updates relative file imports when the flow file itself is moved", () => {
const flow = {
imports: {},
node: {
id: "MainFlow",
instances: [
{
id: "ins1",
nodeId: "ChildFlow",
type: "visual",
source: {
type: "file",
data: "./ChildFlow.flyde",
},
},
{
id: "ins2",
nodeId: "InlineValue",
type: "code",
source: {
type: "package",
data: "@flyde/nodes",
},
},
],
},
};

const changed = updateFlowObjectImports(
flow,
"/workspace/flows/nested/MainFlow.flyde",
[
{
oldPath: "/workspace/flows/MainFlow.flyde",
newPath: "/workspace/flows/nested/MainFlow.flyde",
},
]
);

assert.strictEqual(changed, true);
assert.strictEqual(flow.node.instances[0].source.data, "../ChildFlow.flyde");
assert.strictEqual(flow.node.instances[1].source.data, "@flyde/nodes");
});

test("computes updated relative import across directory moves", () => {
const updated = computeUpdatedRelativeImport(
"./shared/Helper.flyde.js",
"/workspace/flows/MainFlow.flyde",
"/workspace/flows/MainFlow.flyde",
[
{
oldPath: "/workspace/flows/shared",
newPath: "/workspace/common",
},
]
);

assert.strictEqual(updated, "../common/Helper.flyde.js");
});

test("leaves package and external sources untouched", () => {
const flow = {
imports: {
"@flyde/nodes": ["Add"],
},
node: {
id: "MainFlow",
instances: [
{
id: "ins1",
nodeId: "Add",
type: "code",
source: {
type: "package",
data: "@flyde/nodes",
},
},
],
},
};

const changed = updateFlowObjectImports(flow, "/workspace/flows/MainFlow.flyde", [
{
oldPath: "/workspace/flows/Add.flyde.ts",
newPath: "/workspace/flows/sub/Add.flyde.ts",
},
]);

assert.strictEqual(changed, false);
assert.strictEqual(flow.node.instances[0].source.data, "@flyde/nodes");
assert.deepStrictEqual(flow.imports, { "@flyde/nodes": ["Add"] });
});
});
Loading