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 { registerFlydeImportPathRewriter } from "./registerFlydeImportPathRewriter";

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

Expand All @@ -37,6 +38,8 @@ export function activate(context: vscode.ExtensionContext) {
vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0];
const fileRoot = firstWorkspace ? firstWorkspace.uri.fsPath : "";

registerFlydeImportPathRewriter(context);

// Initialize analytics with context
analytics.setContext(context);

Expand Down
139 changes: 139 additions & 0 deletions vscode/src/flydeImportPathRewriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import * as path from "path";

type RewriteCandidate = {
dataLineIndex: number;
importPath: string;
quote: string;
};

export function isFlydeFile(filePath: string) {
return filePath.endsWith(".flyde");
}

export function toFlydeImportPath(fromFlowPath: string, targetPath: string) {
const fromDir = path.dirname(fromFlowPath);
const relativePath = path.relative(fromDir, targetPath).replace(/\\/g, "/");

if (relativePath.startsWith(".")) {
return relativePath;
}

return `./${relativePath}`;
}

export function rewriteMovedFlowImports(
content: string,
oldFlowPath: string,
newFlowPath: string
) {
return rewriteFileSourceImports(content, (importPath) => {
const originalTargetPath = path.resolve(path.dirname(oldFlowPath), importPath);
return toFlydeImportPath(newFlowPath, originalTargetPath);
});
}

export function rewriteReferencesToMovedFile(
content: string,
flowPath: string,
oldTargetPath: string,
newTargetPath: string
) {
return rewriteFileSourceImports(content, (importPath) => {
const resolvedImportPath = path.resolve(path.dirname(flowPath), importPath);

if (!samePath(resolvedImportPath, oldTargetPath)) {
return importPath;
}

return toFlydeImportPath(flowPath, newTargetPath);
});
}

function rewriteFileSourceImports(
content: string,
rewriteImportPath: (importPath: string) => string
) {
const hasTrailingNewline = content.endsWith("\n");
const lines = content.split(/\r?\n/);

const candidates = findFileSourceImportCandidates(lines);

for (const candidate of candidates) {
const rewrittenImportPath = rewriteImportPath(candidate.importPath);

if (rewrittenImportPath === candidate.importPath) {
continue;
}

lines[candidate.dataLineIndex] = lines[candidate.dataLineIndex].replace(
/(\bdata:\s*)(['"]?)(.+?)(\2)\s*$/,
`$1${candidate.quote}${rewrittenImportPath}${candidate.quote}`
);
}

const rewritten = lines.join("\n");
return hasTrailingNewline ? rewritten : rewritten.replace(/\n$/, "");
}

function findFileSourceImportCandidates(lines: string[]) {
const candidates: RewriteCandidate[] = [];

for (let sourceLineIndex = 0; sourceLineIndex < lines.length; sourceLineIndex++) {
const sourceLine = lines[sourceLineIndex];
const sourceMatch = sourceLine.match(/^(\s*)(-\s*)?source:\s*$/);

if (!sourceMatch) {
continue;
}

const sourceIndent = sourceMatch[1].length + (sourceMatch[2]?.length ?? 0);
let hasFileType = false;
let dataLineIndex = -1;
let importPath = "";
let quote = "";

for (let index = sourceLineIndex + 1; index < lines.length; index++) {
const line = lines[index];

if (line.trim() === "") {
continue;
}

const currentIndent = line.match(/^(\s*)/)?.[1].length ?? 0;

if (currentIndent <= sourceIndent) {
break;
}

const typeMatch = line.match(/^\s*type:\s*file\s*$/);
if (typeMatch) {
hasFileType = true;
continue;
}

const dataMatch = line.match(/^\s*data:\s*(['"]?)(.+?)\1\s*$/);
if (dataMatch) {
dataLineIndex = index;
quote = dataMatch[1];
importPath = dataMatch[2];
}
}

if (hasFileType && dataLineIndex !== -1) {
candidates.push({ dataLineIndex, importPath, quote });
}
}

return candidates;
}

function samePath(left: string, right: string) {
const normalizedLeft = path.resolve(left);
const normalizedRight = path.resolve(right);

if (process.platform === "win32") {
return normalizedLeft.toLowerCase() === normalizedRight.toLowerCase();
}

return normalizedLeft === normalizedRight;
}
56 changes: 56 additions & 0 deletions vscode/src/registerFlydeImportPathRewriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as vscode from "vscode";
import {
isFlydeFile,
rewriteMovedFlowImports,
rewriteReferencesToMovedFile,
} from "./flydeImportPathRewriter";

const decoder = new TextDecoder();
const encoder = new TextEncoder();

export function registerFlydeImportPathRewriter(context: vscode.ExtensionContext) {
const disposable = vscode.workspace.onDidRenameFiles(async (event) => {
for (const file of event.files) {
await rewriteMovedFlow(file.oldUri, file.newUri);
await rewriteWorkspaceReferences(file.oldUri, file.newUri);
}
});

context.subscriptions.push(disposable);
}

async function rewriteMovedFlow(oldUri: vscode.Uri, newUri: vscode.Uri) {
if (!isFlydeFile(oldUri.fsPath) || !isFlydeFile(newUri.fsPath)) {
return;
}

await rewriteFile(newUri, (content) =>
rewriteMovedFlowImports(content, oldUri.fsPath, newUri.fsPath)
);
}

async function rewriteWorkspaceReferences(oldUri: vscode.Uri, newUri: vscode.Uri) {
const flowUris = await vscode.workspace.findFiles("**/*.flyde", "**/node_modules/**");

for (const flowUri of flowUris) {
await rewriteFile(flowUri, (content) =>
rewriteReferencesToMovedFile(content, flowUri.fsPath, oldUri.fsPath, newUri.fsPath)
);
}
}

async function rewriteFile(
uri: vscode.Uri,
rewrite: (content: string) => string
) {
try {
const content = decoder.decode(await vscode.workspace.fs.readFile(uri));
const rewritten = rewrite(content);

if (rewritten !== content) {
await vscode.workspace.fs.writeFile(uri, encoder.encode(rewritten));
}
} catch {
// A single unreadable or malformed flow should not block the user's file move.
}
}
81 changes: 81 additions & 0 deletions vscode/src/test/flydeImportPathRewriter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as assert from "assert";
import * as path from "path";
import {
rewriteMovedFlowImports,
rewriteReferencesToMovedFile,
toFlydeImportPath,
} from "../flydeImportPathRewriter";

suite("flyde import path rewriter", () => {
test("rewrites a moved flow's own relative imports", () => {
const content = [
"imports: {}",
"node:",
" instances:",
" - source:",
" type: file",
" data: ./ChildFlow.flyde",
" nodeId: ChildFlow",
"",
].join("\n");

const rewritten = rewriteMovedFlowImports(
content,
path.join("workspace", "flows", "ParentFlow.flyde"),
path.join("workspace", "flows", "nested", "ParentFlow.flyde")
);

assert.match(rewritten, /data: \.\.\/ChildFlow\.flyde/);
});

test("rewrites references when the imported flow moves", () => {
const content = [
"imports: {}",
"node:",
" instances:",
" - source:",
" type: file",
" data: ./ChildFlow.flyde",
" nodeId: ChildFlow",
"",
].join("\n");

const rewritten = rewriteReferencesToMovedFile(
content,
path.join("workspace", "flows", "ParentFlow.flyde"),
path.join("workspace", "flows", "ChildFlow.flyde"),
path.join("workspace", "shared", "ChildFlow.flyde")
);

assert.match(rewritten, /data: \.\.\/shared\/ChildFlow\.flyde/);
});

test("leaves package sources untouched", () => {
const content = [
"node:",
" instances:",
" - source:",
" type: package",
" data: \"@flyde/nodes\"",
" nodeId: Add",
"",
].join("\n");

const rewritten = rewriteMovedFlowImports(
content,
path.join("workspace", "flows", "ParentFlow.flyde"),
path.join("workspace", "nested", "ParentFlow.flyde")
);

assert.strictEqual(rewritten, content);
});

test("uses relative POSIX import paths for same-folder targets", () => {
const importPath = toFlydeImportPath(
path.join("workspace", "flows", "ParentFlow.flyde"),
path.join("workspace", "flows", "ChildFlow.flyde")
);

assert.strictEqual(importPath, "./ChildFlow.flyde");
});
});