Skip to content

feat: Implement memfs for file system management #2281

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions apps/web/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"freestyle-sandboxes": "^0.0.78",
"localforage": "^1.10.0",
"lucide-react": "^0.486.0",
"memfs": "^4.17.2",
"mobx-react-lite": "^4.1.0",
"motion": "^12.6.3",
"next": "^15.2.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ export const useFolder = () => {

await editorEngine.sandbox.rename(oldPath, newPath);

editorEngine.image.scanImages();

setRenameState({
folderToRename: null,
newFolderName: '',
Expand Down Expand Up @@ -149,8 +147,6 @@ export const useFolder = () => {

await editorEngine.sandbox.delete(folderPath, true);

editorEngine.image.scanImages();

setDeleteState({
folderToDelete: null,
isLoading: false,
Expand Down Expand Up @@ -209,9 +205,6 @@ export const useFolder = () => {
}
await editorEngine.sandbox.rename(oldPath, newPath);

// Refresh images
editorEngine.image.scanImages();

setMoveState({
folderToMove: null,
targetFolder: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useEditorEngine } from '@/components/store/editor';
import { createContext, useContext, useMemo, type ReactNode, useCallback, useState, useEffect } from 'react';
import { useCleanupOnPageChange } from '@/hooks/use-subscription-cleanup';
import { DefaultSettings } from '@onlook/constants';
import { isImageFile } from '@onlook/utility';
import { observer } from 'mobx-react-lite';
import { useImageUpload } from '../hooks/use-image-upload';
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
import { useFolder } from '../hooks/use-folder';
import { useImageDelete } from '../hooks/use-image-delete';
import { useImageRename } from '../hooks/use-image-rename';
import { useImageMove } from '../hooks/use-image-move';
import { useImageRename } from '../hooks/use-image-rename';
import { useImageUpload } from '../hooks/use-image-upload';
import type { FolderNode } from './types';
import { useFolder } from '../hooks/use-folder';
import { DefaultSettings } from '@onlook/constants';

interface ImagesContextValue {
folderStructure: FolderNode;
Expand All @@ -30,7 +32,7 @@ interface ImagesProviderProps {

export const ImagesProvider = observer(({ children }: ImagesProviderProps) => {
const editorEngine = useEditorEngine();

const { addSubscription } = useCleanupOnPageChange();

const deleteOperations = useImageDelete();
const renameOperations = useImageRename();
Expand Down Expand Up @@ -91,11 +93,12 @@ export const ImagesProvider = observer(({ children }: ImagesProviderProps) => {
setFolderStructure(baseFolderStructure);
}, [baseFolderStructure]);


const triggerFolderStructureUpdate = useCallback(() => {
setFolderStructure(prev => ({ ...prev }));
}, []);

const isOperating =
const isOperating =
deleteOperations.deleteState.isLoading ||
renameOperations.renameState.isLoading ||
uploadOperations.uploadState.isUploading ||
Expand All @@ -118,6 +121,15 @@ export const ImagesProvider = observer(({ children }: ImagesProviderProps) => {
},
};

useEffect(() => {
const unsubscribe = editorEngine.sandbox.fileEventBus.subscribe('*', (event) => {
if (event.paths && event.paths[0] && isImageFile(event.paths[0])) {
editorEngine.image.scanImages();
}
});
addSubscription('image-manager', unsubscribe);
}, [editorEngine.sandbox.fileEventBus]);

return <ImagesContext.Provider value={value}>{children}</ImagesContext.Provider>;
});

Expand Down
62 changes: 42 additions & 20 deletions apps/web/client/src/components/store/editor/font/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"use client";
'use client';

import type { ParseResult } from '@babel/parser';
import * as t from '@babel/types';
Expand Down Expand Up @@ -82,6 +82,7 @@ export class FontManager {
private _allFontFamilies: RawFont[] = FAMILIES as RawFont[];
private disposers: Array<() => void> = [];
private previousFonts: Font[] = [];
private fontConfigFileWatcher: (() => void) | null = null;

private _fontConfigPath: string | null = null;
tailwindConfigPath = normalizePath(DefaultSettings.TAILWIND_CONFIG);
Expand All @@ -95,9 +96,7 @@ export class FontManager {
return this._fontConfigPath;
}

constructor(
private editorEngine: EditorEngine,
) {
constructor(private editorEngine: EditorEngine) {
makeAutoObservable(this);

// Initialize FlexSearch index
Expand Down Expand Up @@ -131,21 +130,9 @@ export class FontManager {
if (session) {
await this.updateFontConfigPath();
this.loadInitialFonts();
}
},
{ fireImmediately: true },
);

const fontConfigDisposer = reaction(
() =>
this.editorEngine.state.brandTab === BrandTabValue.FONTS &&
this.editorEngine.sandbox?.readFile(this.fontConfigPath),
async (contentPromise) => {
if (contentPromise) {
const content = await contentPromise;
if (content) {
this.syncFontsWithConfigs();
}
this.setupFontConfigFileWatcher();
} else {
this.cleanupFontConfigFileWatcher();
}
},
{ fireImmediately: true },
Expand Down Expand Up @@ -177,7 +164,7 @@ export class FontManager {
{ fireImmediately: true },
);

this.disposers.push(sandboxDisposer, fontConfigDisposer, defaultFontDisposer);
this.disposers.push(sandboxDisposer, defaultFontDisposer);
}

private convertFont(font: RawFont): Font {
Expand Down Expand Up @@ -802,6 +789,9 @@ export class FontManager {
this._currentFontIndex = 0;
this._isFetching = false;

// Clean up file watcher
this.cleanupFontConfigFileWatcher();

// Clean up all reactions
this.disposers.forEach((disposer) => disposer());
this.disposers = [];
Expand Down Expand Up @@ -1532,4 +1522,36 @@ export class FontManager {
console.error(`Error traversing className in ${filePath}:`, error);
}
}

/**
* Sets up file watcher for the font config file
*/
private setupFontConfigFileWatcher(): void {
this.cleanupFontConfigFileWatcher();

const sandbox = this.editorEngine.sandbox;
if (!sandbox || this.editorEngine.state.brandTab !== BrandTabValue.FONTS) {
return;
}

this.fontConfigFileWatcher = sandbox.fileEventBus.subscribe('*', async (event) => {
const normalizedFontConfigPath = normalizePath(this.fontConfigPath);
const affectsFont = event.paths.some(
(path) => normalizePath(path) === normalizedFontConfigPath,
);

if (!affectsFont) {
return;
}

await this.syncFontsWithConfigs();
});
}

private cleanupFontConfigFileWatcher(): void {
if (this.fontConfigFileWatcher) {
this.fontConfigFileWatcher();
this.fontConfigFileWatcher = null;
}
}
}
24 changes: 6 additions & 18 deletions apps/web/client/src/components/store/editor/image/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,8 @@ export class ImageManager {
if (!isIndexingFiles) {
this.scanImages();
}
}
);

reaction(
() => this.editorEngine.sandbox.listBinaryFiles(DefaultSettings.IMAGE_FOLDER),
() => {
this.scanImages();
}
},
);

}

async upload(file: File, destinationFolder: string): Promise<void> {
Expand All @@ -50,7 +42,6 @@ export class ImageManager {
async delete(originPath: string): Promise<void> {
try {
await this.editorEngine.sandbox.delete(originPath);
this.scanImages();
} catch (error) {
console.error('Error deleting image:', error);
throw error;
Expand All @@ -62,7 +53,6 @@ export class ImageManager {
const basePath = getDirName(originPath);
const newPath = `${basePath}/${newName}`;
await this.editorEngine.sandbox.rename(originPath, newPath);
this.scanImages();
} catch (error) {
console.error('Error renaming image:', error);
throw error;
Expand Down Expand Up @@ -150,9 +140,7 @@ export class ImageManager {
this._isScanning = true;

try {
const files = this.editorEngine.sandbox.listBinaryFiles(
DefaultSettings.IMAGE_FOLDER,
);
const files = this.editorEngine.sandbox.listBinaryFiles(DefaultSettings.IMAGE_FOLDER);

if (files.length === 0) {
this.images = [];
Expand All @@ -161,13 +149,11 @@ export class ImageManager {

const imageFiles = files.filter((filePath: string) => isImageFile(filePath));


if (imageFiles.length === 0) {
return;
}

this.images = imageFiles;

} catch (error) {
console.error('Error scanning images:', error);
this.images = [];
Expand Down Expand Up @@ -227,11 +213,13 @@ export class ImageManager {

try {
// Process all images in parallel
const imagePromises = imagePaths.map(path => this.readImageContent(path));
const imagePromises = imagePaths.map((path) => this.readImageContent(path));
const results = await Promise.all(imagePromises);

// Filter out null results
const validImages = results.filter((result): result is ImageContentData => result !== null);
const validImages = results.filter(
(result): result is ImageContentData => result !== null,
);

return validImages;
} catch (error) {
Expand Down
Loading