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:
22
axhub-make/vite-plugins/utils/contentDisposition.ts
Normal file
22
axhub-make/vite-plugins/utils/contentDisposition.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
function encodeRFC5987Value(value: string): string {
|
||||
return encodeURIComponent(value).replace(/['()*]/g, (char) =>
|
||||
`%${char.charCodeAt(0).toString(16).toUpperCase()}`,
|
||||
);
|
||||
}
|
||||
|
||||
function createAsciiFallback(fileName: string): string {
|
||||
const normalized = fileName
|
||||
.normalize('NFKD')
|
||||
.replace(/[^\x20-\x7E]+/g, '_')
|
||||
.replace(/["\\;%]/g, '_')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
return normalized || 'download';
|
||||
}
|
||||
|
||||
export function buildAttachmentContentDisposition(fileName: string): string {
|
||||
const fallback = createAsciiFallback(fileName);
|
||||
const encoded = encodeRFC5987Value(fileName);
|
||||
return `attachment; filename="${fallback}"; filename*=UTF-8''${encoded}`;
|
||||
}
|
||||
62
axhub-make/vite-plugins/utils/cssUtils.ts
Normal file
62
axhub-make/vite-plugins/utils/cssUtils.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* CSS 增量合并函数
|
||||
* 相同选择器:增加新属性,更新已有属性
|
||||
*/
|
||||
export function mergeCss(existingCss: string, newCss: string): string {
|
||||
const parseCss = (css: string): Map<string, Map<string, string>> => {
|
||||
const rules = new Map<string, Map<string, string>>();
|
||||
const ruleRegex = /([^{]+)\{([^}]+)\}/g;
|
||||
let match;
|
||||
|
||||
while ((match = ruleRegex.exec(css)) !== null) {
|
||||
const selector = match[1].trim();
|
||||
const declarations = match[2].trim();
|
||||
|
||||
if (!rules.has(selector)) {
|
||||
rules.set(selector, new Map());
|
||||
}
|
||||
|
||||
const props = rules.get(selector)!;
|
||||
const propRegex = /([^:;]+):([^;]+)/g;
|
||||
let propMatch;
|
||||
|
||||
while ((propMatch = propRegex.exec(declarations)) !== null) {
|
||||
const property = propMatch[1].trim();
|
||||
const value = propMatch[2].trim();
|
||||
props.set(property, value);
|
||||
}
|
||||
}
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
const serializeCss = (rules: Map<string, Map<string, string>>): string => {
|
||||
const lines: string[] = [];
|
||||
|
||||
for (const [selector, props] of rules) {
|
||||
lines.push(`${selector} {`);
|
||||
for (const [property, value] of props) {
|
||||
lines.push(` ${property}: ${value};`);
|
||||
}
|
||||
lines.push('}\n');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
||||
const existingRules = parseCss(existingCss);
|
||||
const newRules = parseCss(newCss);
|
||||
|
||||
for (const [selector, newProps] of newRules) {
|
||||
if (!existingRules.has(selector)) {
|
||||
existingRules.set(selector, newProps);
|
||||
} else {
|
||||
const existingProps = existingRules.get(selector)!;
|
||||
for (const [property, value] of newProps) {
|
||||
existingProps.set(property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return serializeCss(existingRules);
|
||||
}
|
||||
608
axhub-make/vite-plugins/utils/docUtils.ts
Normal file
608
axhub-make/vite-plugins/utils/docUtils.ts
Normal file
@@ -0,0 +1,608 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const PROTECTED_TEMPLATE_BASENAMES = new Set([
|
||||
'spec-template',
|
||||
]);
|
||||
const PROTECTED_DOC_BASENAMES = new Set([
|
||||
'project-overview',
|
||||
]);
|
||||
const DOC_REFERENCE_SCAN_DIRECTORIES = [
|
||||
'src/docs',
|
||||
'rules',
|
||||
'skills',
|
||||
];
|
||||
const DOC_REFERENCE_ALLOWED_EXTENSIONS = new Set([
|
||||
'.md',
|
||||
'.txt',
|
||||
'.json',
|
||||
'.yaml',
|
||||
'.yml',
|
||||
'.csv',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.css',
|
||||
'.html',
|
||||
]);
|
||||
const TEMPLATE_REFERENCE_SCAN_DIRECTORIES = [
|
||||
'src/docs',
|
||||
'rules',
|
||||
'skills',
|
||||
];
|
||||
const DOCS_ROOT_RELATIVE_PATH = 'src/docs';
|
||||
const TEMPLATES_ROOT_RELATIVE_PATH = 'src/docs/templates';
|
||||
const LEGACY_TEMPLATES_ROOT_RELATIVE_PATH = 'assets/templates';
|
||||
|
||||
export const SPEC_DOC_IMAGE_MAX_FILE_SIZE = 5 * 1024 * 1024;
|
||||
export const SPEC_DOC_IMAGE_ALLOWED_EXTENSIONS = new Set([
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.gif',
|
||||
'.webp',
|
||||
'.svg',
|
||||
]);
|
||||
export const SPEC_DOC_IMAGE_MIME_TO_EXTENSION: Record<string, string> = {
|
||||
'image/png': '.png',
|
||||
'image/jpeg': '.jpg',
|
||||
'image/gif': '.gif',
|
||||
'image/webp': '.webp',
|
||||
'image/svg+xml': '.svg',
|
||||
};
|
||||
|
||||
export function sanitizeDocBaseName(input: string) {
|
||||
return input
|
||||
.trim()
|
||||
.replace(/\.md$/i, '')
|
||||
.replace(/[\\/:*?"<>|]/g, '-')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
function normalizePathSegments(input: string) {
|
||||
return String(input || '')
|
||||
.trim()
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\/+/, '')
|
||||
.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
function getDocBaseName(docName: string) {
|
||||
const normalized = normalizePathSegments(docName);
|
||||
const ext = path.extname(normalized);
|
||||
return path.basename(normalized, ext);
|
||||
}
|
||||
|
||||
function escapeRegExp(input: string) {
|
||||
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function normalizeRelativeProjectPath(projectRoot: string, filePath: string) {
|
||||
return path.relative(projectRoot, filePath).split(path.sep).join('/');
|
||||
}
|
||||
|
||||
export function getDocsDir(projectRoot: string = process.cwd()) {
|
||||
return path.resolve(projectRoot, DOCS_ROOT_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
export function getTemplatesDir(projectRoot: string = process.cwd()) {
|
||||
return path.resolve(projectRoot, TEMPLATES_ROOT_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
export function getLegacyTemplatesDir(projectRoot: string = process.cwd()) {
|
||||
return path.resolve(projectRoot, LEGACY_TEMPLATES_ROOT_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
export function toTemplateProjectPath(templateName: string) {
|
||||
const normalizedName = normalizePathSegments(templateName);
|
||||
return normalizedName ? `${TEMPLATES_ROOT_RELATIVE_PATH}/${normalizedName}` : TEMPLATES_ROOT_RELATIVE_PATH;
|
||||
}
|
||||
|
||||
export function isTemplateDocName(docName: string) {
|
||||
const normalizedName = normalizePathSegments(docName);
|
||||
return normalizedName === 'templates' || normalizedName.startsWith('templates/');
|
||||
}
|
||||
|
||||
export function buildDocApiPath(docName: string) {
|
||||
const normalizedName = normalizePathSegments(docName);
|
||||
if (!normalizedName) {
|
||||
return '/api/docs';
|
||||
}
|
||||
|
||||
if (isTemplateDocName(normalizedName)) {
|
||||
const templateName = normalizedName === 'templates'
|
||||
? ''
|
||||
: normalizedName.slice('templates/'.length);
|
||||
return templateName
|
||||
? `/api/docs/templates/${encodeURIComponent(templateName)}`
|
||||
: '/api/docs/templates';
|
||||
}
|
||||
|
||||
return `/api/docs/${encodeURIComponent(normalizedName)}`;
|
||||
}
|
||||
|
||||
function buildDocReferencePatterns(docName: string): RegExp[] {
|
||||
const normalizedDocName = normalizePathSegments(docName);
|
||||
if (!normalizedDocName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ext = path.extname(normalizedDocName).toLowerCase();
|
||||
const normalizedBaseName = ext ? normalizedDocName.slice(0, -ext.length) : normalizedDocName;
|
||||
const candidates = [
|
||||
`src/docs/${normalizedDocName}`,
|
||||
`/docs/${normalizedDocName}`,
|
||||
];
|
||||
|
||||
if (ext === '.md') {
|
||||
candidates.push(`src/docs/${normalizedBaseName}`, `/docs/${normalizedBaseName}`);
|
||||
}
|
||||
|
||||
return Array.from(new Set(candidates))
|
||||
.filter(Boolean)
|
||||
.map((candidate) => new RegExp(`${escapeRegExp(candidate)}(?=$|[^A-Za-z0-9_-])`));
|
||||
}
|
||||
|
||||
function buildTemplateReferencePatterns(templateName: string): RegExp[] {
|
||||
const normalizedTemplateName = normalizePathSegments(templateName);
|
||||
if (!normalizedTemplateName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ext = path.extname(normalizedTemplateName).toLowerCase();
|
||||
const normalizedBaseName = ext ? normalizedTemplateName.slice(0, -ext.length) : normalizedTemplateName;
|
||||
const candidates = [
|
||||
toTemplateProjectPath(normalizedTemplateName),
|
||||
`/docs/templates/${normalizedTemplateName}`,
|
||||
];
|
||||
|
||||
if (ext === '.md') {
|
||||
candidates.push(
|
||||
toTemplateProjectPath(normalizedBaseName),
|
||||
`/docs/templates/${normalizedBaseName}`,
|
||||
);
|
||||
}
|
||||
|
||||
return Array.from(new Set(candidates))
|
||||
.filter(Boolean)
|
||||
.map((candidate) => new RegExp(`${escapeRegExp(candidate)}(?=$|[^A-Za-z0-9_-])`));
|
||||
}
|
||||
|
||||
export function isProtectedTemplateName(templateName: string) {
|
||||
const normalizedName = String(templateName || '').trim();
|
||||
if (!normalizedName) return false;
|
||||
const baseName = path.basename(normalizedName, path.extname(normalizedName));
|
||||
return PROTECTED_TEMPLATE_BASENAMES.has(baseName);
|
||||
}
|
||||
|
||||
export function isProtectedDocName(docName: string) {
|
||||
return PROTECTED_DOC_BASENAMES.has(getDocBaseName(docName));
|
||||
}
|
||||
|
||||
export function safeDecodeURIComponent(input: string): string {
|
||||
try {
|
||||
return decodeURIComponent(input);
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
export function isPathInside(baseDir: string, targetPath: string): boolean {
|
||||
const relative = path.relative(baseDir, targetPath);
|
||||
return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
||||
}
|
||||
|
||||
type TemplateMigrationConflict = {
|
||||
relativePath: string;
|
||||
legacyPath: string;
|
||||
targetPath: string;
|
||||
};
|
||||
|
||||
type TemplateMigrationResult = {
|
||||
moved: string[];
|
||||
deduped: string[];
|
||||
conflicts: TemplateMigrationConflict[];
|
||||
removedLegacyDir: boolean;
|
||||
};
|
||||
|
||||
function areFilesIdentical(leftPath: string, rightPath: string) {
|
||||
const leftStat = fs.statSync(leftPath);
|
||||
const rightStat = fs.statSync(rightPath);
|
||||
if (leftStat.size !== rightStat.size) {
|
||||
return false;
|
||||
}
|
||||
return fs.readFileSync(leftPath).equals(fs.readFileSync(rightPath));
|
||||
}
|
||||
|
||||
function removeEmptyDirectories(directoryPath: string, stopAt: string) {
|
||||
let currentPath = directoryPath;
|
||||
const normalizedStopAt = path.resolve(stopAt);
|
||||
while (isPathInside(normalizedStopAt, currentPath) || currentPath === normalizedStopAt) {
|
||||
if (!fs.existsSync(currentPath)) {
|
||||
break;
|
||||
}
|
||||
const entries = fs.readdirSync(currentPath);
|
||||
if (entries.length > 0) {
|
||||
break;
|
||||
}
|
||||
fs.rmdirSync(currentPath);
|
||||
if (currentPath === normalizedStopAt) {
|
||||
break;
|
||||
}
|
||||
currentPath = path.dirname(currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureTemplatesDirMigrated(projectRoot: string = process.cwd()): TemplateMigrationResult {
|
||||
const templatesDir = getTemplatesDir(projectRoot);
|
||||
const legacyTemplatesDir = getLegacyTemplatesDir(projectRoot);
|
||||
const result: TemplateMigrationResult = {
|
||||
moved: [],
|
||||
deduped: [],
|
||||
conflicts: [],
|
||||
removedLegacyDir: false,
|
||||
};
|
||||
|
||||
if (!fs.existsSync(legacyTemplatesDir)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
fs.mkdirSync(templatesDir, { recursive: true });
|
||||
|
||||
const walkLegacyDir = (directoryPath: string) => {
|
||||
const entries = fs.readdirSync(directoryPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.name.startsWith('.')) {
|
||||
const hiddenPath = path.join(directoryPath, entry.name);
|
||||
if (entry.isFile()) {
|
||||
fs.unlinkSync(hiddenPath);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const legacyPath = path.join(directoryPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkLegacyDir(legacyPath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relativePath = path.relative(legacyTemplatesDir, legacyPath).split(path.sep).join('/');
|
||||
if (!relativePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetPath = path.join(templatesDir, relativePath);
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
fs.renameSync(legacyPath, targetPath);
|
||||
result.moved.push(relativePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (areFilesIdentical(legacyPath, targetPath)) {
|
||||
fs.unlinkSync(legacyPath);
|
||||
result.deduped.push(relativePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.conflicts.push({
|
||||
relativePath,
|
||||
legacyPath,
|
||||
targetPath,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
walkLegacyDir(legacyTemplatesDir);
|
||||
removeEmptyDirectories(legacyTemplatesDir, legacyTemplatesDir);
|
||||
result.removedLegacyDir = !fs.existsSync(legacyTemplatesDir);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resolveDocumentPathFromDocUrl(
|
||||
docUrl: string,
|
||||
host?: string,
|
||||
projectRoot: string = process.cwd(),
|
||||
): { docPath: string } | { status: number; error: string } {
|
||||
let pathname = '';
|
||||
try {
|
||||
pathname = new URL(docUrl, `http://${host || 'localhost'}`).pathname;
|
||||
} catch {
|
||||
return { status: 400, error: 'Invalid docUrl' };
|
||||
}
|
||||
|
||||
const srcRoot = path.resolve(projectRoot, 'src');
|
||||
const docsRoot = getDocsDir(projectRoot);
|
||||
|
||||
if (pathname.startsWith('/api/docs/')) {
|
||||
const encodedDocName = pathname.slice('/api/docs/'.length);
|
||||
if (!encodedDocName) {
|
||||
return { status: 400, error: 'Missing document name in docUrl' };
|
||||
}
|
||||
|
||||
const decodedDocName = safeDecodeURIComponent(encodedDocName);
|
||||
const docPath = path.resolve(docsRoot, decodedDocName);
|
||||
if (!isPathInside(docsRoot, docPath)) {
|
||||
return { status: 403, error: 'Forbidden path' };
|
||||
}
|
||||
return { docPath };
|
||||
}
|
||||
|
||||
if (pathname.startsWith('/docs/')) {
|
||||
const rawDocPath = pathname.slice('/docs/'.length);
|
||||
if (!rawDocPath) {
|
||||
return { status: 400, error: 'Missing document name in docUrl' };
|
||||
}
|
||||
|
||||
const decodedDocPath = safeDecodeURIComponent(rawDocPath);
|
||||
const normalizedDocPath = decodedDocPath.toLowerCase().endsWith('.md')
|
||||
? decodedDocPath
|
||||
: `${decodedDocPath}.md`;
|
||||
const docPath = path.resolve(docsRoot, normalizedDocPath);
|
||||
if (!isPathInside(docsRoot, docPath)) {
|
||||
return { status: 403, error: 'Forbidden path' };
|
||||
}
|
||||
return { docPath };
|
||||
}
|
||||
|
||||
const specMatch = pathname.match(/^\/(components|prototypes|themes)\/([^/]+)\/(spec|prd)\.md$/i);
|
||||
if (specMatch) {
|
||||
const entryType = specMatch[1].toLowerCase();
|
||||
const entryName = safeDecodeURIComponent(specMatch[2]);
|
||||
const docName = `${specMatch[3].toLowerCase()}.md`;
|
||||
const entryRoot = path.resolve(srcRoot, entryType);
|
||||
const docPath = path.resolve(entryRoot, entryName, docName);
|
||||
|
||||
if (!isPathInside(entryRoot, docPath)) {
|
||||
return { status: 403, error: 'Forbidden path' };
|
||||
}
|
||||
return { docPath };
|
||||
}
|
||||
|
||||
return { status: 400, error: 'Unsupported docUrl path' };
|
||||
}
|
||||
|
||||
export function sanitizeImageUploadFileName(originalName: string, mimeType?: string): string {
|
||||
const normalizedMimeType = String(mimeType || '').toLowerCase();
|
||||
const extensionByMime = SPEC_DOC_IMAGE_MIME_TO_EXTENSION[normalizedMimeType] || '';
|
||||
const rawExt = path.extname(originalName || '').toLowerCase();
|
||||
const extension = SPEC_DOC_IMAGE_ALLOWED_EXTENSIONS.has(rawExt)
|
||||
? rawExt
|
||||
: (SPEC_DOC_IMAGE_ALLOWED_EXTENSIONS.has(extensionByMime) ? extensionByMime : '.png');
|
||||
|
||||
const rawBaseName = path.basename(originalName || '', path.extname(originalName || '')).trim();
|
||||
const safeBaseName = (rawBaseName || `image-${Date.now().toString(36)}`)
|
||||
.replace(/[\\/:*?"<>|]/g, '-')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '') || `image-${Date.now().toString(36)}`;
|
||||
|
||||
return `${safeBaseName}${extension}`;
|
||||
}
|
||||
|
||||
export function resolveUniqueFilePath(directoryPath: string, fileName: string): string {
|
||||
const ext = path.extname(fileName);
|
||||
const baseName = path.basename(fileName, ext);
|
||||
|
||||
let index = 1;
|
||||
let candidateName = fileName;
|
||||
let candidatePath = path.join(directoryPath, candidateName);
|
||||
|
||||
while (fs.existsSync(candidatePath)) {
|
||||
index += 1;
|
||||
candidateName = `${baseName}-${index}${ext}`;
|
||||
candidatePath = path.join(directoryPath, candidateName);
|
||||
}
|
||||
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
export function scanDocReferences(docName: string, projectRoot: string = process.cwd()) {
|
||||
const normalizedDocName = normalizePathSegments(docName);
|
||||
if (!normalizedDocName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const docsRoot = getDocsDir(projectRoot);
|
||||
const currentDocPath = path.resolve(docsRoot, normalizedDocName);
|
||||
const referencePatterns = buildDocReferencePatterns(normalizedDocName);
|
||||
if (referencePatterns.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const references = new Set<string>();
|
||||
|
||||
const walkDir = (directoryPath: string) => {
|
||||
if (!fs.existsSync(directoryPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(directoryPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(directoryPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkDir(entryPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (path.resolve(entryPath) === currentDocPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (!DOC_REFERENCE_ALLOWED_EXTENSIONS.has(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(entryPath, 'utf8');
|
||||
if (referencePatterns.some((pattern) => pattern.test(content))) {
|
||||
references.add(normalizeRelativeProjectPath(projectRoot, entryPath));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DOC_REFERENCE_SCAN_DIRECTORIES
|
||||
.map((relativePath) => path.resolve(projectRoot, relativePath))
|
||||
.forEach(walkDir);
|
||||
|
||||
return Array.from(references).sort();
|
||||
}
|
||||
|
||||
export function scanTemplateReferences(templateName: string, projectRoot: string = process.cwd()) {
|
||||
const normalizedTemplateName = normalizePathSegments(templateName);
|
||||
if (!normalizedTemplateName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const templatesRoot = getTemplatesDir(projectRoot);
|
||||
const currentTemplatePath = path.resolve(templatesRoot, normalizedTemplateName);
|
||||
const referencePatterns = buildTemplateReferencePatterns(normalizedTemplateName);
|
||||
if (referencePatterns.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const references = new Set<string>();
|
||||
|
||||
const walkDir = (directoryPath: string) => {
|
||||
if (!fs.existsSync(directoryPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(directoryPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(directoryPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkDir(entryPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (path.resolve(entryPath) === currentTemplatePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (!DOC_REFERENCE_ALLOWED_EXTENSIONS.has(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(entryPath, 'utf8');
|
||||
if (referencePatterns.some((pattern) => pattern.test(content))) {
|
||||
references.add(normalizeRelativeProjectPath(projectRoot, entryPath));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEMPLATE_REFERENCE_SCAN_DIRECTORIES
|
||||
.map((relativePath) => path.resolve(projectRoot, relativePath))
|
||||
.forEach(walkDir);
|
||||
|
||||
return Array.from(references).sort();
|
||||
}
|
||||
|
||||
export function createManualDocTemplate(displayName: string) {
|
||||
return `# ${displayName}
|
||||
|
||||
## 概述
|
||||
请在此补充文档目标、范围与背景信息。
|
||||
|
||||
## 详细内容
|
||||
请在此继续编写正文。
|
||||
`;
|
||||
}
|
||||
|
||||
export function extractMarkdownDisplayName(content: string, fallbackName: string) {
|
||||
const titleMatch = content.match(/^#\s+(.+)$/m);
|
||||
return titleMatch?.[1]?.trim() || fallbackName;
|
||||
}
|
||||
|
||||
export function extractMarkdownDescription(content: string) {
|
||||
const lines = content.split('\n');
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (line && !line.startsWith('#')) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function listTemplateAssets(templatesDir: string) {
|
||||
const templates: Array<{ name: string; displayName: string; description: string }> = [];
|
||||
|
||||
if (!fs.existsSync(templatesDir)) {
|
||||
return templates;
|
||||
}
|
||||
|
||||
const walkTemplatesDir = (dirPath: string) => {
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
entries.forEach((entry) => {
|
||||
if (!entry || entry.name.startsWith('.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkTemplatesDir(fullPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entry.isFile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relativePath = path.relative(templatesDir, fullPath).split(path.sep).join('/');
|
||||
const content = fs.readFileSync(fullPath, 'utf8');
|
||||
templates.push({
|
||||
name: relativePath,
|
||||
displayName: relativePath.replace(/\.[^./\\]+$/u, ''),
|
||||
description: extractMarkdownDescription(content),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
walkTemplatesDir(templatesDir);
|
||||
templates.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')));
|
||||
return templates;
|
||||
}
|
||||
|
||||
export function sanitizeImportFileBaseName(fileName: string): string {
|
||||
return String(fileName || '')
|
||||
.trim()
|
||||
.replace(/\.[^/.]+$/, '')
|
||||
.replace(/[\\/:*?"<>|]/g, '-')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
export function resolveUniqueMarkdownPath(docsDir: string, baseName: string) {
|
||||
const safeBaseName = sanitizeImportFileBaseName(baseName) || `doc-${Date.now().toString(36)}`;
|
||||
let fileName = `${safeBaseName}.md`;
|
||||
let nextPath = path.join(docsDir, fileName);
|
||||
let suffix = 1;
|
||||
while (fs.existsSync(nextPath)) {
|
||||
fileName = `${safeBaseName}-${suffix}.md`;
|
||||
nextPath = path.join(docsDir, fileName);
|
||||
suffix += 1;
|
||||
}
|
||||
return { fileName, absolutePath: nextPath };
|
||||
}
|
||||
55
axhub-make/vite-plugins/utils/entriesManifest.ts
Normal file
55
axhub-make/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);
|
||||
}
|
||||
247
axhub-make/vite-plugins/utils/entriesManifestCore.js
Normal file
247
axhub-make/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);
|
||||
}
|
||||
186
axhub-make/vite-plugins/utils/entryScanner.ts
Normal file
186
axhub-make/vite-plugins/utils/entryScanner.ts
Normal 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;
|
||||
}
|
||||
17
axhub-make/vite-plugins/utils/fileUtils.ts
Normal file
17
axhub-make/vite-plugins/utils/fileUtils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import fs from 'fs';
|
||||
|
||||
/**
|
||||
* 从文件注释中读取显示名称
|
||||
*/
|
||||
export function getDisplayName(filePath: string): string | null {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const nameMatch = content.match(/@(?:name|displayName)\s+(.+)/);
|
||||
if (nameMatch && nameMatch[1]) {
|
||||
return nameMatch[1].trim();
|
||||
}
|
||||
} catch (err) {
|
||||
// 忽略读取错误
|
||||
}
|
||||
return null;
|
||||
}
|
||||
125
axhub-make/vite-plugins/utils/httpUtils.ts
Normal file
125
axhub-make/vite-plugins/utils/httpUtils.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { networkInterfaces } from 'os';
|
||||
import archiver from 'archiver';
|
||||
|
||||
import { buildAttachmentContentDisposition } from './contentDisposition';
|
||||
|
||||
export function getLocalIP(): string {
|
||||
const interfaces = networkInterfaces();
|
||||
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
const nets = interfaces[name];
|
||||
if (!nets) continue;
|
||||
|
||||
for (const net of nets) {
|
||||
if (net.family === 'IPv4' && !net.internal) {
|
||||
return net.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'localhost';
|
||||
}
|
||||
|
||||
export function getRequestPathname(req: any): string {
|
||||
try {
|
||||
return new URL(req.url || '/', `http://${req.headers.host}`).pathname;
|
||||
} catch {
|
||||
return (req.url || '/').split('?')[0];
|
||||
}
|
||||
}
|
||||
|
||||
export function readJsonBody(req: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk: Buffer) => {
|
||||
body += chunk.toString('utf8');
|
||||
});
|
||||
req.on('end', () => {
|
||||
if (!body) {
|
||||
resolve({});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(body));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
export function readRequestBody(req: any): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on('data', (chunk: Buffer | string) => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
});
|
||||
req.on('end', () => {
|
||||
resolve(Buffer.concat(chunks).toString('utf8'));
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
export function readErrorString(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
export function limitErrorText(value: string, maxLength: number = 500): string {
|
||||
if (value.length <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return `${value.slice(0, maxLength)}...`;
|
||||
}
|
||||
|
||||
export function serializeErrorForLog(error: any) {
|
||||
const cause = error?.cause;
|
||||
const stack = readErrorString(error?.stack);
|
||||
const causeStack = readErrorString(cause?.stack);
|
||||
|
||||
return {
|
||||
name: readErrorString(error?.name) || undefined,
|
||||
message: readErrorString(error?.message) || undefined,
|
||||
code: readErrorString(error?.code) || undefined,
|
||||
errno: readErrorString(error?.errno) || undefined,
|
||||
syscall: readErrorString(error?.syscall) || undefined,
|
||||
address: readErrorString(error?.address) || undefined,
|
||||
port: typeof error?.port === 'number' ? error.port : undefined,
|
||||
causeName: readErrorString(cause?.name) || undefined,
|
||||
causeMessage: readErrorString(cause?.message) || undefined,
|
||||
causeCode: readErrorString(cause?.code) || undefined,
|
||||
causeErrno: readErrorString(cause?.errno) || undefined,
|
||||
causeSyscall: readErrorString(cause?.syscall) || undefined,
|
||||
causeAddress: readErrorString(cause?.address) || undefined,
|
||||
causePort: typeof cause?.port === 'number' ? cause.port : undefined,
|
||||
stack: stack ? limitErrorText(stack, 1200) : undefined,
|
||||
causeStack: causeStack ? limitErrorText(causeStack, 1200) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function streamDirectoryAsZip(res: any, sourceDir: string, fileName: string) {
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.setHeader('Content-Disposition', buildAttachmentContentDisposition(fileName));
|
||||
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
|
||||
archive.on('warning', (warning: any) => {
|
||||
console.warn('[download-dist-plugin] ZIP warning:', warning);
|
||||
});
|
||||
|
||||
archive.on('error', (error: any) => {
|
||||
console.error('[download-dist-plugin] ZIP error:', error);
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 500;
|
||||
res.end(JSON.stringify({ error: `Failed to create zip: ${error.message}` }));
|
||||
return;
|
||||
}
|
||||
res.destroy(error);
|
||||
});
|
||||
|
||||
archive.pipe(res);
|
||||
archive.directory(sourceDir, false);
|
||||
void archive.finalize();
|
||||
}
|
||||
20
axhub-make/vite-plugins/utils/installSkillTargets.ts
Normal file
20
axhub-make/vite-plugins/utils/installSkillTargets.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const INSTALL_SKILL_TARGET_DIRS: Record<string, string | null> = {
|
||||
// IDE editors
|
||||
cursor: '.cursor/skills',
|
||||
windsurf: '.windsurf/skills',
|
||||
qoder: 'skills',
|
||||
trae: '.trae/skills',
|
||||
trae_cn: '.trae/skills',
|
||||
vscode: '.github/skills',
|
||||
kiro: '.kiro/skills',
|
||||
antigravity: '.agent/skills',
|
||||
// CLI / Genie tools
|
||||
'claude-code': '.claude/skills',
|
||||
// Codex project-level skills follow the official recommended location.
|
||||
codex: '.agents/skills',
|
||||
opencode: '.opencode/skills',
|
||||
};
|
||||
|
||||
export function getInstallSkillTargetDir(client: string): string | null {
|
||||
return INSTALL_SKILL_TARGET_DIRS[client] ?? null;
|
||||
}
|
||||
7
axhub-make/vite-plugins/utils/makeConstants.ts
Normal file
7
axhub-make/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';
|
||||
446
axhub-make/vite-plugins/utils/markitdown.ts
Normal file
446
axhub-make/vite-plugins/utils/markitdown.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
export const DOC_IMPORT_MAX_FILE_SIZE = 30 * 1024 * 1024;
|
||||
export const DOC_IMPORT_MAX_FILE_COUNT = 30;
|
||||
export const DOC_IMPORT_MAX_TOTAL_SIZE = 150 * 1024 * 1024;
|
||||
|
||||
export const DOC_IMPORT_SUPPORTED_EXTENSIONS = new Set([
|
||||
'.md',
|
||||
'.txt',
|
||||
'.pdf',
|
||||
'.docx',
|
||||
'.pptx',
|
||||
'.xlsx',
|
||||
'.csv',
|
||||
'.json',
|
||||
'.html',
|
||||
'.xml',
|
||||
]);
|
||||
|
||||
type MarkitdownCommandCandidate = {
|
||||
command: string;
|
||||
args: string[];
|
||||
commandSource: string;
|
||||
};
|
||||
|
||||
type PythonRuntimeProbe = {
|
||||
command: string;
|
||||
available: boolean;
|
||||
versionText: string;
|
||||
major: number;
|
||||
minor: number;
|
||||
meetsRequirement: boolean;
|
||||
};
|
||||
|
||||
export type MarkitdownResolvedCommand = {
|
||||
installed: boolean;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
pythonCommand?: string;
|
||||
commandSource: string;
|
||||
version: string;
|
||||
installHints: string[];
|
||||
error: string;
|
||||
};
|
||||
|
||||
type MarkitdownOptionalFeature = {
|
||||
extension: string;
|
||||
feature: string;
|
||||
modules: string[];
|
||||
};
|
||||
|
||||
const MARKITDOWN_MIN_PYTHON_MAJOR = 3;
|
||||
const MARKITDOWN_MIN_PYTHON_MINOR = 10;
|
||||
|
||||
const MARKITDOWN_DIRECT_COMMAND_CANDIDATE: MarkitdownCommandCandidate = {
|
||||
command: 'markitdown',
|
||||
args: [],
|
||||
commandSource: 'markitdown',
|
||||
};
|
||||
|
||||
const MARKITDOWN_PYTHON_COMMAND_CANDIDATES = [
|
||||
'python3.12',
|
||||
'python3.11',
|
||||
'python3.10',
|
||||
'python3',
|
||||
'python',
|
||||
];
|
||||
|
||||
const MARKITDOWN_REQUIRED_OPTIONAL_FEATURES: MarkitdownOptionalFeature[] = [
|
||||
{ extension: '.pdf', feature: 'pdf', modules: ['pdfminer', 'pdfplumber'] },
|
||||
{ extension: '.docx', feature: 'docx', modules: ['mammoth'] },
|
||||
{ extension: '.pptx', feature: 'pptx', modules: ['pptx'] },
|
||||
{ extension: '.xlsx', feature: 'xlsx', modules: ['pandas', 'openpyxl'] },
|
||||
];
|
||||
|
||||
function parsePythonVersionText(versionText: string): { major: number; minor: number } | null {
|
||||
const match = versionText.match(/Python\s+(\d+)\.(\d+)/i);
|
||||
if (!match) return null;
|
||||
return {
|
||||
major: Number(match[1] || 0),
|
||||
minor: Number(match[2] || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function isPythonVersionSupported(major: number, minor: number): boolean {
|
||||
if (major > MARKITDOWN_MIN_PYTHON_MAJOR) return true;
|
||||
if (major < MARKITDOWN_MIN_PYTHON_MAJOR) return false;
|
||||
return minor >= MARKITDOWN_MIN_PYTHON_MINOR;
|
||||
}
|
||||
|
||||
function probePythonRuntime(command: string): PythonRuntimeProbe {
|
||||
const versionAttempt = spawnSync(command, ['--version'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 8000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
if (versionAttempt.error || versionAttempt.status !== 0) {
|
||||
return {
|
||||
command,
|
||||
available: false,
|
||||
versionText: '',
|
||||
major: 0,
|
||||
minor: 0,
|
||||
meetsRequirement: false,
|
||||
};
|
||||
}
|
||||
|
||||
const versionText = String(versionAttempt.stdout || versionAttempt.stderr || '').trim();
|
||||
const parsedVersion = parsePythonVersionText(versionText);
|
||||
const major = parsedVersion?.major || 0;
|
||||
const minor = parsedVersion?.minor || 0;
|
||||
return {
|
||||
command,
|
||||
available: true,
|
||||
versionText,
|
||||
major,
|
||||
minor,
|
||||
meetsRequirement: isPythonVersionSupported(major, minor),
|
||||
};
|
||||
}
|
||||
|
||||
function buildMarkitdownInstallHints(preferredPythonCommand?: string): string[] {
|
||||
const installCommand = `${preferredPythonCommand || 'python3.11'} -m pip install -U 'markitdown[pdf,docx,pptx,xlsx]'`;
|
||||
if (preferredPythonCommand) {
|
||||
return [installCommand];
|
||||
}
|
||||
|
||||
return [
|
||||
'brew install python@3.11',
|
||||
installCommand,
|
||||
];
|
||||
}
|
||||
|
||||
function isCommandWorking(candidate: MarkitdownCommandCandidate): {
|
||||
success: boolean;
|
||||
version: string;
|
||||
details: string;
|
||||
} {
|
||||
const versionAttempt = spawnSync(
|
||||
candidate.command,
|
||||
[...candidate.args, '--version'],
|
||||
{ encoding: 'utf8', timeout: 8000, maxBuffer: 1024 * 1024 },
|
||||
);
|
||||
const versionOutput = String(versionAttempt.stdout || versionAttempt.stderr || '').trim();
|
||||
if (!versionAttempt.error && versionAttempt.status === 0) {
|
||||
return {
|
||||
success: true,
|
||||
version: versionOutput || 'unknown',
|
||||
details: '',
|
||||
};
|
||||
}
|
||||
|
||||
const helpAttempt = spawnSync(
|
||||
candidate.command,
|
||||
[...candidate.args, '--help'],
|
||||
{ encoding: 'utf8', timeout: 8000, maxBuffer: 1024 * 1024 },
|
||||
);
|
||||
const helpOutput = String(helpAttempt.stdout || helpAttempt.stderr || '').trim();
|
||||
if (!helpAttempt.error && helpAttempt.status === 0) {
|
||||
return {
|
||||
success: true,
|
||||
version: versionOutput || 'available',
|
||||
details: '',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
version: '',
|
||||
details: [versionOutput, helpOutput].filter(Boolean).join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMarkitdownPythonCommand(candidate: MarkitdownCommandCandidate): string | undefined {
|
||||
if (candidate.args[0] === '-m' && candidate.args[1] === 'markitdown') {
|
||||
return candidate.command;
|
||||
}
|
||||
|
||||
const whichAttempt = spawnSync('which', [candidate.command], {
|
||||
encoding: 'utf8',
|
||||
timeout: 8000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
if (whichAttempt.error || whichAttempt.status !== 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const executablePath = String(whichAttempt.stdout || '').trim().split(/\r?\n/)[0]?.trim();
|
||||
if (!executablePath || !fs.existsSync(executablePath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const firstLine = fs.readFileSync(executablePath, 'utf8').split(/\r?\n/, 1)[0]?.trim() || '';
|
||||
if (!firstLine.startsWith('#!')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const shebang = firstLine.slice(2).trim();
|
||||
if (!shebang) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const shebangParts = shebang.split(/\s+/).filter(Boolean);
|
||||
if (shebangParts[0] === '/usr/bin/env') {
|
||||
return shebangParts[1];
|
||||
}
|
||||
|
||||
return shebangParts[0];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function probeMarkitdownOptionalDependencies(pythonCommand?: string): {
|
||||
ready: boolean;
|
||||
missingExtensions: string[];
|
||||
error: string;
|
||||
installHints: string[];
|
||||
} {
|
||||
if (!pythonCommand) {
|
||||
return {
|
||||
ready: true,
|
||||
missingExtensions: [],
|
||||
error: '',
|
||||
installHints: [],
|
||||
};
|
||||
}
|
||||
|
||||
const checksJson = JSON.stringify(MARKITDOWN_REQUIRED_OPTIONAL_FEATURES);
|
||||
const probeScript = [
|
||||
'import importlib.util, json',
|
||||
`checks = json.loads(${JSON.stringify(checksJson)})`,
|
||||
'missing = []',
|
||||
'for item in checks:',
|
||||
" missing_modules = [name for name in item['modules'] if importlib.util.find_spec(name) is None]",
|
||||
' if missing_modules:',
|
||||
" missing.append({'extension': item['extension'], 'feature': item['feature'], 'missingModules': missing_modules})",
|
||||
"print(json.dumps({'missing': missing}, ensure_ascii=False))",
|
||||
].join('\n');
|
||||
|
||||
const probeAttempt = spawnSync(pythonCommand, ['-c', probeScript], {
|
||||
encoding: 'utf8',
|
||||
timeout: 8000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
if (probeAttempt.error || probeAttempt.status !== 0) {
|
||||
return {
|
||||
ready: false,
|
||||
missingExtensions: [],
|
||||
error: `markitdown 依赖检测失败,请执行 ${buildMarkitdownInstallHints(pythonCommand)[0]} 后重试。`,
|
||||
installHints: buildMarkitdownInstallHints(pythonCommand),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(String(probeAttempt.stdout || '{}')) as {
|
||||
missing?: Array<{ extension?: string; missingModules?: string[] }>;
|
||||
};
|
||||
const missing = Array.isArray(payload?.missing) ? payload.missing : [];
|
||||
if (missing.length === 0) {
|
||||
return {
|
||||
ready: true,
|
||||
missingExtensions: [],
|
||||
error: '',
|
||||
installHints: [],
|
||||
};
|
||||
}
|
||||
|
||||
const missingDescriptions = missing.map((item) => {
|
||||
const extension = String(item?.extension || '').trim() || 'unknown';
|
||||
const missingModules = Array.isArray(item?.missingModules) ? item.missingModules.filter(Boolean) : [];
|
||||
return `${extension}${missingModules.length > 0 ? `(缺少:${missingModules.join(', ')})` : ''}`;
|
||||
});
|
||||
|
||||
return {
|
||||
ready: false,
|
||||
missingExtensions: missing.map((item) => String(item?.extension || '').trim()).filter(Boolean),
|
||||
error: `markitdown 已安装,但以下格式依赖不完整:${missingDescriptions.join('、')}。请先安装完整依赖后再导入非 .md 文档。`,
|
||||
installHints: buildMarkitdownInstallHints(pythonCommand),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
ready: false,
|
||||
missingExtensions: [],
|
||||
error: `markitdown 依赖检测结果无法解析,请执行 ${buildMarkitdownInstallHints(pythonCommand)[0]} 后重试。`,
|
||||
installHints: buildMarkitdownInstallHints(pythonCommand),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveMarkitdownCommand(): MarkitdownResolvedCommand {
|
||||
const directCommandResult = isCommandWorking(MARKITDOWN_DIRECT_COMMAND_CANDIDATE);
|
||||
if (directCommandResult.success) {
|
||||
const pythonCommand = resolveMarkitdownPythonCommand(MARKITDOWN_DIRECT_COMMAND_CANDIDATE);
|
||||
const dependencyProbe = probeMarkitdownOptionalDependencies(pythonCommand);
|
||||
return {
|
||||
installed: dependencyProbe.ready,
|
||||
command: MARKITDOWN_DIRECT_COMMAND_CANDIDATE.command,
|
||||
args: MARKITDOWN_DIRECT_COMMAND_CANDIDATE.args,
|
||||
pythonCommand,
|
||||
commandSource: MARKITDOWN_DIRECT_COMMAND_CANDIDATE.commandSource,
|
||||
version: directCommandResult.version,
|
||||
installHints: dependencyProbe.installHints,
|
||||
error: dependencyProbe.error,
|
||||
};
|
||||
}
|
||||
|
||||
const pythonRuntimeProbes = MARKITDOWN_PYTHON_COMMAND_CANDIDATES
|
||||
.map((command) => probePythonRuntime(command))
|
||||
.filter((probe, index, allProbes) => allProbes.findIndex((item) => item.command === probe.command) === index);
|
||||
const supportedPythonRuntimes = pythonRuntimeProbes.filter((probe) => probe.available && probe.meetsRequirement);
|
||||
const preferredPythonCommand = supportedPythonRuntimes[0]?.command;
|
||||
|
||||
let sawLegacyPackage = false;
|
||||
let sawModuleMissing = false;
|
||||
|
||||
for (const pythonRuntime of supportedPythonRuntimes) {
|
||||
const pythonCandidate: MarkitdownCommandCandidate = {
|
||||
command: pythonRuntime.command,
|
||||
args: ['-m', 'markitdown'],
|
||||
commandSource: `${pythonRuntime.command} -m markitdown`,
|
||||
};
|
||||
const candidateResult = isCommandWorking(pythonCandidate);
|
||||
if (candidateResult.success) {
|
||||
const dependencyProbe = probeMarkitdownOptionalDependencies(pythonRuntime.command);
|
||||
return {
|
||||
installed: dependencyProbe.ready,
|
||||
command: pythonCandidate.command,
|
||||
args: pythonCandidate.args,
|
||||
pythonCommand: pythonRuntime.command,
|
||||
commandSource: pythonCandidate.commandSource,
|
||||
version: candidateResult.version,
|
||||
installHints: dependencyProbe.installHints,
|
||||
error: dependencyProbe.error,
|
||||
};
|
||||
}
|
||||
|
||||
const details = candidateResult.details || '';
|
||||
if (/markitdown\.__main__|cannot be directly executed/i.test(details)) {
|
||||
sawLegacyPackage = true;
|
||||
} else if (/No module named markitdown/i.test(details)) {
|
||||
sawModuleMissing = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (supportedPythonRuntimes.length === 0) {
|
||||
const availableVersions = pythonRuntimeProbes
|
||||
.filter((probe) => probe.available)
|
||||
.map((probe) => `${probe.command} (${probe.versionText || 'unknown'})`)
|
||||
.join(', ');
|
||||
return {
|
||||
installed: false,
|
||||
commandSource: 'unavailable',
|
||||
version: '',
|
||||
installHints: buildMarkitdownInstallHints(),
|
||||
error: availableVersions
|
||||
? `markitdown 需要 Python 3.10+。当前仅检测到:${availableVersions}`
|
||||
: 'markitdown 需要 Python 3.10+。当前未检测到可用的 Python 运行时。',
|
||||
};
|
||||
}
|
||||
|
||||
if (sawLegacyPackage) {
|
||||
return {
|
||||
installed: false,
|
||||
commandSource: 'unavailable',
|
||||
version: '',
|
||||
installHints: buildMarkitdownInstallHints(preferredPythonCommand),
|
||||
error: '检测到旧版 markitdown(例如 0.0.1a1),该版本没有 CLI 入口。请在 Python 3.10+ 环境重新安装最新版。',
|
||||
};
|
||||
}
|
||||
|
||||
if (sawModuleMissing) {
|
||||
return {
|
||||
installed: false,
|
||||
commandSource: 'unavailable',
|
||||
version: '',
|
||||
installHints: buildMarkitdownInstallHints(preferredPythonCommand),
|
||||
error: `未在 ${preferredPythonCommand || 'Python 3.10+'} 环境中检测到 markitdown,请先安装后重试。`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
installed: false,
|
||||
commandSource: 'unavailable',
|
||||
version: '',
|
||||
installHints: buildMarkitdownInstallHints(preferredPythonCommand),
|
||||
error: 'markitdown 不可用,请安装后重试。',
|
||||
};
|
||||
}
|
||||
|
||||
export function convertFileToMarkdownWithMarkitdown(params: {
|
||||
command: string;
|
||||
args: string[];
|
||||
sourcePath: string;
|
||||
}): { success: true; content: string } | { success: false; error: string } {
|
||||
const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'axhub-doc-import-'));
|
||||
const outputPath = path.join(tempDir, 'output.md');
|
||||
try {
|
||||
const result = spawnSync(
|
||||
params.command,
|
||||
[...params.args, '--keep-data-uris', params.sourcePath, '-o', outputPath],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
timeout: 120000,
|
||||
maxBuffer: 1024 * 1024 * 20,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error.message || 'markitdown execution failed',
|
||||
};
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
const stderr = String(result.stderr || '').trim();
|
||||
const stdout = String(result.stdout || '').trim();
|
||||
const details = stderr || stdout || `exit code ${result.status}`;
|
||||
return {
|
||||
success: false,
|
||||
error: `markitdown convert failed: ${details}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'markitdown did not produce output file',
|
||||
};
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(outputPath, 'utf8');
|
||||
return {
|
||||
success: true,
|
||||
content,
|
||||
};
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
157
axhub-make/vite-plugins/utils/proxyUtils.ts
Normal file
157
axhub-make/vite-plugins/utils/proxyUtils.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { limitErrorText, readErrorString } from './httpUtils';
|
||||
|
||||
export function formatAxureProxyErrorDetails(error: any): string {
|
||||
const parts: string[] = [];
|
||||
const message = readErrorString(error?.message);
|
||||
const causeMessage = readErrorString(error?.cause?.message);
|
||||
const code = readErrorString(error?.code) || readErrorString(error?.cause?.code);
|
||||
const errno = readErrorString(error?.errno) || readErrorString(error?.cause?.errno);
|
||||
const syscall = readErrorString(error?.syscall) || readErrorString(error?.cause?.syscall);
|
||||
const address = readErrorString(error?.address) || readErrorString(error?.cause?.address);
|
||||
const port =
|
||||
typeof error?.port === 'number'
|
||||
? String(error.port)
|
||||
: typeof error?.cause?.port === 'number'
|
||||
? String(error.cause.port)
|
||||
: '';
|
||||
|
||||
if (message) {
|
||||
parts.push(message);
|
||||
}
|
||||
if (causeMessage && causeMessage !== message) {
|
||||
parts.push(`cause=${causeMessage}`);
|
||||
}
|
||||
if (code) {
|
||||
parts.push(`code=${code}`);
|
||||
}
|
||||
if (errno && errno !== code) {
|
||||
parts.push(`errno=${errno}`);
|
||||
}
|
||||
if (syscall) {
|
||||
parts.push(`syscall=${syscall}`);
|
||||
}
|
||||
if (address) {
|
||||
parts.push(`address=${address}`);
|
||||
}
|
||||
if (port) {
|
||||
parts.push(`port=${port}`);
|
||||
}
|
||||
|
||||
return parts.join('; ') || 'Unknown upstream error';
|
||||
}
|
||||
|
||||
export function normalizeAxvgPayloadText(rawBody: string): string {
|
||||
const source = rawBody.trim();
|
||||
if (!source) {
|
||||
return '// axvg\n{}';
|
||||
}
|
||||
|
||||
if (source.startsWith('// axvg')) {
|
||||
return source;
|
||||
}
|
||||
|
||||
return `// axvg\n${source}`;
|
||||
}
|
||||
|
||||
export function buildAxureBridgeUnavailablePayload(params: {
|
||||
route: string;
|
||||
method: string;
|
||||
bridgeUrl: string;
|
||||
payloadBytes?: number;
|
||||
error?: any;
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
responseText?: string;
|
||||
}) {
|
||||
const errorCode =
|
||||
readErrorString(params.error?.code)
|
||||
|| readErrorString(params.error?.cause?.code)
|
||||
|| undefined;
|
||||
const errorMessage =
|
||||
readErrorString(params.error?.message)
|
||||
|| readErrorString(params.responseText)
|
||||
|| (typeof params.status === 'number' ? `Axure Bridge unavailable (HTTP ${params.status})` : 'Axure Bridge unavailable');
|
||||
const details =
|
||||
params.error
|
||||
? formatAxureProxyErrorDetails(params.error)
|
||||
: limitErrorText(readErrorString(params.responseText), 800) || undefined;
|
||||
|
||||
return {
|
||||
available: false,
|
||||
running: false,
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
details,
|
||||
code: errorCode,
|
||||
route: params.route,
|
||||
method: params.method,
|
||||
bridgeUrl: params.bridgeUrl,
|
||||
payloadBytes: params.payloadBytes || undefined,
|
||||
status: typeof params.status === 'number' ? params.status : undefined,
|
||||
statusText: readErrorString(params.statusText) || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function isLoopbackOrPrivateHostname(hostname: string): boolean {
|
||||
const normalized = String(hostname || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
normalized === 'localhost' ||
|
||||
normalized === '127.0.0.1' ||
|
||||
normalized === '0.0.0.0' ||
|
||||
normalized === '::1' ||
|
||||
normalized === '[::1]'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^127\./.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^10\./.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^192\.168\./.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^169\.254\./.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const match172 = normalized.match(/^172\.(\d{1,3})\./);
|
||||
if (match172) {
|
||||
const secondOctet = Number(match172[1]);
|
||||
if (secondOctet >= 16 && secondOctet <= 31) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isAllowedProxyImageUrl(rawUrl: string): boolean {
|
||||
let parsedUrl: URL;
|
||||
try {
|
||||
parsedUrl = new URL(rawUrl);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isLoopbackOrPrivateHostname(parsedUrl.hostname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export { limitErrorText };
|
||||
237
axhub-make/vite-plugins/utils/sidebarTreeStore.ts
Normal file
237
axhub-make/vite-plugins/utils/sidebarTreeStore.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user