Skip to content

refactor: allow undo redo action with font #1751

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

Merged
merged 9 commits into from
Apr 13, 2025
Merged
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
49 changes: 29 additions & 20 deletions apps/studio/electron/main/assets/fonts/font.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import generate from '@babel/generator';
import { formatContent, readFile, writeFile } from '../../code/files';
import fs from 'fs';
import { extractFontParts, getFontFileName } from '@onlook/utility';
import type { CodeDiff } from '@onlook/models';

/**
* Adds a new font to the project by:
Expand Down Expand Up @@ -129,9 +130,12 @@ export async function addFont(projectRoot: string, font: Font) {
// Generate the new code from the AST
const { code } = generate(ast);

// Write the updated content back to the file
const formattedCode = await formatContent(fontPath, code);
await writeFile(fontPath, formattedCode);
const codeDiff: CodeDiff = {
original: content,
generated: code,
path: fontPath,
};
return codeDiff;
} catch (error) {
console.error('Error adding font:', error);
}
Expand Down Expand Up @@ -278,25 +282,30 @@ export async function removeFont(projectRoot: string, font: Font) {
/import\s+localFont\s+from\s+['"]next\/font\/local['"];\n?/g;
code = code.replace(localFontImportRegex, '');
}
const formattedCode = await formatContent(fontPath, code);
await writeFile(fontPath, formattedCode);
const codeDiff: CodeDiff = {
original: content,
generated: code,
path: fontPath,
};

// Delete font files if found
if (fontFilesToDelete.length > 0) {
for (const fileRelativePath of fontFilesToDelete) {
const absoluteFilePath = pathModule.join(projectRoot, fileRelativePath);
if (fs.existsSync(absoluteFilePath)) {
try {
fs.unlinkSync(absoluteFilePath);
console.log(`Deleted font file: ${absoluteFilePath}`);
} catch (error) {
console.error(`Error deleting font file ${absoluteFilePath}:`, error);
}
} else {
console.log(`Font file not found: ${absoluteFilePath}`);
}
}
}
// if (fontFilesToDelete.length > 0) {
// for (const fileRelativePath of fontFilesToDelete) {
// const absoluteFilePath = pathModule.join(projectRoot, fileRelativePath);
// if (fs.existsSync(absoluteFilePath)) {
// try {
// fs.unlinkSync(absoluteFilePath);
// console.log(`Deleted font file: ${absoluteFilePath}`);
// } catch (error) {
// console.error(`Error deleting font file ${absoluteFilePath}:`, error);
// }
// } else {
// console.log(`Font file not found: ${absoluteFilePath}`);
// }
// }
// }
// Commented out for now—since we have undo/redo functionality for adding/removing fonts, we shouldn’t delete these files to ensure proper rollback support.
return codeDiff;
} else {
console.log(`Font ${fontIdToRemove} not found in font.ts`);
}
Expand Down
27 changes: 20 additions & 7 deletions apps/studio/electron/main/assets/fonts/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
removeFontsFromClassName,
} from './utils';
import type { TraverseCallback } from './types';
import type { CodeDiff } from '@onlook/models/code';

/**
* Traverses JSX elements in a file to find and modify className attributes
Expand All @@ -33,6 +34,7 @@ export async function traverseClassName(

const content = await readFile(filePath);
if (!content) {
console.error(`Failed to read file: ${filePath}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error logging for a missing file read is duplicated here. Consider refactoring the file reading logic into a helper to avoid repetition.

return;
}

Expand Down Expand Up @@ -124,6 +126,7 @@ export async function addFontVariableToElement(

const content = await readFile(filePath);
if (!content) {
console.error(`Failed to read file: ${filePath}`);
return;
}

Expand Down Expand Up @@ -235,6 +238,7 @@ export async function removeFontVariableFromLayout(

const content = await readFile(filePath);
if (!content) {
console.error(`Failed to read file: ${filePath}`);
return;
}

Expand Down Expand Up @@ -293,11 +297,17 @@ export async function updateFontInLayout(
filePath: string,
font: Font,
targetElements: string[],
): Promise<void> {
): Promise<null | CodeDiff> {
let updatedAst = false;
const fontClassName = `font-${font.id}`;
let result = null;

const content = await readFile(filePath);
if (!content) {
console.error(`Failed to read file: ${filePath}`);
return null;
}

await traverseClassName(filePath, targetElements, (classNameAttr, ast) => {
if (t.isStringLiteral(classNameAttr.value)) {
classNameAttr.value = createStringLiteralWithFont(
Expand All @@ -322,13 +332,16 @@ export async function updateFontInLayout(
}
if (updatedAst) {
const { code } = generate(ast);
result = code;
const codeDiff: CodeDiff = {
original: content,
generated: code,
path: filePath,
};
result = codeDiff;
}
});

if (result) {
fs.writeFileSync(filePath, result);
}
return result;
}

/**
Expand Down Expand Up @@ -399,10 +412,10 @@ export async function setDefaultFont(projectRoot: string, font: Font) {

if (routerConfig.type === 'app') {
const layoutPath = pathModule.join(routerConfig.basePath, 'layout.tsx');
await updateFontInLayout(layoutPath, font, ['html']);
return await updateFontInLayout(layoutPath, font, ['html']);
} else {
const appPath = pathModule.join(routerConfig.basePath, '_app.tsx');
await updateFontInLayout(appPath, font, ['div', 'main', 'section', 'body']);
return await updateFontInLayout(appPath, font, ['div', 'main', 'section', 'body']);
}
} catch (error) {
console.error('Error setting default font:', error);
Expand Down
70 changes: 63 additions & 7 deletions apps/studio/electron/main/assets/fonts/watcher.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { subscribe, type AsyncSubscription } from '@parcel/watcher';
import { DefaultSettings } from '@onlook/models/constants';
import { DefaultSettings, MainChannels } from '@onlook/models/constants';
import * as pathModule from 'path';
import { scanFonts } from './scanner';
import fs from 'fs';
Expand All @@ -8,16 +8,20 @@ import { removeFontVariableFromLayout } from './layout';
import { removeFontFromTailwindConfig, updateTailwindFontConfig } from './tailwind';
import type { Font } from '@onlook/models/assets';
import { detectRouterType } from '../../pages';
import { mainWindow } from '../../index';

export class FontFileWatcher {
private subscription: AsyncSubscription | null = null;
private previousFonts: Font[] = [];
private selfModified: Set<string> = new Set();
Copy link
Contributor

@Kitenite Kitenite Apr 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think eventually we want a generic file watcher that any service can subscribe to instead of having the same pattern duplicated in multiple places. Out of scope for now.


async watch(projectRoot: string) {
await this.clearSubscription();

const fontPath = pathModule.resolve(projectRoot, DefaultSettings.FONT_CONFIG);
const fontDir = pathModule.dirname(fontPath);
const _fontPath = pathModule.resolve(projectRoot, DefaultSettings.FONT_CONFIG);
const fontDir = pathModule.dirname(_fontPath);
let _layoutPath: string = '';
let _appPath: string = '';

if (!fs.existsSync(fontDir)) {
console.error(`Font directory does not exist: ${fontDir}`);
Expand All @@ -30,6 +34,15 @@ export class FontFileWatcher {
this.previousFonts = [];
}

const routerConfig = await detectRouterType(projectRoot);
if (routerConfig) {
if (routerConfig.type === 'app') {
_layoutPath = pathModule.join(routerConfig.basePath, 'layout.tsx');
} else {
_appPath = pathModule.join(routerConfig.basePath, '_app.tsx');
}
}

try {
this.subscription = await subscribe(
fontDir,
Expand All @@ -42,10 +55,16 @@ export class FontFileWatcher {
if (events.length > 0) {
for (const event of events) {
const eventPath = pathModule.normalize(event.path);
const expectedPath = pathModule.normalize(fontPath);
const fontPath = pathModule.normalize(_fontPath);
const layoutPath = pathModule.normalize(_layoutPath);
const appPath = pathModule.normalize(_appPath);
if (this.selfModified.has(eventPath)) {
this.selfModified.delete(eventPath);
continue;
}

if (
(eventPath === expectedPath ||
(eventPath === fontPath ||
eventPath.endsWith(DefaultSettings.FONT_CONFIG)) &&
(event.type === 'update' || event.type === 'create')
) {
Expand All @@ -55,6 +74,17 @@ export class FontFileWatcher {
console.error('Error syncing fonts with configs:', error);
}
}
if (
(eventPath === layoutPath || eventPath === appPath) &&
(event.type === 'update' || event.type === 'create')
) {
this.selfModified.add(eventPath);
try {
mainWindow?.webContents.send(MainChannels.GET_DEFAULT_FONT);
} catch (error) {
console.error('Error syncing fonts with configs:', error);
}
}
}
}
},
Expand All @@ -78,17 +108,19 @@ export class FontFileWatcher {
);

const addedFonts = currentFonts.filter(
(currFont) => !this.previousFonts.some((prevFont) => prevFont.id === currFont.id),
(currFont) => !this.previousFonts.some((prevFont) => currFont.id === prevFont.id),
);

for (const font of removedFonts) {
const routerConfig = await detectRouterType(projectRoot);
if (routerConfig) {
if (routerConfig.type === 'app') {
const layoutPath = pathModule.join(routerConfig.basePath, 'layout.tsx');
this.selfModified.add(layoutPath);
await removeFontVariableFromLayout(layoutPath, font.id, ['html']);
} else {
const appPath = pathModule.join(routerConfig.basePath, '_app.tsx');
this.selfModified.add(appPath);
await removeFontVariableFromLayout(appPath, font.id, [
'div',
'main',
Expand All @@ -98,16 +130,40 @@ export class FontFileWatcher {
}
}

const tailwindConfigPath = pathModule.join(projectRoot, 'tailwind.config.ts');
this.selfModified.add(tailwindConfigPath);
await removeFontFromTailwindConfig(projectRoot, font);
}

if (addedFonts.length > 0) {
for (const font of addedFonts) {
const tailwindConfigPath = pathModule.join(projectRoot, 'tailwind.config.ts');
this.selfModified.add(tailwindConfigPath);
await updateTailwindFontConfig(projectRoot, font);
await addFontVariableToLayout(projectRoot, font.id);

const routerConfig = await detectRouterType(projectRoot);
if (routerConfig) {
if (routerConfig.type === 'app') {
const layoutPath = pathModule.join(routerConfig.basePath, 'layout.tsx');
this.selfModified.add(layoutPath);
await addFontVariableToLayout(projectRoot, font.id);
} else {
const appPath = pathModule.join(routerConfig.basePath, '_app.tsx');
this.selfModified.add(appPath);
await addFontVariableToLayout(projectRoot, font.id);
}
}
}
}

if (removedFonts.length > 0 || addedFonts.length > 0) {
mainWindow?.webContents.send(MainChannels.FONTS_CHANGED, {
currentFonts,
removedFonts,
addedFonts,
});
}

this.previousFonts = currentFonts;
} catch (error) {
console.error('Error syncing fonts:', error);
Expand Down
Loading