Initial commit: OneOS prototype workspace baseline.
Include weekly report and other prototype sources with project tooling config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
633
scripts/sync-project-metadata.mjs
Normal file
633
scripts/sync-project-metadata.mjs
Normal file
@@ -0,0 +1,633 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript';
|
||||
|
||||
import { readServerInfo } from './utils/serverInfo.mjs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export const PROJECT_ID = 'make-project';
|
||||
export const PROJECT_NAME = '';
|
||||
export const PRODUCT_NAME = 'Axhub Make';
|
||||
export const MAKE_CLIENT_MARKER_KIND = 'axhub-make-client';
|
||||
export const MAKE_CLIENT_MARKER_RELATIVE_PATH = '.axhub/make/client.json';
|
||||
export const DEFAULT_CLIENT_ORIGIN = 'http://localhost:51720';
|
||||
export const DETERMINISTIC_UPDATED_AT = '2026-05-03T00:00:00.000Z';
|
||||
|
||||
export const resourceLayout = {
|
||||
prototypes: ['src/prototypes'],
|
||||
docs: ['src/resources'],
|
||||
themes: ['src/themes'],
|
||||
media: ['src/resources/assets'],
|
||||
};
|
||||
|
||||
export const resourceWriteTargets = {
|
||||
prototypes: { type: 'project-relative-path', path: resourceLayout.prototypes[0] },
|
||||
docs: { type: 'project-relative-path', path: resourceLayout.docs[0] },
|
||||
themes: { type: 'project-relative-path', path: resourceLayout.themes[0] },
|
||||
media: { type: 'project-relative-path', path: resourceLayout.media[0] },
|
||||
};
|
||||
|
||||
export const localExportCapabilities = {
|
||||
html: false,
|
||||
make: false,
|
||||
};
|
||||
|
||||
// Snapshot from https://getdesign.md/api/cli/downloads?brands=...
|
||||
const GETDESIGN_DOWNLOAD_SNAPSHOT_DATE = '2026-05-15';
|
||||
const GETDESIGN_THEME_STATS_BY_ID = {
|
||||
'airbnb': { sourceSlug: 'airbnb', downloads: 7111 },
|
||||
'airtable': { sourceSlug: 'airtable', downloads: 3446 },
|
||||
'apple': { sourceSlug: 'apple', downloads: 13995 },
|
||||
'binance': { sourceSlug: 'binance', downloads: 2179 },
|
||||
'bmw': { sourceSlug: 'bmw', downloads: 2917 },
|
||||
'bmw-m': { sourceSlug: 'bmw-m', downloads: 555 },
|
||||
'bugatti': { sourceSlug: 'bugatti', downloads: 1473 },
|
||||
'cal-com': { sourceSlug: 'cal', downloads: 3691 },
|
||||
'claude': { sourceSlug: 'claude', downloads: 10192 },
|
||||
'clay': { sourceSlug: 'clay', downloads: 3383 },
|
||||
'clickhouse': { sourceSlug: 'clickhouse', downloads: 2495 },
|
||||
'cohere': { sourceSlug: 'cohere', downloads: 3083 },
|
||||
'coinbase': { sourceSlug: 'coinbase', downloads: 3399 },
|
||||
'composio': { sourceSlug: 'composio', downloads: 2346 },
|
||||
'cursor': { sourceSlug: 'cursor', downloads: 4650 },
|
||||
'elevenlabs': { sourceSlug: 'elevenlabs', downloads: 3438 },
|
||||
'expo': { sourceSlug: 'expo', downloads: 2457 },
|
||||
'ferrari': { sourceSlug: 'ferrari', downloads: 3158 },
|
||||
'figma': { sourceSlug: 'figma', downloads: 4351 },
|
||||
'framer': { sourceSlug: 'framer', downloads: 3781 },
|
||||
'hashicorp': { sourceSlug: 'hashicorp', downloads: 2498 },
|
||||
'ibm': { sourceSlug: 'ibm', downloads: 3093 },
|
||||
'intercom': { sourceSlug: 'intercom', downloads: 3033 },
|
||||
'june': { sourceSlug: 'june', downloads: 0 },
|
||||
'kraken': { sourceSlug: 'kraken', downloads: 2738 },
|
||||
'lamborghini': { sourceSlug: 'lamborghini', downloads: 2632 },
|
||||
'linear': { sourceSlug: 'linear.app', downloads: 12010 },
|
||||
'lovable': { sourceSlug: 'lovable', downloads: 2907 },
|
||||
'mastercard': { sourceSlug: 'mastercard', downloads: 1275 },
|
||||
'meta': { sourceSlug: 'meta', downloads: 1927 },
|
||||
'minimax': { sourceSlug: 'minimax', downloads: 2742 },
|
||||
'mintlify': { sourceSlug: 'mintlify', downloads: 3271 },
|
||||
'miro': { sourceSlug: 'miro', downloads: 2587 },
|
||||
'mistral-ai': { sourceSlug: 'mistral.ai', downloads: 2481 },
|
||||
'mongodb': { sourceSlug: 'mongodb', downloads: 2399 },
|
||||
'nike': { sourceSlug: 'nike', downloads: 2158 },
|
||||
'notion': { sourceSlug: 'notion', downloads: 11442 },
|
||||
'nvidia': { sourceSlug: 'nvidia', downloads: 2610 },
|
||||
'ollama': { sourceSlug: 'ollama', downloads: 2602 },
|
||||
'opencode': { sourceSlug: 'opencode.ai', downloads: 3033 },
|
||||
'pinterest': { sourceSlug: 'pinterest', downloads: 2837 },
|
||||
'playstation': { sourceSlug: 'playstation', downloads: 1507 },
|
||||
'posthog': { sourceSlug: 'posthog', downloads: 2949 },
|
||||
'renault': { sourceSlug: 'renault', downloads: 2186 },
|
||||
'replicate': { sourceSlug: 'replicate', downloads: 2267 },
|
||||
'revolut': { sourceSlug: 'revolut', downloads: 3289 },
|
||||
'runway': { sourceSlug: 'runwayml', downloads: 2496 },
|
||||
'sanity': { sourceSlug: 'sanity', downloads: 2322 },
|
||||
'sentry': { sourceSlug: 'sentry', downloads: 2949 },
|
||||
'shopify': { sourceSlug: 'shopify', downloads: 2063 },
|
||||
'slack': { sourceSlug: 'slack', downloads: 0 },
|
||||
'spacex': { sourceSlug: 'spacex', downloads: 3053 },
|
||||
'spotify': { sourceSlug: 'spotify', downloads: 4283 },
|
||||
'starbucks': { sourceSlug: 'starbucks', downloads: 1100 },
|
||||
'stripe': { sourceSlug: 'stripe', downloads: 9401 },
|
||||
'supabase': { sourceSlug: 'supabase', downloads: 4044 },
|
||||
'superhuman': { sourceSlug: 'superhuman', downloads: 3186 },
|
||||
'tesla': { sourceSlug: 'tesla', downloads: 3228 },
|
||||
'the-verge': { sourceSlug: 'theverge', downloads: 1508 },
|
||||
'together-ai': { sourceSlug: 'together.ai', downloads: 2352 },
|
||||
'uber': { sourceSlug: 'uber', downloads: 2933 },
|
||||
'vercel': { sourceSlug: 'vercel', downloads: 10946 },
|
||||
'vodafone': { sourceSlug: 'vodafone', downloads: 750 },
|
||||
'voltagent': { sourceSlug: 'voltagent', downloads: 2847 },
|
||||
'warp': { sourceSlug: 'warp', downloads: 2541 },
|
||||
'webflow': { sourceSlug: 'webflow', downloads: 2596 },
|
||||
'wired': { sourceSlug: 'wired', downloads: 1488 },
|
||||
'wise': { sourceSlug: 'wise', downloads: 3274 },
|
||||
'xai': { sourceSlug: 'x.ai', downloads: 2599 },
|
||||
'zapier': { sourceSlug: 'zapier', downloads: 2597 },
|
||||
};
|
||||
|
||||
function toPosix(input) {
|
||||
return String(input || '').replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
function sortById(left, right) {
|
||||
return left.id.localeCompare(right.id);
|
||||
}
|
||||
|
||||
function getThemeStats(themeId) {
|
||||
return GETDESIGN_THEME_STATS_BY_ID[themeId] || null;
|
||||
}
|
||||
|
||||
function sortThemesByGetDesignDownloads(left, right) {
|
||||
const leftStats = getThemeStats(left.id);
|
||||
const rightStats = getThemeStats(right.id);
|
||||
if (leftStats && rightStats) {
|
||||
return rightStats.downloads - leftStats.downloads || sortById(left, right);
|
||||
}
|
||||
if (leftStats) return -1;
|
||||
if (rightStats) return 1;
|
||||
return sortById(left, right);
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stringValue(value) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function isLegacyOfficialProjectName(projectId, name) {
|
||||
return projectId === PROJECT_ID && (name === 'Axhub Make' || name === 'Axhub-Make');
|
||||
}
|
||||
|
||||
export function normalizeMakeClientProjectIdentity(project) {
|
||||
const rawProject = project && typeof project === 'object' && !Array.isArray(project)
|
||||
? project
|
||||
: {};
|
||||
const id = stringValue(rawProject.id) || PROJECT_ID;
|
||||
const name = typeof rawProject.name === 'string' ? rawProject.name.trim() : '';
|
||||
return {
|
||||
id,
|
||||
name: isLegacyOfficialProjectName(id, name) ? '' : name,
|
||||
};
|
||||
}
|
||||
|
||||
const PAGE_ID_RE = /^[a-z0-9-]+$/u;
|
||||
|
||||
function normalizePageId(value) {
|
||||
const id = stringValue(value);
|
||||
return PAGE_ID_RE.test(id) ? id : '';
|
||||
}
|
||||
|
||||
export function readMakeClientProjectIdentity(projectRoot) {
|
||||
const marker = readJson(path.join(projectRoot, MAKE_CLIENT_MARKER_RELATIVE_PATH));
|
||||
const project = marker?.project && typeof marker.project === 'object' && !Array.isArray(marker.project)
|
||||
? marker.project
|
||||
: {};
|
||||
const id = stringValue(project.id);
|
||||
if (marker?.schemaVersion !== 1 || marker?.kind !== MAKE_CLIENT_MARKER_KIND || !id) {
|
||||
return {
|
||||
id: PROJECT_ID,
|
||||
name: PROJECT_NAME,
|
||||
};
|
||||
}
|
||||
return normalizeMakeClientProjectIdentity(project);
|
||||
}
|
||||
|
||||
function writeJsonAtomic(filePath, value) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
||||
try {
|
||||
fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
||||
fs.renameSync(tempPath, filePath);
|
||||
} finally {
|
||||
if (fs.existsSync(tempPath)) {
|
||||
fs.unlinkSync(tempPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function listFiles(rootDir, predicate) {
|
||||
if (!fs.existsSync(rootDir)) return [];
|
||||
const result = [];
|
||||
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
||||
if (entry.name.startsWith('.')) continue;
|
||||
const fullPath = path.join(rootDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
result.push(...listFiles(fullPath, predicate));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && predicate(fullPath)) {
|
||||
result.push(fullPath);
|
||||
}
|
||||
}
|
||||
return result.sort((a, b) => toPosix(a).localeCompare(toPosix(b)));
|
||||
}
|
||||
|
||||
function isIgnoredResourceRelativePath(relativePath) {
|
||||
const normalized = toPosix(relativePath).replace(/^\/+|\/+$/g, '');
|
||||
if (!normalized) return true;
|
||||
if (normalized.toLowerCase() === 'readme.md') return true;
|
||||
return normalized.split('/').some((segment) => segment.startsWith('.'));
|
||||
}
|
||||
|
||||
function readDisplayName(indexFilePath, fallback) {
|
||||
if (!fs.existsSync(indexFilePath)) return fallback;
|
||||
const source = fs.readFileSync(indexFilePath, 'utf8');
|
||||
const displayName = source.match(/@name\s+([^\n]+)/)?.[1]?.replace(/\*\/\s*$/u, '').trim();
|
||||
return displayName || fallback;
|
||||
}
|
||||
|
||||
function getLiteralPropertyValue(objectLiteral, propertyName) {
|
||||
const property = objectLiteral.properties.find((candidate) => (
|
||||
ts.isPropertyAssignment(candidate)
|
||||
&& (
|
||||
(ts.isIdentifier(candidate.name) && candidate.name.text === propertyName)
|
||||
|| (ts.isStringLiteral(candidate.name) && candidate.name.text === propertyName)
|
||||
)
|
||||
));
|
||||
if (!property || !ts.isPropertyAssignment(property)) {
|
||||
return null;
|
||||
}
|
||||
const initializer = property.initializer;
|
||||
return ts.isStringLiteral(initializer) || ts.isNoSubstitutionTemplateLiteral(initializer)
|
||||
? initializer.text.trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
function extractHashRouteFromCall(callExpression) {
|
||||
const expression = callExpression.expression;
|
||||
if (!ts.isIdentifier(expression) || expression.text !== 'defineHashPageRoute') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pagesArg = callExpression.arguments[0];
|
||||
if (!pagesArg || !ts.isArrayLiteralExpression(pagesArg)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pages = [];
|
||||
for (const element of pagesArg.elements) {
|
||||
if (!ts.isObjectLiteralExpression(element)) {
|
||||
continue;
|
||||
}
|
||||
const id = normalizePageId(getLiteralPropertyValue(element, 'id'));
|
||||
const title = stringValue(getLiteralPropertyValue(element, 'title'));
|
||||
if (id && title) {
|
||||
pages.push({ id, title });
|
||||
}
|
||||
}
|
||||
if (!pages.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const optionsArg = callExpression.arguments[1];
|
||||
const requestedDefaultPageId = optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
||||
? normalizePageId(getLiteralPropertyValue(optionsArg, 'defaultPageId'))
|
||||
: '';
|
||||
const defaultPageId = pages.some((page) => page.id === requestedDefaultPageId)
|
||||
? requestedDefaultPageId
|
||||
: pages[0].id;
|
||||
|
||||
return {
|
||||
pages,
|
||||
defaultPageId,
|
||||
};
|
||||
}
|
||||
|
||||
function extractHashRouteFromFile(filePath) {
|
||||
const sourceText = fs.readFileSync(filePath, 'utf8');
|
||||
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, filePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
||||
let route = null;
|
||||
|
||||
const visit = (node) => {
|
||||
if (route) {
|
||||
return;
|
||||
}
|
||||
if (ts.isCallExpression(node)) {
|
||||
route = extractHashRouteFromCall(node);
|
||||
if (route) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
|
||||
visit(sourceFile);
|
||||
return route;
|
||||
}
|
||||
|
||||
function extractHashRouteMetadata(prototypeDir) {
|
||||
const files = listFiles(prototypeDir, (filePath) => /\.(tsx?|mts|cts)$/iu.test(filePath));
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const route = extractHashRouteFromFile(filePath);
|
||||
if (route) {
|
||||
return route;
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed source and leave the prototype without page metadata.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function titleFromMarkdown(markdownPath, fallback) {
|
||||
if (!fs.existsSync(markdownPath)) return fallback;
|
||||
const source = fs.readFileSync(markdownPath, 'utf8');
|
||||
const heading = source.match(/^#\s+(.+)$/mu)?.[1]?.trim();
|
||||
return heading || fallback;
|
||||
}
|
||||
|
||||
function titleFromTokenFile(tokenPath, fallback) {
|
||||
const token = readJson(tokenPath);
|
||||
if (token && typeof token === 'object') {
|
||||
const title = typeof token.title === 'string'
|
||||
? token.title.trim()
|
||||
: typeof token.name === 'string'
|
||||
? token.name.trim()
|
||||
: '';
|
||||
if (title) return title;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeThemeResourceTitle(title, fallback) {
|
||||
const rawTitle = typeof title === 'string' ? title.trim() : '';
|
||||
const fallbackTitle = typeof fallback === 'string' ? fallback.trim() : '';
|
||||
if (!rawTitle) return fallbackTitle;
|
||||
|
||||
const repeatedThemeTitle = rawTitle.match(/^(.+?)\s+主题\s*-\s*(.+)$/u);
|
||||
if (repeatedThemeTitle) {
|
||||
const before = repeatedThemeTitle[1]?.trim();
|
||||
const after = repeatedThemeTitle[2]?.trim();
|
||||
if (before && after && before.toLowerCase() === after.toLowerCase()) {
|
||||
return after;
|
||||
}
|
||||
}
|
||||
|
||||
return rawTitle;
|
||||
}
|
||||
|
||||
function createAxureArtifactMetadata(projectRoot, prototypeName) {
|
||||
const artifactRoot = path.join(projectRoot, '.axhub/make/artifacts/axure', prototypeName);
|
||||
const files = {
|
||||
manifestPath: '.axhub/make/artifacts/axure/{name}/manifest.json',
|
||||
indexBundlePath: '.axhub/make/artifacts/axure/{name}/index-bundle.json',
|
||||
axureJsonPath: '.axhub/make/artifacts/axure/{name}/axure-json.json',
|
||||
coverSvgPath: '.axhub/make/artifacts/axure/{name}/cover.svg',
|
||||
};
|
||||
const existing = {};
|
||||
for (const [key, template] of Object.entries(files)) {
|
||||
const relativePath = template.replace('{name}', prototypeName);
|
||||
if (fs.existsSync(path.join(projectRoot, relativePath))) {
|
||||
existing[key] = relativePath;
|
||||
}
|
||||
}
|
||||
if (!Object.keys(existing).length && !fs.existsSync(artifactRoot)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
axure: {
|
||||
caseId: prototypeName,
|
||||
...existing,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createFigmaArtifactMetadata(projectRoot, prototypeName) {
|
||||
const artifactRoot = path.join(projectRoot, '.axhub/make/artifacts/figma', prototypeName);
|
||||
const files = {
|
||||
canvasFigPath: '.axhub/make/artifacts/figma/{name}/canvas.fig',
|
||||
metaPath: '.axhub/make/artifacts/figma/{name}/meta.json',
|
||||
aiChatPath: '.axhub/make/artifacts/figma/{name}/ai_chat.json',
|
||||
codeManifestPath: '.axhub/make/artifacts/figma/{name}/canvas.code-manifest.json',
|
||||
manifestPath: '.axhub/make/artifacts/figma/{name}/manifest.json',
|
||||
thumbnailPath: '.axhub/make/artifacts/figma/{name}/thumbnail.png',
|
||||
};
|
||||
const existing = {};
|
||||
for (const [key, template] of Object.entries(files)) {
|
||||
const relativePath = template.replace('{name}', prototypeName);
|
||||
if (fs.existsSync(path.join(projectRoot, relativePath))) {
|
||||
existing[key] = relativePath;
|
||||
}
|
||||
}
|
||||
const imagesRelativePath = `.axhub/make/artifacts/figma/${prototypeName}/images`;
|
||||
if (fs.existsSync(path.join(projectRoot, imagesRelativePath))) {
|
||||
existing.imagesDir = imagesRelativePath;
|
||||
}
|
||||
if (!Object.keys(existing).length && !fs.existsSync(artifactRoot)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
figma: {
|
||||
resourceId: prototypeName,
|
||||
...existing,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function readRuntimeEntryKeys(projectRoot) {
|
||||
const manifest = readJson(path.join(projectRoot, '.axhub/make/entries.json'));
|
||||
const items = manifest && typeof manifest === 'object' && manifest.items && typeof manifest.items === 'object'
|
||||
? manifest.items
|
||||
: {};
|
||||
const legacyJs = manifest && typeof manifest === 'object' && manifest.js && typeof manifest.js === 'object'
|
||||
? manifest.js
|
||||
: {};
|
||||
const keys = new Set([
|
||||
...Object.keys(items),
|
||||
...Object.keys(legacyJs),
|
||||
]);
|
||||
|
||||
return Array.from(keys)
|
||||
.map((key) => {
|
||||
const normalizedKey = toPosix(key).replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
const item = items[key];
|
||||
const group = stringValue(item?.group) || normalizedKey.split('/')[0] || '';
|
||||
const name = stringValue(item?.name) || normalizedKey.split('/').slice(1).join('/') || '';
|
||||
if (!normalizedKey || group !== 'prototypes' || !name) {
|
||||
return null;
|
||||
}
|
||||
return { key: normalizedKey, name };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function createRuntimeArtifactMetadata(projectRoot, prototypeName, runtimeEntryKeys, options = {}) {
|
||||
if (options.includeRuntimeArtifacts === false) {
|
||||
return undefined;
|
||||
}
|
||||
const entry = runtimeEntryKeys.find((item) => item.name === prototypeName || item.key === `prototypes/${prototypeName}`);
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
const builtJsPath = `dist/${entry.key}.js`;
|
||||
if (!fs.existsSync(path.join(projectRoot, builtJsPath))) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
runtime: {
|
||||
builtJsPath,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createResourceClientUrl(clientOrigin, resourceKind, resourceName) {
|
||||
const pathname = `/${resourceKind}/${encodeURIComponent(resourceName)}`;
|
||||
const normalizedOrigin = String(clientOrigin || '').trim().replace(/\/+$/u, '');
|
||||
if (!normalizedOrigin) {
|
||||
return pathname;
|
||||
}
|
||||
return `${normalizedOrigin}${pathname}`;
|
||||
}
|
||||
|
||||
function collectPrototypes(projectRoot, clientOrigin, options = {}) {
|
||||
const roots = resourceLayout.prototypes.map((dir) => path.join(projectRoot, dir));
|
||||
const runtimeEntryKeys = readRuntimeEntryKeys(projectRoot);
|
||||
const items = [];
|
||||
for (const root of roots) {
|
||||
if (!fs.existsSync(root)) continue;
|
||||
for (const entry of fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const indexFile = path.join(root, entry.name, 'index.tsx');
|
||||
if (!fs.existsSync(indexFile)) continue;
|
||||
const filePath = toPosix(path.relative(projectRoot, indexFile));
|
||||
const route = extractHashRouteMetadata(path.join(root, entry.name));
|
||||
const item = {
|
||||
id: entry.name,
|
||||
name: entry.name,
|
||||
title: readDisplayName(indexFile, entry.name),
|
||||
clientUrl: createResourceClientUrl(clientOrigin, 'prototypes', entry.name),
|
||||
previewMode: 'clientRuntime',
|
||||
description: '',
|
||||
updatedAt: DETERMINISTIC_UPDATED_AT,
|
||||
filePath,
|
||||
...(options.includeAbsoluteFilePaths === false ? {} : { absoluteFilePath: path.resolve(indexFile) }),
|
||||
...(route ? { pages: route.pages, defaultPageId: route.defaultPageId } : {}),
|
||||
};
|
||||
const artifacts = {
|
||||
...createFigmaArtifactMetadata(projectRoot, entry.name),
|
||||
...createAxureArtifactMetadata(projectRoot, entry.name),
|
||||
...createRuntimeArtifactMetadata(projectRoot, entry.name, runtimeEntryKeys, options),
|
||||
};
|
||||
if (Object.keys(artifacts).length > 0) {
|
||||
item.artifacts = artifacts;
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
return items.sort(sortById);
|
||||
}
|
||||
|
||||
function collectDocs(projectRoot, options = {}) {
|
||||
const docs = [];
|
||||
for (const root of resourceLayout.docs.map((dir) => path.resolve(projectRoot, dir))) {
|
||||
for (const filePath of listFiles(root, () => true)) {
|
||||
const relativePath = toPosix(path.relative(root, filePath));
|
||||
if (isIgnoredResourceRelativePath(relativePath)) continue;
|
||||
const isMarkdown = path.extname(filePath).toLowerCase() === '.md';
|
||||
const id = isMarkdown ? relativePath.replace(/\.md$/iu, '') : relativePath;
|
||||
docs.push({
|
||||
id,
|
||||
name: id,
|
||||
title: isMarkdown
|
||||
? titleFromMarkdown(filePath, path.basename(filePath, '.md'))
|
||||
: relativePath.replace(/\.[^.]+$/u, ''),
|
||||
path: options.includeAbsoluteFilePaths === false
|
||||
? toPosix(path.relative(projectRoot, filePath))
|
||||
: path.resolve(filePath),
|
||||
description: '',
|
||||
updatedAt: DETERMINISTIC_UPDATED_AT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return docs.sort(sortById);
|
||||
}
|
||||
|
||||
function collectThemes(projectRoot, clientOrigin) {
|
||||
const items = [];
|
||||
for (const root of resourceLayout.themes.map((dir) => path.resolve(projectRoot, dir))) {
|
||||
if (!fs.existsSync(root)) continue;
|
||||
for (const entry of fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const indexFile = path.join(root, entry.name, 'index.tsx');
|
||||
if (!fs.existsSync(indexFile)) continue;
|
||||
const tokenPath = path.join(root, entry.name, 'designToken.json');
|
||||
const getdesignStats = getThemeStats(entry.name);
|
||||
const rawTitle = readDisplayName(indexFile, titleFromTokenFile(tokenPath, entry.name));
|
||||
items.push({
|
||||
id: entry.name,
|
||||
name: entry.name,
|
||||
title: normalizeThemeResourceTitle(rawTitle, entry.name),
|
||||
clientUrl: createResourceClientUrl(clientOrigin, 'themes', entry.name),
|
||||
sourcePath: toPosix(path.relative(projectRoot, path.join(root, entry.name))),
|
||||
updatedAt: DETERMINISTIC_UPDATED_AT,
|
||||
...(getdesignStats
|
||||
? {
|
||||
getdesign: {
|
||||
source: 'getdesign.md',
|
||||
sourceSlug: getdesignStats.sourceSlug,
|
||||
downloads: getdesignStats.downloads,
|
||||
snapshotDate: GETDESIGN_DOWNLOAD_SNAPSHOT_DATE,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
return items
|
||||
.sort(sortThemesByGetDesignDownloads)
|
||||
.map((item) => Object.fromEntries(Object.entries(item).filter(([, value]) => value !== undefined)));
|
||||
}
|
||||
|
||||
export function buildMakeProjectMetadata(projectRoot, options = {}) {
|
||||
const clientOrigin = String(options.clientOrigin ?? DEFAULT_CLIENT_ORIGIN).replace(/\/+$/u, '');
|
||||
const projectIdentity = readMakeClientProjectIdentity(projectRoot);
|
||||
const prototypes = collectPrototypes(projectRoot, clientOrigin, options);
|
||||
const docs = collectDocs(projectRoot, options);
|
||||
const themes = collectThemes(projectRoot, clientOrigin);
|
||||
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
project: {
|
||||
id: projectIdentity.id,
|
||||
name: projectIdentity.name,
|
||||
},
|
||||
resources: {
|
||||
prototypes,
|
||||
docs,
|
||||
themes,
|
||||
},
|
||||
navigation: {
|
||||
prototypes: prototypes.map((item) => item.id),
|
||||
docs: docs.map((item) => item.id),
|
||||
},
|
||||
orders: {
|
||||
themes: themes.map((item) => item.id),
|
||||
},
|
||||
capabilities: {
|
||||
quickEdit: true,
|
||||
quickEditMode: 'clientRuntime',
|
||||
figmaExport: true,
|
||||
axureExport: true,
|
||||
localExports: localExportCapabilities,
|
||||
},
|
||||
resourceWriteTargets,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveClientOrigin(projectRoot, fallbackOrigin = DEFAULT_CLIENT_ORIGIN) {
|
||||
const runtime = readServerInfo(projectRoot, 'runtime');
|
||||
return runtime?.origin || fallbackOrigin;
|
||||
}
|
||||
|
||||
export function syncMakeProjectMetadata(projectRoot, options = {}) {
|
||||
const metadata = buildMakeProjectMetadata(projectRoot, {
|
||||
clientOrigin: options.includeRuntimeUrls === true
|
||||
? options.clientOrigin ?? resolveClientOrigin(projectRoot)
|
||||
: '',
|
||||
includeAbsoluteFilePaths: options.includeAbsoluteFilePaths === true,
|
||||
includeRuntimeArtifacts: options.includeRuntimeArtifacts === true,
|
||||
});
|
||||
const metadataPath = path.join(projectRoot, '.axhub/make/project.json');
|
||||
writeJsonAtomic(metadataPath, metadata);
|
||||
return { metadata, metadataPath };
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
|
||||
const appRoot = path.resolve(__dirname, '..');
|
||||
const { metadata, metadataPath } = syncMakeProjectMetadata(appRoot);
|
||||
console.log(`Synced ${metadata.project.name || 'unnamed project'} metadata: ${metadataPath}`);
|
||||
}
|
||||
Reference in New Issue
Block a user