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
5 changes: 4 additions & 1 deletion .github/workflows/visual-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -135,4 +138,4 @@ jobs:
repo: context.repo.repo,
body: comment
});
}
}
40 changes: 40 additions & 0 deletions vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 =
Expand Down
122 changes: 122 additions & 0 deletions vscode/src/update-flyde-import-paths.test.ts
Original file line number Diff line number Diff line change
@@ -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"
);
});
});
105 changes: 105 additions & 0 deletions vscode/src/update-flyde-import-paths.ts
Original file line number Diff line number Diff line change
@@ -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;
}