Files
ONE-OS/axhub-make/vite-plugins/utils/sidebarTreeStore.ts
王冕 a27e3b8e43 feat: sync full workspace including web modules, docs, and configurations to Gitea
Optimized the root .gitignore to exclude virtual environments, node modules,
and temp folders to ensure clean and lightweight version tracking.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 18:12:25 +08:00

238 lines
7.2 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import type { SidebarTreeTab } from './entryScanner';
export type ResourceOrderType = 'themes' | 'data' | 'templates';
export type SidebarTreeNodeKind = 'folder' | 'item';
export interface SidebarTreeNode {
id: string;
kind: SidebarTreeNodeKind;
title: string;
itemKey?: string;
children?: SidebarTreeNode[];
}
export interface SidebarTreeStore {
version: number;
updatedAt: string;
prototypes: SidebarTreeNode[];
components: SidebarTreeNode[];
docs: SidebarTreeNode[];
canvas: SidebarTreeNode[];
themes: string[];
data: string[];
templates: string[];
}
interface EntriesWithLegacySidebarTree {
sidebarTree?: {
version?: number;
prototypes?: SidebarTreeNode[];
components?: SidebarTreeNode[];
docs?: SidebarTreeNode[];
canvas?: SidebarTreeNode[];
themes?: string[];
data?: string[];
templates?: string[];
};
}
interface SidebarTreeStoreOptions {
version: number;
legacyEntriesPath: string;
storePath: string;
}
const ENTRIES_MANIFEST_RELATIVE_PATH = path.join('.axhub', 'make', 'entries.json');
export const SIDEBAR_TREE_STORE_RELATIVE_PATH = path.join('.axhub', 'make', 'sidebar-tree.json');
function createDefaultStore(version: number): SidebarTreeStore {
return {
version,
updatedAt: new Date().toISOString(),
prototypes: [],
components: [],
docs: [],
canvas: [],
themes: [],
data: [],
templates: [],
};
}
function readJsonFile(filePath: string): unknown {
if (!fs.existsSync(filePath)) {
return null;
}
try {
const raw = fs.readFileSync(filePath, 'utf8');
return JSON.parse(raw);
} catch {
return null;
}
}
function cloneTree(nodes: SidebarTreeNode[]): SidebarTreeNode[] {
return nodes.map((node) => ({
...node,
children: Array.isArray(node.children) ? cloneTree(node.children) : undefined,
}));
}
function normalizeStore(data: unknown, version: number): SidebarTreeStore | null {
if (!data || typeof data !== 'object') {
return null;
}
const parsed = data as Partial<SidebarTreeStore>;
const prototypes = Array.isArray(parsed.prototypes) ? cloneTree(parsed.prototypes) : [];
const components = Array.isArray(parsed.components) ? cloneTree(parsed.components) : [];
const docs = Array.isArray(parsed.docs) ? cloneTree(parsed.docs) : [];
const canvas = Array.isArray(parsed.canvas) ? cloneTree(parsed.canvas) : [];
const themes = Array.isArray(parsed.themes)
? parsed.themes.filter((key): key is string => typeof key === 'string')
: [];
const dataOrder = Array.isArray(parsed.data)
? parsed.data.filter((key): key is string => typeof key === 'string')
: [];
const updatedAt = typeof parsed.updatedAt === 'string' && parsed.updatedAt.trim()
? parsed.updatedAt
: new Date().toISOString();
const templates = Array.isArray(parsed.templates)
? parsed.templates.filter((key): key is string => typeof key === 'string')
: [];
return {
version,
updatedAt,
prototypes,
components,
docs,
canvas,
themes,
data: dataOrder,
templates,
};
}
function readLegacySidebarTree(legacyEntriesPath: string, version: number): SidebarTreeStore | null {
const data = readJsonFile(legacyEntriesPath);
if (!data || typeof data !== 'object') {
return null;
}
const entries = data as EntriesWithLegacySidebarTree;
const legacy = entries.sidebarTree;
if (!legacy || typeof legacy !== 'object') {
return null;
}
return {
version,
updatedAt: new Date().toISOString(),
prototypes: Array.isArray(legacy.prototypes) ? cloneTree(legacy.prototypes) : [],
components: Array.isArray(legacy.components) ? cloneTree(legacy.components) : [],
docs: Array.isArray((legacy as any).docs) ? cloneTree((legacy as any).docs) : [],
canvas: Array.isArray((legacy as any).canvas) ? cloneTree((legacy as any).canvas) : [],
themes: Array.isArray((legacy as any).themes)
? (legacy as any).themes.filter((key: unknown): key is string => typeof key === 'string')
: [],
data: Array.isArray((legacy as any).data)
? (legacy as any).data.filter((key: unknown): key is string => typeof key === 'string')
: [],
templates: Array.isArray((legacy as any).templates)
? (legacy as any).templates.filter((key: unknown): key is string => typeof key === 'string')
: [],
};
}
function writeStoreAtomic(storePath: string, store: SidebarTreeStore): void {
const dir = path.dirname(storePath);
fs.mkdirSync(dir, { recursive: true });
const tempPath = `${storePath}.tmp-${process.pid}-${Date.now()}`;
try {
fs.writeFileSync(tempPath, JSON.stringify(store, null, 2), 'utf8');
fs.renameSync(tempPath, storePath);
} finally {
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath);
}
}
}
function resolveOptions(projectRoot: string, options?: Partial<SidebarTreeStoreOptions>): SidebarTreeStoreOptions {
return {
version: options?.version ?? 1,
legacyEntriesPath: options?.legacyEntriesPath ?? path.join(projectRoot, ENTRIES_MANIFEST_RELATIVE_PATH),
storePath: options?.storePath ?? path.join(projectRoot, SIDEBAR_TREE_STORE_RELATIVE_PATH),
};
}
export function createSidebarTreeStore(projectRoot: string, options?: Partial<SidebarTreeStoreOptions>) {
const resolved = resolveOptions(projectRoot, options);
const ensureStore = (): SidebarTreeStore => {
const loaded = normalizeStore(readJsonFile(resolved.storePath), resolved.version);
if (loaded) {
return loaded;
}
const migrated = readLegacySidebarTree(resolved.legacyEntriesPath, resolved.version);
const nextStore = migrated || createDefaultStore(resolved.version);
writeStoreAtomic(resolved.storePath, nextStore);
return nextStore;
};
const saveStore = (store: SidebarTreeStore) => {
const nextStore: SidebarTreeStore = {
version: resolved.version,
updatedAt: new Date().toISOString(),
prototypes: cloneTree(store.prototypes),
components: cloneTree(store.components),
docs: cloneTree(store.docs),
canvas: cloneTree(store.canvas),
themes: Array.isArray(store.themes) ? [...store.themes] : [],
data: Array.isArray(store.data) ? [...store.data] : [],
templates: Array.isArray(store.templates) ? [...store.templates] : [],
};
writeStoreAtomic(resolved.storePath, nextStore);
return nextStore;
};
return {
getStorePath() {
return resolved.storePath;
},
getStore() {
return ensureStore();
},
getTree(tab: SidebarTreeTab) {
const store = ensureStore();
return cloneTree(store[tab]);
},
setTree(tab: SidebarTreeTab, tree: SidebarTreeNode[]) {
const store = ensureStore();
const nextStore: SidebarTreeStore = {
...store,
[tab]: cloneTree(tree),
};
return saveStore(nextStore);
},
getResourceOrder(type: ResourceOrderType) {
const store = ensureStore();
return Array.isArray(store[type]) ? [...store[type]] : [];
},
setResourceOrder(type: ResourceOrderType, order: string[]) {
const store = ensureStore();
const nextStore: SidebarTreeStore = {
...store,
[type]: Array.isArray(order) ? [...order] : [],
};
return saveStore(nextStore);
},
};
}