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>
This commit is contained in:
王冕
2026-06-09 18:12:25 +08:00
parent 351688006e
commit a27e3b8e43
1510 changed files with 162044 additions and 1517 deletions

View File

@@ -0,0 +1,186 @@
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;
}