Skip to content

Commit 47321cd

Browse files
feat: update metadata of page (#1766)
* handle image * add scan project metadata * refactor type * update scan pages * refactor duplicated upload image * fetch assets * handle use client case * handle page URL * update default title and default description * remove log * revert files * Update scan on settings modal open * Clean up from template * Update gitignore * Fix build error and rename Metadata to PageMetadata * remove default mime type * add default mime type * display upload button in favicon * update base url
1 parent 96bde6a commit 47321cd

File tree

22 files changed

+1510
-59
lines changed

22 files changed

+1510
-59
lines changed

apps/studio/electron/main/assets/images.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,7 @@ async function scanImagesDirectory(projectRoot: string): Promise<ImageContentDat
2020
) {
2121
const imagePath = path.join(imagesPath, entry.name);
2222
const image = readFileSync(imagePath, { encoding: 'base64' });
23-
const mimeType = mime.getType(imagePath);
24-
if (!mimeType) {
25-
console.error(`Failed to get mime type for ${imagePath}`);
26-
continue;
27-
}
23+
const mimeType = mime.getType(imagePath) || 'application/octet-stream';
2824
images.push({
2925
fileName: entry.name,
3026
content: `data:${mimeType};base64,${image}`,
@@ -50,26 +46,44 @@ export async function scanNextJsImages(projectRoot: string): Promise<ImageConten
5046
}
5147
}
5248

53-
export async function saveImageToProject(
54-
projectFolder: string,
55-
content: string,
56-
fileName: string,
57-
): Promise<string> {
58-
try {
59-
const imageFolder = path.join(projectFolder, DefaultSettings.IMAGE_FOLDER);
60-
const imagePath = path.join(imageFolder, fileName);
49+
async function getUniqueFileName(imageFolder: string, fileName: string): Promise<string> {
50+
let imagePath = path.join(imageFolder, fileName);
51+
let counter = 1;
52+
53+
const fileExt = path.extname(fileName);
54+
const baseName = path.basename(fileName, fileExt);
6155

56+
// Keep trying until we find a unique name
57+
while (true) {
6258
try {
6359
await fs.access(imagePath);
64-
throw new Error(`File ${fileName} already exists`);
60+
// If file exists, try with a new suffix
61+
const newFileName = `${baseName} (${counter})${fileExt}`;
62+
imagePath = path.join(imageFolder, newFileName);
63+
counter++;
6564
} catch (err: any) {
6665
if (err.code === 'ENOENT') {
67-
const buffer = Buffer.from(content, 'base64');
68-
await fs.writeFile(imagePath, buffer);
69-
return imagePath;
66+
// File doesn't exist, we can use this path
67+
return path.basename(imagePath);
7068
}
7169
throw err;
7270
}
71+
}
72+
}
73+
74+
export async function saveImageToProject(
75+
projectFolder: string,
76+
content: string,
77+
fileName: string,
78+
): Promise<string> {
79+
try {
80+
const imageFolder = path.join(projectFolder, DefaultSettings.IMAGE_FOLDER);
81+
const uniqueFileName = await getUniqueFileName(imageFolder, fileName);
82+
const imagePath = path.join(imageFolder, uniqueFileName);
83+
84+
const buffer = Buffer.from(content, 'base64');
85+
await fs.writeFile(imagePath, buffer);
86+
return imagePath;
7387
} catch (error) {
7488
console.error('Error uploading image:', error);
7589
throw error;

apps/studio/electron/main/events/code.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,30 @@ import type { CodeDiff, CodeDiffRequest } from '@onlook/models/code';
22
import { MainChannels } from '@onlook/models/constants';
33
import type { TemplateNode } from '@onlook/models/element';
44
import { ipcMain } from 'electron';
5+
import {
6+
addFont,
7+
addLocalFont,
8+
getDefaultFont,
9+
removeFont,
10+
scanFonts,
11+
setDefaultFont,
12+
} from '../assets/fonts/index';
13+
import { FontFileWatcher } from '../assets/fonts/watcher';
14+
import {
15+
deleteTailwindColorGroup,
16+
scanTailwindConfig,
17+
updateTailwindColorConfig,
18+
} from '../assets/styles';
519
import { openFileInIde, openInIde, pickDirectory, readCodeBlock, writeCode } from '../code/';
620
import { getTemplateNodeClass } from '../code/classes';
721
import { extractComponentsFromDirectory } from '../code/components';
822
import { getCodeDiffs } from '../code/diff';
923
import { isChildTextEditable } from '../code/diff/text';
1024
import { readFile } from '../code/files';
25+
import { getTemplateNodeProps } from '../code/props';
1126
import { getTemplateNodeChild } from '../code/templateNode';
1227
import runManager from '../run';
1328
import { getFileContentWithoutIds } from '../run/cleanup';
14-
import { getTemplateNodeProps } from '../code/props';
15-
import {
16-
scanTailwindConfig,
17-
updateTailwindColorConfig,
18-
deleteTailwindColorGroup,
19-
} from '../assets/styles';
20-
import {
21-
addFont,
22-
removeFont,
23-
scanFonts,
24-
setDefaultFont,
25-
getDefaultFont,
26-
addLocalFont,
27-
} from '../assets/fonts/index';
28-
import { FontFileWatcher } from '../assets/fonts/watcher';
2929

3030
const fontFileWatcher = new FontFileWatcher();
3131

apps/studio/electron/main/events/page.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import type { PageMetadata } from '@onlook/models';
12
import { MainChannels } from '@onlook/models/constants';
23
import { ipcMain } from 'electron';
4+
import path from 'path';
35
import {
46
createNextJsPage,
57
deleteNextJsPage,
8+
detectRouterType,
69
duplicateNextJsPage,
10+
extractMetadata,
711
renameNextJsPage,
812
scanNextJsPages,
913
} from '../pages';
14+
import { updateNextJsPage } from '../pages/update';
1015

1116
export function listenForPageMessages() {
1217
ipcMain.handle(MainChannels.SCAN_PAGES, async (_event, projectRoot: string) => {
@@ -62,4 +67,34 @@ export function listenForPageMessages() {
6267
return await duplicateNextJsPage(projectRoot, sourcePath, targetPath);
6368
},
6469
);
70+
71+
ipcMain.handle(
72+
MainChannels.UPDATE_PAGE_METADATA,
73+
async (
74+
_event,
75+
{
76+
projectRoot,
77+
pagePath,
78+
metadata,
79+
}: { projectRoot: string; pagePath: string; metadata: PageMetadata },
80+
) => {
81+
return await updateNextJsPage(projectRoot, pagePath, metadata);
82+
},
83+
);
84+
85+
ipcMain.handle(
86+
MainChannels.SCAN_PROJECT_METADATA,
87+
async (_event, { projectRoot }: { projectRoot: string }) => {
88+
const routerConfig = await detectRouterType(projectRoot);
89+
if (routerConfig) {
90+
if (routerConfig.type === 'app') {
91+
const layoutPath = path.join(routerConfig.basePath, 'layout.tsx');
92+
return await extractMetadata(layoutPath);
93+
} else {
94+
return;
95+
}
96+
}
97+
return null;
98+
},
99+
);
65100
}

apps/studio/electron/main/pages/scan.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { PageNode } from '@onlook/models/pages';
1+
import { parse } from '@babel/parser';
2+
import traverse from '@babel/traverse';
3+
import * as t from '@babel/types';
4+
import type { PageMetadata, PageNode } from '@onlook/models/pages';
25
import { promises as fs } from 'fs';
36
import { nanoid } from 'nanoid';
47
import * as path from 'path';
@@ -10,6 +13,87 @@ import {
1013
ROOT_PATH_IDENTIFIERS,
1114
} from './helpers';
1215

16+
export async function extractMetadata(filePath: string): Promise<PageMetadata | undefined> {
17+
try {
18+
const content = await fs.readFile(filePath, 'utf-8');
19+
20+
// Parse the file content using Babel
21+
const ast = parse(content, {
22+
sourceType: 'module',
23+
plugins: ['typescript', 'jsx'],
24+
});
25+
26+
let metadata: PageMetadata | undefined;
27+
28+
// Traverse the AST to find metadata export
29+
traverse(ast, {
30+
ExportNamedDeclaration(path) {
31+
const declaration = path.node.declaration;
32+
if (t.isVariableDeclaration(declaration)) {
33+
const declarator = declaration.declarations[0];
34+
if (
35+
t.isIdentifier(declarator.id) &&
36+
declarator.id.name === 'metadata' &&
37+
t.isObjectExpression(declarator.init)
38+
) {
39+
metadata = {};
40+
// Extract properties from the object expression
41+
for (const prop of declarator.init.properties) {
42+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
43+
const key = prop.key.name;
44+
if (t.isStringLiteral(prop.value)) {
45+
(metadata as any)[key] = prop.value.value;
46+
} else if (t.isObjectExpression(prop.value)) {
47+
(metadata as any)[key] = extractObjectValue(prop.value);
48+
} else if (t.isArrayExpression(prop.value)) {
49+
(metadata as any)[key] = extractArrayValue(prop.value);
50+
}
51+
}
52+
}
53+
}
54+
}
55+
},
56+
});
57+
58+
return metadata;
59+
} catch (error) {
60+
console.error(`Error reading metadata from ${filePath}:`, error);
61+
return undefined;
62+
}
63+
}
64+
65+
function extractObjectValue(obj: t.ObjectExpression): any {
66+
const result: any = {};
67+
for (const prop of obj.properties) {
68+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
69+
const key = prop.key.name;
70+
if (t.isStringLiteral(prop.value)) {
71+
result[key] = prop.value.value;
72+
} else if (t.isObjectExpression(prop.value)) {
73+
result[key] = extractObjectValue(prop.value);
74+
} else if (t.isArrayExpression(prop.value)) {
75+
result[key] = extractArrayValue(prop.value);
76+
}
77+
}
78+
}
79+
return result;
80+
}
81+
82+
function extractArrayValue(arr: t.ArrayExpression): any[] {
83+
return arr.elements
84+
.map((element) => {
85+
if (t.isStringLiteral(element)) {
86+
return element.value;
87+
} else if (t.isObjectExpression(element)) {
88+
return extractObjectValue(element);
89+
} else if (t.isArrayExpression(element)) {
90+
return extractArrayValue(element);
91+
}
92+
return null;
93+
})
94+
.filter(Boolean);
95+
}
96+
1397
async function scanAppDirectory(dir: string, parentPath: string = ''): Promise<PageNode[]> {
1498
const nodes: PageNode[] = [];
1599
const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -38,6 +122,28 @@ async function scanAppDirectory(dir: string, parentPath: string = ''): Promise<P
38122
cleanPath = '/' + cleanPath.replace(/^\/|\/$/g, '');
39123

40124
const isRoot = ROOT_PATH_IDENTIFIERS.includes(cleanPath);
125+
126+
// Extract metadata from both page and layout files
127+
const pageMetadata = await extractMetadata(path.join(dir, pageFile.name));
128+
129+
// Look for layout file in the same directory
130+
const layoutFile = entries.find(
131+
(entry) =>
132+
entry.isFile() &&
133+
entry.name.startsWith('layout.') &&
134+
ALLOWED_EXTENSIONS.includes(path.extname(entry.name)),
135+
);
136+
137+
const layoutMetadata = layoutFile
138+
? await extractMetadata(path.join(dir, layoutFile.name))
139+
: undefined;
140+
141+
// Merge metadata, with page metadata taking precedence over layout metadata
142+
const metadata = {
143+
...layoutMetadata,
144+
...pageMetadata,
145+
};
146+
41147
nodes.push({
42148
id: nanoid(),
43149
name: isDynamicRoute
@@ -49,6 +155,7 @@ async function scanAppDirectory(dir: string, parentPath: string = ''): Promise<P
49155
children: [],
50156
isActive: false,
51157
isRoot,
158+
metadata,
52159
});
53160
}
54161

@@ -110,6 +217,9 @@ async function scanPagesDirectory(dir: string, parentPath: string = ''): Promise
110217

111218
const isRoot = ROOT_PATH_IDENTIFIERS.includes(cleanPath);
112219

220+
// Extract metadata from the page file
221+
const metadata = await extractMetadata(path.join(dir, entry.name));
222+
113223
nodes.push({
114224
id: nanoid(),
115225
name:
@@ -122,6 +232,7 @@ async function scanPagesDirectory(dir: string, parentPath: string = ''): Promise
122232
children: [],
123233
isActive: false,
124234
isRoot,
235+
metadata,
125236
});
126237
}
127238
}

0 commit comments

Comments
 (0)