Skip to content

fix: resolve #112 - [flydelabs/flyde] Broken imports after moving files - #292

Open
Ollie520 wants to merge 1 commit into
flydelabs:mainfrom
Ollie520:bounty-fix-112
Open

Ollie520 wants to merge 1 commit into
flydelabs:mainfrom
Ollie520:bounty-fix-112

Conversation

@Ollie520

Copy link
Copy Markdown

Resolves #112
/claim #112

Solution for [github/flydelabs_flyde_112]: [flydelabs/flyde] Broken imports after moving files


Pull Request Summary

Problem:
When a Flyde flow file (e.g., `my_subflow.flyde`) is moved to a different directory, any other Flyde flow files that import it will have broken import paths. The current import mechanism uses relative paths, which become invalid after a file move. This requires manual correction of all affected import paths, leading to a poor developer experience.

Root Cause:
The Flyde editor currently stores import paths as static relative paths within the `.flyde` JSON files. When a file is moved, these relative paths are no longer valid from the perspective of the importing file. The editor does not automatically update these paths.

Solution:
Implement a file system watcher that monitors for file rename/move events within the workspace. When a `.flyde` file is moved, identify all other `.flyde` files that import it. For each importing file, update the `path` property of the relevant import node to reflect the new relative path to the moved file. This ensures that imports remain valid and functional after file system operations.

Key Changes:
- Introduce a `FileWatcherService` to listen for `vscode.workspace.onDidRenameFiles` events.
- Implement logic within the `FileWatcherService` to:
    - Determine if a renamed/moved file is a `.flyde` flow.
    - Find all `.flyde` files in the workspace that might import the moved file.
    - Parse each potential importing file to identify import nodes.
    - Calculate the new relative path for the moved file.
    - Update the `path` property of the import node in the importing file's JSON structure.
    - Save the modified importing file.
- Integrate the `FileWatcherService` into the extension's activation lifecycle.

Testing:
1. Create two Flyde flow files: `main.flyde` and `subflow.flyde` in the same directory.
2. In `main.flyde`, add an import node that imports `subflow.flyde`. Verify it works.
3. Create a new directory, e.g., `subflows/`.
4. Move `subflow.flyde` from its original location to `subflows/subflow.flyde` using VS Code's file explorer.
5. Open `main.flyde`. Verify that the import path for `subflow.flyde` has been automatically updated (e.g., from `./subflow.flyde` to `./subflows/subflow.flyde`).
6. Run `main.flyde` to confirm the import is still functional.
7. Test edge cases:
    - Moving a file to a sibling directory.
    - Moving a file to a parent directory.
    - Moving a file to a deeply nested directory.
    - Moving a file that is not imported by any other file (should have no effect on other files).
    - Moving a file that is imported by multiple files.

File: src/extension.ts

import * as vscode from 'vscode';
import { FlydeEditorProvider } from './flydeEditor';
import { FileWatcherService } from './fileWatcherService'; // Import the new service

export function activate(context: vscode.ExtensionContext) {
	console.log('Congratulations, your extension "flyde" is now active!');

	// Register the custom editor provider
	context.subscriptions.push(FlydeEditorProvider.register(context));

	// Initialize and register the FileWatcherService
	const fileWatcherService = new FileWatcherService();
	context.subscriptions.push(fileWatcherService); // Add to subscriptions for disposal
}

export function deactivate() {}

File: src/fileWatcherService.ts

import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs/promises';

/**
 * Represents the structure of a Flyde flow file, specifically focusing on imports.
 */
interface FlydeFlow {
    nodes: Array<{
        id: string;
        type: string;
        inputs?: any;
        outputs?: any;
        params?: any;
        // Other properties might exist, but we only care about 'path' for imports
        path?: string; // For 'import' type nodes
    }>;
    // Other properties of a Flyde flow file
}

export class FileWatcherService implements vscode.Disposable {
    private disposable: vscode.Disposable;

    constructor() {
        this.disposable = vscode.workspace.onDidRenameFiles(this.handleFileRename, this);
        console.log('Flyde FileWatcherService initialized.');
    }

    /**
     * Handles file rename/move events in the workspace.
     * @param event The rename event containing old and new URIs.
     */
    private async handleFileRename(event: vscode.FileRenameEvent) {
        console.log('File rename event detected.');

        for (const file of event.files) {
            const oldUri = file.oldUri;
            const newUri = file.newUri;

            // We only care about .flyde files being moved
            if (oldUri.fsPath.endsWith('.flyde') && newUri.fsPath.endsWith('.flyde')) {
                console.log(`Flyde file moved: ${oldUri.fsPath} -> ${newUri.fsPath}`);
                await this.updateImportsInWorkspace(oldUri, newUri);
            }
        }
    }

    /**
     * Updates import paths in all relevant .flyde files in the workspace
     * when a .flyde file is moved.
     * @param movedFileOldUri The old URI of the moved .flyde file.
     * @param movedFileNewUri The new URI of the moved .flyde file.
     */
    private async updateImportsInWorkspace(movedFileOldUri: vscode.Uri, movedFileNewUri: vscode.Uri) {
        const workspaceFolders = vscode.workspace.workspaceFolders;
        if (!workspaceFolders || workspaceFolders.length === 0) {
            console.log('No workspace folders open.');
            return;
        }

        // Find all .flyde files in the workspace
        const flydeFiles = await vscode.workspace.findFiles('**/*.flyde', '**/node_modules/**');

        for (const importingFileUri of flydeFiles) {
            // Skip the moved file itself
            if (importingFileUri.fsPath === movedFileNewUri.fsPath) {
                continue;
            }

            try {
                const content = await fs.readFile(importingFileUri.fsPath, 'utf8');
                const flow: FlydeFlow = JSON.parse(content);
                let changed = false;

                for (const node of flow.nodes) {
                    // Check if it's an 'import' type node and has a 'path' property
                    if (node.type === 'import' && node.path) {
                        const currentImportPath = node.path;
                        const importingFileDir = path.dirname(importingFileUri.fsPath);

                        // Resolve the absolute path of the currently imported file
                        const importedFileAbsolutePath = path.resolve(importingFileDir, currentImportPath);

                        // Check if this import path points to the file that was just moved
                        if (importedFileAbsolutePath === movedFileOldUri.fsPath) {
                            // Calculate the new relative path from the importing file to the moved file's new location
                            const newRelativePath = path.relative(importingFileDir, movedFileNewUri.fsPath);

                            // Ensure the path starts with ./ or ../ if it's a relative path
                            const formattedNewRelativePath = this.formatRelativePath(newRelativePath);

                            if (node.path !== formattedNewRelativePath) {
                                node.path = formattedNewRelativePath;
                                changed = true;
                                console.log(`Updated import in ${importingFileUri.fsPath}: ${currentImportPath} -> ${formattedNewRelativePath}`);
                            }
                        }
                    }
                }

                if (changed) {
                    // Write the updated content back to the file
                    await fs.writeFile(importingFileUri.fsPath, JSON.stringify(flow, null, 2), 'utf8');
                    vscode.window.showInformationMessage(`Flyde: Updated import path in ${path.basename(importingFileUri.fsPath)}.`);
                }
            } catch (error: any) {
                console.error(`Error processing file ${importingFileUri.fsPath}: ${error.message}`);
                // vscode.window.showErrorMessage(`Flyde: Error updating imports in ${path.basename(importingFileUri.fsPath)}: ${error.message}`);
            }
        }
    }

    /**
     * Formats a relative path to ensure it starts with './' or '../' if necessary.
     * @param relativePath The calculated relative path.
     * @returns The formatted relative path.
     */
    private formatRelativePath(relativePath: string): string {
        if (relativePath.startsWith('..')) {
            return relativePath;
        }
        if (relativePath.startsWith('.' + path.sep)) { // e.g., './folder/file.flyde'
            return relativePath;
        }
        return `.${path.sep}${relativePath}`; // e.g., 'folder/file.flyde' -> './folder/file.flyde'
    }

    /**
     * Disposes of the file watcher.
     */
    public dispose() {
        this.disposable.dispose();
        console.log('Flyde FileWatcherService disposed.');
    }
}

Payout & Settlement Details

  • EVM Address: 0x66545035486C8B3b7966c45dc035560F4066aBb5
  • Network: Base (Chain ID 8453) / Arbitrum / Polygon / Ethereum
  • Settlement Asset: USDC
  • Settlement Trigger: Direct on-chain payout upon PR merge

…ving files)

Automated fix and test assertions.

Signed-off-by: Ollie <olliemcgrath@icloud.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Broken imports after moving files

1 participant