/** * 将 .axhub/make/sidebar-tree.json 左侧菜单同步到各原型 annotation-source.json 的「原型目录」。 * * 用法: * node scripts/sync-project-prototype-directory.mjs * node scripts/sync-project-prototype-directory.mjs --prototype vehicle-management */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const NAV_FOLDER_ID = 'oneos-project-nav'; function parseArgs(argv) { const args = { prototype: '', projectRoot: '' }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--prototype' && argv[i + 1]) { args.prototype = argv[++i].trim(); } else if (arg === '--project-root' && argv[i + 1]) { args.projectRoot = path.resolve(argv[++i].trim()); } } return args; } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function writeJson(filePath, value) { fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8'); } function prototypeIdFromItemKey(itemKey) { const normalized = String(itemKey || '').trim().replace(/^\/+/u, ''); const match = normalized.match(/^prototypes\/(.+)$/u); return match ? match[1] : ''; } function navLinkId(prototypeId) { return `nav-link-${prototypeId.replace(/[^a-zA-Z0-9]+/gu, '-').replace(/^-+|-+$/gu, '').toLowerCase()}`; } function createLinkNode(sidebarItem, currentPrototypeId) { const prototypeId = prototypeIdFromItemKey(sidebarItem.itemKey); if (!prototypeId) return null; const isCurrent = prototypeId === currentPrototypeId; return { type: 'link', id: navLinkId(prototypeId), title: isCurrent ? `${sidebarItem.title}(当前)` : sidebarItem.title, href: `/prototypes/${prototypeId}`, target: 'self', }; } function sidebarItemsToDirectoryChildren(sidebarItems, currentPrototypeId) { const children = []; for (const item of sidebarItems || []) { if (item?.kind === 'item') { const link = createLinkNode(item, currentPrototypeId); if (link) children.push(link); continue; } if (item?.kind !== 'folder' || !Array.isArray(item.children)) { continue; } const folderChildren = item.children .filter((child) => child?.kind === 'item') .map((child) => createLinkNode(child, currentPrototypeId)) .filter(Boolean); if (!folderChildren.length) continue; children.push({ type: 'folder', id: item.id ? `nav-${item.id}` : `nav-folder-${children.length + 1}`, title: item.title || '分组', defaultExpanded: item.defaultExpanded !== false, children: folderChildren, }); } return children; } function loadSidebarPrototypes(projectRoot) { const sidebarPath = path.join(projectRoot, '.axhub/make/sidebar-tree.json'); if (!fs.existsSync(sidebarPath)) { throw new Error(`缺少侧边栏配置:${sidebarPath}`); } const sidebar = readJson(sidebarPath); return Array.isArray(sidebar.prototypes) ? sidebar.prototypes : []; } function listPrototypeIdsWithAnnotation(projectRoot) { const prototypesRoot = path.join(projectRoot, 'src/prototypes'); if (!fs.existsSync(prototypesRoot)) return []; return fs.readdirSync(prototypesRoot, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(prototypesRoot, entry.name, 'annotation-source.json'))) .map((entry) => entry.name) .sort((a, b) => a.localeCompare(b, 'zh-CN')); } function fixPublishedLinks(nodes, prototypeId) { const walk = (items) => { for (const node of items || []) { if (node?.type === 'link' && typeof node.href === 'string') { const href = node.href.trim(); const selfMatch = href.match(new RegExp(`^/prototypes/${prototypeId.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}(?:[#?].*)?$`, 'u')); if (selfMatch) { node.href = './index.html'; node.target = node.target || 'self'; } else { const otherMatch = href.match(/^\/prototypes\/([^/?#]+)(?:[#?].*)?$/u); if (otherMatch) { node.href = `../${otherMatch[1]}/index.html`; node.target = node.target || 'self'; } } } if (node?.type === 'folder' && Array.isArray(node.children)) { walk(node.children); } } }; walk(nodes); } function syncPrototypeDirectory(prototypeId, sidebarItems, projectRoot) { const annotationPath = path.join(projectRoot, 'src/prototypes', prototypeId, 'annotation-source.json'); if (!fs.existsSync(annotationPath)) { console.log(`[skip] ${prototypeId}: 无 annotation-source.json`); return false; } const annotation = readJson(annotationPath); const navChildren = sidebarItemsToDirectoryChildren(sidebarItems, prototypeId); const navFolder = { type: 'folder', id: NAV_FOLDER_ID, title: 'ONE-OS 原型导航', defaultExpanded: true, children: navChildren, }; const existingNodes = Array.isArray(annotation.directory?.nodes) ? annotation.directory.nodes : []; const preservedNodes = existingNodes.filter((node) => node?.id !== NAV_FOLDER_ID); fixPublishedLinks(preservedNodes, prototypeId); annotation.directory = { nodes: [navFolder, ...preservedNodes], }; annotation.data = annotation.data || {}; annotation.data.updatedAt = Date.now(); writeJson(annotationPath, annotation); console.log(`[sync] ${prototypeId}: 已写入 ${navChildren.length} 个顶级目录项(含分组)`); return true; } function main() { const args = parseArgs(process.argv.slice(2)); const projectRoot = args.projectRoot || path.resolve(__dirname, '..'); const sidebarItems = loadSidebarPrototypes(projectRoot); const targets = args.prototype ? [args.prototype] : listPrototypeIdsWithAnnotation(projectRoot); if (!sidebarItems.length) { console.error('sidebar-tree.json 中未找到 prototypes 菜单。'); process.exit(1); } if (!targets.length) { console.log('未找到带 annotation-source.json 的原型。'); process.exit(0); } let changed = 0; for (const prototypeId of targets) { if (syncPrototypeDirectory(prototypeId, sidebarItems, projectRoot)) { changed += 1; } } console.log(`完成:处理 ${targets.length} 个原型,更新 ${changed} 个 annotation-source.json`); } main();