Files
ONE-OS/axhub-make/vite-plugins/utils/entryScanner.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

187 lines
5.0 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import { getDisplayName } from './fileUtils';
import { migrateLegacyEntries, toCompatMaps } from './entriesManifest';
export type SidebarTreeTab = 'prototypes' | 'components' | 'docs' | 'canvas';
type ScannableGroup = 'components' | 'prototypes' | 'themes';
export interface ScannedEntryItem {
name: string;
displayName: string;
demoUrl: string;
specUrl: string;
jsUrl: string;
filePath: string;
isReference?: boolean;
hasSubPages?: boolean;
}
export interface EntriesFileData extends Record<string, unknown> {
schemaVersion?: number;
generatedAt?: string;
items?: Record<string, {
group?: string;
name?: string;
js?: string;
html?: string;
}>;
js?: Record<string, string>;
html?: Record<string, string>;
}
export interface EntryScanResult {
entries: {
js: Record<string, string>;
html: Record<string, string>;
};
items: Record<SidebarTreeTab, ScannedEntryItem[]>;
}
const MANIFEST_SCANNED_GROUPS: ScannableGroup[] = ['components', 'prototypes', 'themes'];
function encodeUrlPathSegments(value: string): string {
return value
.split('/')
.filter(Boolean)
.map((segment) => encodeURIComponent(segment))
.join('/');
}
function isScannedGroupKey(key: string): boolean {
return MANIFEST_SCANNED_GROUPS.some((group) => key.startsWith(`${group}/`));
}
function scanGroup(projectRoot: string, group: ScannableGroup): {
entries: { js: Record<string, string>; html: Record<string, string> };
items: ScannedEntryItem[];
} {
const groupDir = path.resolve(projectRoot, 'src', group);
const entries = { js: {} as Record<string, string>, html: {} as Record<string, string> };
const items: ScannedEntryItem[] = [];
if (!fs.existsSync(groupDir)) {
return { entries, items };
}
const names = fs
.readdirSync(groupDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort((a, b) => a.localeCompare(b));
for (const name of names) {
const folderPath = path.join(groupDir, name);
const jsEntry = path.join(folderPath, 'index.tsx');
if (!fs.existsSync(jsEntry)) {
continue;
}
const key = `${group}/${name}`;
entries.js[key] = jsEntry;
entries.html[key] = path.join(folderPath, 'index.html');
let displayName = getDisplayName(jsEntry) || name;
const encodedKey = encodeUrlPathSegments(key);
items.push({
name,
displayName,
demoUrl: `/${encodedKey}`,
specUrl: `/${encodedKey}/spec`,
jsUrl: `/build/${encodedKey}.js`,
filePath: jsEntry,
isReference: name.startsWith('ref-'),
hasSubPages: group === 'prototypes' && fs.existsSync(path.join(folderPath, 'pages.json')),
});
}
items.sort((a, b) => a.name.localeCompare(b.name));
return {
entries,
items,
};
}
export function scanEntries(projectRoot: string): EntryScanResult {
const result: EntryScanResult = {
entries: { js: {}, html: {} },
items: {
components: [],
prototypes: [],
docs: [],
canvas: [],
},
};
for (const group of MANIFEST_SCANNED_GROUPS) {
const scanned = scanGroup(projectRoot, group);
Object.assign(result.entries.js, scanned.entries.js);
Object.assign(result.entries.html, scanned.entries.html);
if (group === 'components' || group === 'prototypes') {
result.items[group] = scanned.items;
}
}
return result;
}
export function allowedItemKeysByTab(
scannedEntries: Record<string, string>,
tab: SidebarTreeTab,
): Set<string> {
return new Set(
Object.keys(scannedEntries)
.filter((key) => key.startsWith(`${tab}/`))
.sort((a, b) => a.localeCompare(b)),
);
}
export function mergeScannedEntries(existing: EntriesFileData, scanned: EntryScanResult['entries']): EntriesFileData {
const migrated = migrateLegacyEntries(existing, process.cwd());
const nextItems = { ...(migrated.items || {}) };
for (const key of Object.keys(nextItems)) {
if (isScannedGroupKey(key) && !Object.prototype.hasOwnProperty.call(scanned.js, key)) {
delete nextItems[key];
}
}
for (const [key, jsPath] of Object.entries(scanned.js || {})) {
const [group, ...nameParts] = key.split('/');
const name = nameParts.join('/');
if (!group || !name) continue;
nextItems[key] = {
group,
name,
js: jsPath,
html: scanned.html?.[key] || `src/${group}/${name}/index.html`,
};
}
const sortedItems = Object.keys(nextItems)
.sort((a, b) => a.localeCompare(b))
.reduce<Record<string, {
group?: string;
name?: string;
js?: string;
html?: string;
}>>((acc, key) => {
acc[key] = nextItems[key];
return acc;
}, {});
const compat = toCompatMaps(sortedItems);
const nextEntries: EntriesFileData = {
...existing,
schemaVersion: 2,
generatedAt: new Date().toISOString(),
items: sortedItems,
js: compat.js,
html: compat.html,
};
delete (nextEntries as { sidebarTree?: unknown }).sidebarTree;
return nextEntries;
}