Skip to content

feat: update metadata of page #1766

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 21 commits into from
Apr 13, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
31 changes: 31 additions & 0 deletions apps/studio/electron/main/events/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { ipcMain } from 'electron';
import {
createNextJsPage,
deleteNextJsPage,
detectRouterType,
duplicateNextJsPage,
renameNextJsPage,
scanNextJsPages,
} from '../pages';
import type { Metadata } from '@onlook/models';
import { updateNextJsPage } from '../pages/update';
import path from 'path';

export function listenForPageMessages() {
ipcMain.handle(MainChannels.SCAN_PAGES, async (_event, projectRoot: string) => {
Expand Down Expand Up @@ -62,4 +66,31 @@ export function listenForPageMessages() {
return await duplicateNextJsPage(projectRoot, sourcePath, targetPath);
},
);

ipcMain.handle(
MainChannels.UPDATE_PAGE_METADATA,
async (
_event,
{
projectRoot,
pagePath,
metadata,
isRoot,
}: { projectRoot: string; pagePath: string; metadata: Metadata; isRoot: boolean },
) => {
let fullPath = pagePath;
const routerConfig = await detectRouterType(projectRoot);
if (routerConfig) {
if (routerConfig.type === 'app') {
if (!isRoot) {
fullPath = path.join(fullPath, 'page.tsx');
}
} else {
return;
}
}

return await updateNextJsPage(projectRoot, fullPath, metadata);
},
);
}
96 changes: 95 additions & 1 deletion apps/studio/electron/main/pages/scan.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { PageNode } from '@onlook/models/pages';
import type { Metadata, PageNode } from '@onlook/models/pages';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import * as t from '@babel/types';
import { promises as fs } from 'fs';
import { nanoid } from 'nanoid';
import * as path from 'path';
Expand All @@ -10,6 +13,87 @@ import {
ROOT_PATH_IDENTIFIERS,
} from './helpers';

async function extractMetadata(filePath: string): Promise<Metadata | undefined> {
try {
const content = await fs.readFile(filePath, 'utf-8');

// Parse the file content using Babel
const ast = parse(content, {
sourceType: 'module',
plugins: ['typescript', 'jsx'],
});

let metadata: Metadata | undefined;

// Traverse the AST to find metadata export
traverse(ast, {
ExportNamedDeclaration(path) {
const declaration = path.node.declaration;
if (t.isVariableDeclaration(declaration)) {
const declarator = declaration.declarations[0];
if (
t.isIdentifier(declarator.id) &&
declarator.id.name === 'metadata' &&
t.isObjectExpression(declarator.init)
) {
metadata = {};
// Extract properties from the object expression
for (const prop of declarator.init.properties) {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
const key = prop.key.name;
if (t.isStringLiteral(prop.value)) {
(metadata as any)[key] = prop.value.value;
} else if (t.isObjectExpression(prop.value)) {
(metadata as any)[key] = extractObjectValue(prop.value);
} else if (t.isArrayExpression(prop.value)) {
(metadata as any)[key] = extractArrayValue(prop.value);
}
}
}
}
}
},
});

return metadata;
} catch (error) {
console.error(`Error reading metadata from ${filePath}:`, error);
return undefined;
}
}

function extractObjectValue(obj: t.ObjectExpression): any {
const result: any = {};
for (const prop of obj.properties) {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
const key = prop.key.name;
if (t.isStringLiteral(prop.value)) {
result[key] = prop.value.value;
} else if (t.isObjectExpression(prop.value)) {
result[key] = extractObjectValue(prop.value);
} else if (t.isArrayExpression(prop.value)) {
result[key] = extractArrayValue(prop.value);
}
}
}
return result;
}

function extractArrayValue(arr: t.ArrayExpression): any[] {
return arr.elements
.map((element) => {
if (t.isStringLiteral(element)) {
return element.value;
} else if (t.isObjectExpression(element)) {
return extractObjectValue(element);
} else if (t.isArrayExpression(element)) {
return extractArrayValue(element);
}
return null;
})
.filter(Boolean);
}

async function scanAppDirectory(dir: string, parentPath: string = ''): Promise<PageNode[]> {
const nodes: PageNode[] = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
Expand Down Expand Up @@ -38,6 +122,11 @@ async function scanAppDirectory(dir: string, parentPath: string = ''): Promise<P
cleanPath = '/' + cleanPath.replace(/^\/|\/$/g, '');

const isRoot = ROOT_PATH_IDENTIFIERS.includes(cleanPath);

// Extract metadata from the page file
const metadata = await extractMetadata(path.join(dir, pageFile.name));
console.log(metadata);
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove debug console.log before production.

Suggested change
console.log(metadata);


nodes.push({
id: nanoid(),
name: isDynamicRoute
Expand All @@ -49,6 +138,7 @@ async function scanAppDirectory(dir: string, parentPath: string = ''): Promise<P
children: [],
isActive: false,
isRoot,
metadata,
});
}

Expand Down Expand Up @@ -110,6 +200,9 @@ async function scanPagesDirectory(dir: string, parentPath: string = ''): Promise

const isRoot = ROOT_PATH_IDENTIFIERS.includes(cleanPath);

// Extract metadata from the page file
const metadata = await extractMetadata(path.join(dir, entry.name));

nodes.push({
id: nanoid(),
name:
Expand All @@ -122,6 +215,7 @@ async function scanPagesDirectory(dir: string, parentPath: string = ''): Promise
children: [],
isActive: false,
isRoot,
metadata,
});
}
}
Expand Down
Loading