Initial commit: 小羚羚小程序 Axhub Make workspace.
Include xll-miniapp prototype, PRD resources, annotation directory sync, agent skills, and cloud publishing setup. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
55
vite-plugins/utils/entriesManifest.ts
Normal file
55
vite-plugins/utils/entriesManifest.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
getEntriesPath as getEntriesPathCore,
|
||||
migrateLegacyEntries as migrateLegacyEntriesCore,
|
||||
readEntriesManifest as readEntriesManifestCore,
|
||||
scanProjectEntries as scanProjectEntriesCore,
|
||||
toCompatMaps as toCompatMapsCore,
|
||||
writeEntriesManifestAtomic as writeEntriesManifestAtomicCore,
|
||||
} from './entriesManifestCore.js';
|
||||
|
||||
export type EntryGroup = 'components' | 'prototypes' | 'themes';
|
||||
|
||||
export interface EntriesManifestItem {
|
||||
group: string;
|
||||
name: string;
|
||||
js: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
export interface EntriesManifestV2 {
|
||||
schemaVersion: 2;
|
||||
generatedAt: string;
|
||||
items: Record<string, EntriesManifestItem>;
|
||||
js: Record<string, string>;
|
||||
html: Record<string, string>;
|
||||
}
|
||||
|
||||
export function toCompatMaps(items: Record<string, EntriesManifestItem>): {
|
||||
js: Record<string, string>;
|
||||
html: Record<string, string>;
|
||||
} {
|
||||
return toCompatMapsCore(items);
|
||||
}
|
||||
|
||||
export function scanProjectEntries(
|
||||
projectRoot: string,
|
||||
groups: EntryGroup[] = ['components', 'prototypes', 'themes'],
|
||||
): EntriesManifestV2 {
|
||||
return scanProjectEntriesCore(projectRoot, groups);
|
||||
}
|
||||
|
||||
export function migrateLegacyEntries(raw: unknown, projectRoot: string): EntriesManifestV2 {
|
||||
return migrateLegacyEntriesCore(raw, projectRoot);
|
||||
}
|
||||
|
||||
export function writeEntriesManifestAtomic(projectRoot: string, manifest: EntriesManifestV2): EntriesManifestV2 {
|
||||
return writeEntriesManifestAtomicCore(projectRoot, manifest);
|
||||
}
|
||||
|
||||
export function readEntriesManifest(projectRoot: string): EntriesManifestV2 {
|
||||
return readEntriesManifestCore(projectRoot);
|
||||
}
|
||||
|
||||
export function getEntriesPath(projectRoot: string): string {
|
||||
return getEntriesPathCore(projectRoot);
|
||||
}
|
||||
24
vite-plugins/utils/entriesManifestCore.d.ts
vendored
Normal file
24
vite-plugins/utils/entriesManifestCore.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface EntriesManifestItem {
|
||||
group: string;
|
||||
name: string;
|
||||
js: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
export interface EntriesManifestV2 {
|
||||
schemaVersion: 2;
|
||||
generatedAt: string;
|
||||
items: Record<string, EntriesManifestItem>;
|
||||
js: Record<string, string>;
|
||||
html: Record<string, string>;
|
||||
}
|
||||
|
||||
export function toCompatMaps(items: Record<string, EntriesManifestItem>): {
|
||||
js: Record<string, string>;
|
||||
html: Record<string, string>;
|
||||
};
|
||||
export function scanProjectEntries(projectRoot: string, groups?: string[]): EntriesManifestV2;
|
||||
export function migrateLegacyEntries(raw: unknown, projectRoot: string): EntriesManifestV2;
|
||||
export function writeEntriesManifestAtomic(projectRoot: string, manifest: EntriesManifestV2): EntriesManifestV2;
|
||||
export function readEntriesManifest(projectRoot: string): EntriesManifestV2;
|
||||
export function getEntriesPath(projectRoot: string): string;
|
||||
247
vite-plugins/utils/entriesManifestCore.js
Normal file
247
vite-plugins/utils/entriesManifestCore.js
Normal file
@@ -0,0 +1,247 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const DEFAULT_GROUPS = ['components', 'prototypes', 'themes'];
|
||||
const SCHEMA_VERSION = 2;
|
||||
const ENTRIES_RELATIVE_PATH = path.join('.axhub', 'make', 'entries.json');
|
||||
|
||||
function toPosixPath(input) {
|
||||
return String(input || '').split(path.sep).join('/');
|
||||
}
|
||||
|
||||
function normalizeRelativePath(projectRoot, filePath) {
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const absoluteCandidate = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.resolve(projectRoot, filePath);
|
||||
const relative = path.relative(projectRoot, absoluteCandidate);
|
||||
|
||||
if (!relative || relative.startsWith('..')) {
|
||||
return toPosixPath(filePath).replace(/^\.?\//, '');
|
||||
}
|
||||
|
||||
return toPosixPath(relative).replace(/^\.?\//, '');
|
||||
}
|
||||
|
||||
function sortRecordByKey(record) {
|
||||
const next = {};
|
||||
Object.keys(record || {})
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.forEach((key) => {
|
||||
next[key] = record[key];
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizeItemKey(key) {
|
||||
const normalized = String(key || '').trim().replace(/\\/g, '/');
|
||||
if (!normalized || !normalized.includes('/')) return '';
|
||||
return normalized.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
function parseKey(key) {
|
||||
const normalized = normalizeItemKey(key);
|
||||
if (!normalized) return null;
|
||||
const [group, ...nameParts] = normalized.split('/');
|
||||
const name = nameParts.join('/');
|
||||
if (!group || !name) return null;
|
||||
return { group, name };
|
||||
}
|
||||
|
||||
function sanitizeItem(item, projectRoot, fallbackKey) {
|
||||
const keyInfo = parseKey(fallbackKey);
|
||||
if (!keyInfo) return null;
|
||||
|
||||
const group = String(item?.group || keyInfo.group).trim();
|
||||
const name = String(item?.name || keyInfo.name).trim();
|
||||
if (!group || !name) return null;
|
||||
|
||||
const key = `${group}/${name}`;
|
||||
const js = normalizeRelativePath(
|
||||
projectRoot,
|
||||
item?.js || `src/${group}/${name}/index.tsx`,
|
||||
);
|
||||
const html = normalizeRelativePath(
|
||||
projectRoot,
|
||||
item?.html || `src/${group}/${name}/index.html`,
|
||||
);
|
||||
|
||||
return {
|
||||
key,
|
||||
item: {
|
||||
group,
|
||||
name,
|
||||
js,
|
||||
html,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildManifestFromItems(items, generatedAt) {
|
||||
const compat = toCompatMaps(items);
|
||||
return {
|
||||
schemaVersion: SCHEMA_VERSION,
|
||||
generatedAt: generatedAt || new Date().toISOString(),
|
||||
items,
|
||||
js: compat.js,
|
||||
html: compat.html,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeManifest(raw, projectRoot, generatedAt) {
|
||||
const nextItems = {};
|
||||
const sourceItems =
|
||||
raw && typeof raw === 'object' && raw.items && typeof raw.items === 'object'
|
||||
? raw.items
|
||||
: {};
|
||||
|
||||
Object.keys(sourceItems)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.forEach((key) => {
|
||||
const sanitized = sanitizeItem(sourceItems[key], projectRoot, key);
|
||||
if (sanitized) {
|
||||
nextItems[sanitized.key] = sanitized.item;
|
||||
}
|
||||
});
|
||||
|
||||
return buildManifestFromItems(nextItems, generatedAt);
|
||||
}
|
||||
|
||||
export function toCompatMaps(items) {
|
||||
const js = {};
|
||||
const html = {};
|
||||
Object.keys(items || {})
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.forEach((key) => {
|
||||
const item = items[key];
|
||||
if (!item || typeof item !== 'object') return;
|
||||
const jsPath = typeof item.js === 'string' ? item.js.trim() : '';
|
||||
const htmlPath = typeof item.html === 'string' ? item.html.trim() : '';
|
||||
if (jsPath) {
|
||||
js[key] = jsPath;
|
||||
}
|
||||
if (htmlPath) {
|
||||
html[key] = htmlPath;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
js: sortRecordByKey(js),
|
||||
html: sortRecordByKey(html),
|
||||
};
|
||||
}
|
||||
|
||||
export function scanProjectEntries(projectRoot, groups = DEFAULT_GROUPS) {
|
||||
const root = path.resolve(projectRoot, 'src');
|
||||
const items = {};
|
||||
|
||||
for (const group of groups) {
|
||||
const groupDir = path.join(root, group);
|
||||
if (!fs.existsSync(groupDir)) continue;
|
||||
|
||||
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 jsEntry = path.join(groupDir, name, 'index.tsx');
|
||||
if (!fs.existsSync(jsEntry)) continue;
|
||||
const key = `${group}/${name}`;
|
||||
items[key] = {
|
||||
group,
|
||||
name,
|
||||
js: toPosixPath(path.relative(projectRoot, jsEntry)),
|
||||
html: toPosixPath(path.relative(projectRoot, path.join(groupDir, name, 'index.html'))),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return buildManifestFromItems(sortRecordByKey(items));
|
||||
}
|
||||
|
||||
export function migrateLegacyEntries(raw, projectRoot) {
|
||||
if (raw && typeof raw === 'object' && raw.schemaVersion === SCHEMA_VERSION && raw.items) {
|
||||
return normalizeManifest(raw, projectRoot, raw.generatedAt);
|
||||
}
|
||||
|
||||
const legacyJs =
|
||||
raw && typeof raw === 'object' && raw.js && typeof raw.js === 'object'
|
||||
? raw.js
|
||||
: {};
|
||||
const legacyHtml =
|
||||
raw && typeof raw === 'object' && raw.html && typeof raw.html === 'object'
|
||||
? raw.html
|
||||
: {};
|
||||
|
||||
const keys = new Set([
|
||||
...Object.keys(legacyJs),
|
||||
...Object.keys(legacyHtml),
|
||||
]);
|
||||
|
||||
const items = {};
|
||||
Array.from(keys)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.forEach((key) => {
|
||||
const parsed = parseKey(key);
|
||||
if (!parsed) return;
|
||||
const js = normalizeRelativePath(
|
||||
projectRoot,
|
||||
legacyJs[key] || `src/${parsed.group}/${parsed.name}/index.tsx`,
|
||||
);
|
||||
const html = normalizeRelativePath(
|
||||
projectRoot,
|
||||
legacyHtml[key] || `src/${parsed.group}/${parsed.name}/index.html`,
|
||||
);
|
||||
items[key] = {
|
||||
group: parsed.group,
|
||||
name: parsed.name,
|
||||
js,
|
||||
html,
|
||||
};
|
||||
});
|
||||
|
||||
return buildManifestFromItems(items);
|
||||
}
|
||||
|
||||
export function writeEntriesManifestAtomic(projectRoot, manifest) {
|
||||
const entriesPath = getEntriesPath(projectRoot);
|
||||
const normalized = normalizeManifest(manifest, projectRoot, manifest?.generatedAt);
|
||||
const tempPath = `${entriesPath}.tmp-${process.pid}-${Date.now()}`;
|
||||
fs.mkdirSync(path.dirname(entriesPath), { recursive: true });
|
||||
|
||||
fs.writeFileSync(tempPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
|
||||
fs.renameSync(tempPath, entriesPath);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function readEntriesManifest(projectRoot) {
|
||||
const entriesPath = getEntriesPath(projectRoot);
|
||||
if (!fs.existsSync(entriesPath)) {
|
||||
const scanned = scanProjectEntries(projectRoot, DEFAULT_GROUPS);
|
||||
return writeEntriesManifestAtomic(projectRoot, scanned);
|
||||
}
|
||||
|
||||
let raw;
|
||||
try {
|
||||
raw = JSON.parse(fs.readFileSync(entriesPath, 'utf8'));
|
||||
} catch {
|
||||
raw = {};
|
||||
}
|
||||
|
||||
const migrated = migrateLegacyEntries(raw, projectRoot);
|
||||
const rawString = JSON.stringify(raw);
|
||||
const nextString = JSON.stringify(migrated);
|
||||
if (rawString !== nextString) {
|
||||
return writeEntriesManifestAtomic(projectRoot, migrated);
|
||||
}
|
||||
return migrated;
|
||||
}
|
||||
|
||||
export function getEntriesPath(projectRoot) {
|
||||
return path.resolve(projectRoot, ENTRIES_RELATIVE_PATH);
|
||||
}
|
||||
15
vite-plugins/utils/httpUtils.ts
Normal file
15
vite-plugins/utils/httpUtils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { networkInterfaces } from 'node:os';
|
||||
|
||||
export function getLocalIP(): string {
|
||||
const interfaces = networkInterfaces();
|
||||
|
||||
for (const nets of Object.values(interfaces)) {
|
||||
for (const net of nets || []) {
|
||||
if (net.family === 'IPv4' && !net.internal) {
|
||||
return net.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'localhost';
|
||||
}
|
||||
7
vite-plugins/utils/makeConstants.ts
Normal file
7
vite-plugins/utils/makeConstants.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import path from 'path';
|
||||
|
||||
export const MAKE_STATE_DIR = path.join('.axhub', 'make');
|
||||
export const MAKE_CONFIG_RELATIVE_PATH = path.join(MAKE_STATE_DIR, 'axhub.config.json');
|
||||
export const MAKE_DEV_SERVER_INFO_RELATIVE_PATH = path.join(MAKE_STATE_DIR, '.dev-server-info.json');
|
||||
export const MAKE_ENTRIES_RELATIVE_PATH = path.join(MAKE_STATE_DIR, 'entries.json');
|
||||
export const AXURE_BRIDGE_BASE_URL = 'http://localhost:32767';
|
||||
103
vite-plugins/utils/portOccupancy.ts
Normal file
103
vite-plugins/utils/portOccupancy.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
export interface PortProcessLookup {
|
||||
platform?: NodeJS.Platform;
|
||||
spawnSync?: typeof spawnSync;
|
||||
}
|
||||
|
||||
export interface ReleasePortOptions extends PortProcessLookup {
|
||||
killPid?: (pid: number, signal?: NodeJS.Signals) => void;
|
||||
currentPid?: number;
|
||||
waitMs?: number;
|
||||
}
|
||||
|
||||
function parsePidList(output: string): number[] {
|
||||
const seen = new Set<number>();
|
||||
for (const line of output.split(/\r?\n/u)) {
|
||||
const pid = Number(line.trim());
|
||||
if (Number.isInteger(pid) && pid > 0) {
|
||||
seen.add(pid);
|
||||
}
|
||||
}
|
||||
return Array.from(seen);
|
||||
}
|
||||
|
||||
export function findListeningPidsOnPort(port: number, options: PortProcessLookup = {}): number[] {
|
||||
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const platform = options.platform || process.platform;
|
||||
const run = options.spawnSync || spawnSync;
|
||||
|
||||
if (platform === 'win32') {
|
||||
const result = run('powershell.exe', [
|
||||
'-NoProfile',
|
||||
'-Command',
|
||||
`Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`,
|
||||
], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
timeout: 2000,
|
||||
});
|
||||
return parsePidList(String(result.stdout || ''));
|
||||
}
|
||||
|
||||
const result = run('lsof', [
|
||||
'-tiTCP',
|
||||
`:${port}`,
|
||||
'-sTCP:LISTEN',
|
||||
], {
|
||||
encoding: 'utf8',
|
||||
timeout: 2000,
|
||||
});
|
||||
return parsePidList(String(result.stdout || ''));
|
||||
}
|
||||
|
||||
function waitForPortRelease(port: number, options: ReleasePortOptions): void {
|
||||
const deadline = Date.now() + Math.max(0, options.waitMs ?? 1500);
|
||||
while (Date.now() < deadline) {
|
||||
if (findListeningPidsOnPort(port, options).length === 0) {
|
||||
return;
|
||||
}
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
|
||||
}
|
||||
}
|
||||
|
||||
export function releaseListeningProcessesOnPort(port: number, options: ReleasePortOptions = {}): number[] {
|
||||
const currentPid = options.currentPid ?? process.pid;
|
||||
const pids = findListeningPidsOnPort(port, options).filter((pid) => pid !== currentPid);
|
||||
if (pids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ((options.platform || process.platform) === 'win32') {
|
||||
const run = options.spawnSync || spawnSync;
|
||||
for (const pid of pids) {
|
||||
run('taskkill.exe', ['/PID', String(pid), '/T', '/F'], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const killPid = options.killPid || process.kill.bind(process);
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
killPid(pid, 'SIGTERM');
|
||||
} catch {
|
||||
// Process may have exited between lookup and signal.
|
||||
}
|
||||
}
|
||||
waitForPortRelease(port, options);
|
||||
for (const pid of findListeningPidsOnPort(port, options).filter((pid) => pid !== currentPid)) {
|
||||
try {
|
||||
killPid(pid, 'SIGKILL');
|
||||
} catch {
|
||||
// Process may have exited after the second lookup.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pids;
|
||||
}
|
||||
40
vite-plugins/utils/previewTitle.ts
Normal file
40
vite-plugins/utils/previewTitle.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
export type PreviewResourceGroup = 'components' | 'prototypes' | 'themes';
|
||||
export type PreviewTitleMode = 'dev' | 'export';
|
||||
|
||||
export function readEntryDisplayName(indexFilePath: string): string | null {
|
||||
try {
|
||||
const content = fs.readFileSync(indexFilePath, 'utf8');
|
||||
const match = content.match(/@name\s+([^\n]+)/);
|
||||
const displayName = match?.[1]?.replace(/\*\/\s*$/, '').trim();
|
||||
return displayName || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildPreviewTitle(options: {
|
||||
group: PreviewResourceGroup;
|
||||
name: string;
|
||||
displayName?: string | null;
|
||||
mode: PreviewTitleMode;
|
||||
}): string {
|
||||
const label = String(options.displayName || options.name || '').trim() || '未命名预览';
|
||||
const suffixMap: Record<PreviewResourceGroup, Record<PreviewTitleMode, string>> = {
|
||||
components: {
|
||||
dev: '组件预览(开发)',
|
||||
export: '组件预览',
|
||||
},
|
||||
prototypes: {
|
||||
dev: '原型预览(开发)',
|
||||
export: '原型预览',
|
||||
},
|
||||
themes: {
|
||||
dev: '主题预览(开发)',
|
||||
export: '主题预览',
|
||||
},
|
||||
};
|
||||
|
||||
return `${label} - ${suffixMap[options.group][options.mode]}`;
|
||||
}
|
||||
Reference in New Issue
Block a user