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

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

859 lines
28 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 代码检查插件
*
* 提供 /api/code-review 端点,用于检查代码是否符合开发规范
*
* 检查项目:
* 1. 导出规范:必须存在 `export default`
* - 仅在 Axure 导出检查模式下,默认导出名必须是 `Component`
* 2. Tailwind CSS如果使用了 Tailwind 类名,必须在 style.css 中添加 `@import "tailwindcss"`
* 3. Axure API如果使用了 Axure API必须符合 axure-types.ts 的类型定义
*/
import type { Plugin } from 'vite';
import fs from 'fs';
import path from 'path';
import ts from 'typescript';
// 检查结果类型
export interface ReviewIssue {
type: 'error' | 'warning';
rule: string;
message: string;
line?: number;
suggestion?: string;
}
export interface ReviewResult {
file: string;
passed: boolean;
issues: ReviewIssue[];
}
interface ReviewOptions {
enforceComponentExportName?: boolean;
}
export type AxureApiListKey = 'eventList' | 'actionList' | 'varList' | 'configList' | 'dataList';
export interface AxureApiListPreview {
sourceKey: string | null;
raw: string | null;
items: Array<Record<string, unknown>>;
parseStatus: 'parsed' | 'raw' | 'missing';
warnings: string[];
}
export interface AxureApiPreviewResult {
file: string;
passedSourceCheck: boolean;
hasAxureHandle: boolean;
lists: Record<AxureApiListKey, AxureApiListPreview>;
}
const AXURE_LIST_KEYS: AxureApiListKey[] = ['eventList', 'actionList', 'varList', 'configList', 'dataList'];
function extractDefaultExportName(content: string): string | null {
const namedFunctionMatch = content.match(/export\s+default\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/);
if (namedFunctionMatch) {
return namedFunctionMatch[1];
}
const namedClassMatch = content.match(/export\s+default\s+class\s+([A-Za-z_$][\w$]*)\b/);
if (namedClassMatch) {
return namedClassMatch[1];
}
const identifierMatch = content.match(/export\s+default\s+(?!function\b|class\b)([A-Za-z_$][\w$]*)\s*;?/);
if (identifierMatch) {
return identifierMatch[1];
}
return null;
}
/**
* 检查文件是否包含 default export
*/
function checkExportDefault(content: string, filePath: string, options: ReviewOptions = {}): ReviewIssue[] {
const issues: ReviewIssue[] = [];
// 检查是否有 default export不限制导出名称
const hasExportDefault = /\bexport\s+default\b/.test(content);
if (!hasExportDefault) {
issues.push({
type: 'error',
rule: 'export-default',
message: '缺少 export default 导出',
suggestion: '请添加 default 导出例如export default MyComponent'
});
return issues;
}
if (options.enforceComponentExportName) {
const exportedName = extractDefaultExportName(content);
if (exportedName !== 'Component') {
issues.push({
type: 'error',
rule: 'export-default-name',
message: exportedName
? `导出名称错误:使用了 "${exportedName}"Axure 导出检查要求使用 "Component"`
: 'Axure 导出检查要求默认导出为命名变量 "Component"',
suggestion: '请使用 `const Component = ...` 并导出 `export default Component`'
});
}
}
return issues;
}
/**
* 检查 Tailwind CSS 配置
*/
function checkTailwindCSS(content: string, filePath: string): ReviewIssue[] {
const issues: ReviewIssue[] = [];
// 检查是否使用了 Tailwind 类名(常见的 Tailwind 类名模式)
const tailwindPatterns = [
/className=["'][^"']*\b(flex|grid|block|inline|hidden|relative|absolute|fixed|sticky)\b/,
/className=["'][^"']*\b(w-|h-|m-|p-|text-|bg-|border-|rounded-)/,
/className=["'][^"']*\b(hover:|focus:|active:|disabled:)/,
/className=["'][^"']*\b(sm:|md:|lg:|xl:|2xl:)/
];
const usesTailwind = tailwindPatterns.some(pattern => pattern.test(content));
if (usesTailwind) {
// 检查是否导入了 CSS 文件(支持 style.css, styles.css 等)
const hasStyleImport = /import\s+['"]\.\/[^'"]*\.css['"]/.test(content);
if (!hasStyleImport) {
issues.push({
type: 'error',
rule: 'tailwind-style-import',
message: '使用了 Tailwind CSS 类名,但未导入 CSS 文件',
suggestion: '在文件顶部添加import \'./style.css\' 或 import \'./styles.css\''
});
} else {
// 提取导入的 CSS 文件名
const styleImportMatch = content.match(/import\s+['"]\.\/([^'"]+\.css)['"]/);
if (styleImportMatch) {
const cssFileName = styleImportMatch[1];
const dir = path.dirname(filePath);
const stylePath = path.join(dir, cssFileName);
if (fs.existsSync(stylePath)) {
const styleContent = fs.readFileSync(stylePath, 'utf8');
const hasTailwindImport = /@import\s+["']tailwindcss["']/.test(styleContent);
if (!hasTailwindImport) {
issues.push({
type: 'error',
rule: 'tailwind-css-import',
message: `${cssFileName} 中缺少 @import "tailwindcss"`,
suggestion: `${cssFileName} 文件中添加:@import "tailwindcss";`
});
}
} else {
issues.push({
type: 'error',
rule: 'tailwind-style-file',
message: `导入了 ${cssFileName} 但文件不存在`,
suggestion: `创建 ${cssFileName} 文件并添加:@import "tailwindcss";`
});
}
}
}
}
return issues;
}
/**
* 检查 Axure API 使用规范
*/
function checkAxureAPI(content: string, filePath: string): ReviewIssue[] {
const issues: ReviewIssue[] = [];
// 检查是否使用了 forwardRef
const usesForwardRef = /forwardRef\s*</.test(content);
if (!usesForwardRef) {
// 可能不是 Axure 组件,跳过检查
return issues;
}
// 检查是否导入了 AxureProps 和 AxureHandle支持多行导入
const hasAxurePropsImport = /import\s+type\s*\{[^}]*\bAxureProps\b[^}]*\}\s*from\s+['"].*axure-types/.test(content.replace(/\n/g, ' '));
const hasAxureHandleImport = /import\s+type\s*\{[^}]*\bAxureHandle\b[^}]*\}\s*from\s+['"].*axure-types/.test(content.replace(/\n/g, ' '));
if (!hasAxurePropsImport) {
issues.push({
type: 'error',
rule: 'axure-api-props',
message: '使用了 forwardRef 但未导入 AxureProps 类型',
suggestion: '从 axure-types 导入import type { AxureProps, AxureHandle } from \'../../common/axure-types\''
});
}
if (!hasAxureHandleImport) {
issues.push({
type: 'error',
rule: 'axure-api-handle',
message: '使用了 forwardRef 但未导入 AxureHandle 类型',
suggestion: '从 axure-types 导入import type { AxureProps, AxureHandle } from \'../../common/axure-types\''
});
}
// 检查 forwardRef 类型标注
const forwardRefMatch = content.match(/forwardRef\s*<\s*([^,>]+)\s*,\s*([^>]+)\s*>/);
if (forwardRefMatch) {
const handleType = forwardRefMatch[1].trim();
const propsType = forwardRefMatch[2].trim();
if (handleType !== 'AxureHandle') {
issues.push({
type: 'error',
rule: 'axure-api-handle-type',
message: `forwardRef 第一个类型参数错误:使用了 "${handleType}",应该使用 "AxureHandle"`,
suggestion: '使用正确的类型forwardRef<AxureHandle, AxureProps>'
});
}
if (propsType !== 'AxureProps') {
issues.push({
type: 'error',
rule: 'axure-api-props-type',
message: `forwardRef 第二个类型参数错误:使用了 "${propsType}",应该使用 "AxureProps"`,
suggestion: '使用正确的类型forwardRef<AxureHandle, AxureProps>'
});
}
}
// 检查是否有 useImperativeHandle
const hasUseImperativeHandle = /useImperativeHandle\s*\(/.test(content);
if (usesForwardRef && !hasUseImperativeHandle) {
issues.push({
type: 'warning',
rule: 'axure-api-imperative-handle',
message: '使用了 forwardRef 但未使用 useImperativeHandle',
suggestion: '使用 useImperativeHandle 暴露 AxureHandle 接口'
});
}
// 检查 onEvent 参数类型payload 必须是 string
const onEventCalls = content.match(/onEvent(?:Handler)?\s*\(\s*['"][^'"]+['"]\s*,\s*([^)]+)\)/g);
if (onEventCalls) {
onEventCalls.forEach(call => {
// 检查是否传递了对象字面量作为 payload
if (/\{[^}]+\}/.test(call)) {
issues.push({
type: 'error',
rule: 'axure-api-event-payload',
message: 'onEvent 的 payload 参数必须是字符串类型,不能传递对象',
suggestion: '将对象转换为 JSON 字符串JSON.stringify(payload)'
});
}
});
}
return issues;
}
/**
* 其他推荐检查项(需要用户确认)
*/
function checkRecommended(content: string, filePath: string): ReviewIssue[] {
const issues: ReviewIssue[] = [];
// 检查是否有 @name 注释
const hasNameComment = /@name\s+.+/.test(content);
if (!hasNameComment) {
issues.push({
type: 'warning',
rule: 'file-header-name',
message: '缺少 @name 注释',
suggestion: '在文件头部添加:/**\\n * @name 组件名称\\n */'
});
}
// 检查是否在 JSX 中直接定义函数(性能问题)
const hasInlineFunction = /onClick=\{function\s*\(/.test(content) || /onClick=\{\(\s*\)\s*=>/.test(content);
if (hasInlineFunction) {
issues.push({
type: 'warning',
rule: 'jsx-inline-function',
message: '在 JSX 中直接定义了函数,可能影响性能',
suggestion: '使用 useCallback 预定义函数'
});
}
// 检查是否使用了 ES6 解构 state不推荐
const hasStateDestructure = /const\s*\[\s*\w+\s*,\s*\w+\s*\]\s*=\s*useState/.test(content);
if (hasStateDestructure) {
issues.push({
type: 'warning',
rule: 'state-destructure',
message: '使用了 ES6 解构 state不符合项目规范',
suggestion: '使用数组索引访问const countState = useState(0); const count = countState[0];'
});
}
return issues;
}
/**
* 检查单个文件
*/
function reviewFile(filePath: string, options: ReviewOptions = {}): ReviewResult {
const issues: ReviewIssue[] = [];
try {
if (!fs.existsSync(filePath)) {
return {
file: filePath,
passed: false,
issues: [{
type: 'error',
rule: 'file-not-found',
message: '文件不存在'
}]
};
}
const content = fs.readFileSync(filePath, 'utf8');
console.log('[Code Review] Starting checks for:', filePath);
// 执行各项检查
const exportIssues = checkExportDefault(content, filePath, options);
console.log('[Code Review] Export issues:', exportIssues.length);
issues.push(...exportIssues);
const tailwindIssues = checkTailwindCSS(content, filePath);
console.log('[Code Review] Tailwind issues:', tailwindIssues.length);
issues.push(...tailwindIssues);
const axureIssues = checkAxureAPI(content, filePath);
console.log('[Code Review] Axure issues:', axureIssues.length);
issues.push(...axureIssues);
console.log('[Code Review] Total issues:', issues.length);
} catch (error: any) {
issues.push({
type: 'error',
rule: 'file-read-error',
message: `读取文件失败: ${error.message}`
});
}
// 只有 error 类型的问题才算不通过
const hasErrors = issues.some(issue => issue.type === 'error');
return {
file: filePath,
passed: !hasErrors,
issues
};
}
function createMissingListPreview(): AxureApiListPreview {
return {
sourceKey: null,
raw: null,
items: [],
parseStatus: 'missing',
warnings: [],
};
}
function createEmptyAxureApiPreview(filePath: string, passedSourceCheck: boolean): AxureApiPreviewResult {
return {
file: filePath,
passedSourceCheck,
hasAxureHandle: false,
lists: {
eventList: createMissingListPreview(),
actionList: createMissingListPreview(),
varList: createMissingListPreview(),
configList: createMissingListPreview(),
dataList: createMissingListPreview(),
},
};
}
function isUseImperativeHandleCallee(expression: ts.Expression): boolean {
if (ts.isIdentifier(expression)) {
return expression.text === 'useImperativeHandle';
}
if (ts.isPropertyAccessExpression(expression)) {
return expression.name.text === 'useImperativeHandle';
}
return false;
}
function collectTopLevelConstInitializers(sourceFile: ts.SourceFile): Map<string, ts.Expression> {
const map = new Map<string, ts.Expression>();
sourceFile.statements.forEach((statement) => {
if (!ts.isVariableStatement(statement)) {
return;
}
const isConst = (statement.declarationList.flags & ts.NodeFlags.Const) !== 0;
if (!isConst) {
return;
}
statement.declarationList.declarations.forEach((declaration) => {
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
return;
}
map.set(declaration.name.text, declaration.initializer);
});
});
return map;
}
function getObjectPropertyName(name: ts.PropertyName): string | null {
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
return name.text;
}
return null;
}
function findReturnedObjectFromFunction(fn: ts.Expression | undefined): ts.ObjectLiteralExpression | null {
if (!fn) {
return null;
}
if (ts.isArrowFunction(fn)) {
if (ts.isObjectLiteralExpression(fn.body)) {
return fn.body;
}
if (ts.isParenthesizedExpression(fn.body) && ts.isObjectLiteralExpression(fn.body.expression)) {
return fn.body.expression;
}
if (ts.isBlock(fn.body)) {
for (const statement of fn.body.statements) {
if (ts.isReturnStatement(statement) && statement.expression && ts.isObjectLiteralExpression(statement.expression)) {
return statement.expression;
}
}
}
}
if (ts.isFunctionExpression(fn) || ts.isFunctionDeclaration(fn)) {
if (!fn.body) {
return null;
}
for (const statement of fn.body.statements) {
if (ts.isReturnStatement(statement) && statement.expression && ts.isObjectLiteralExpression(statement.expression)) {
return statement.expression;
}
}
}
return null;
}
interface HandleReturnObjectSearchResult {
hasAxureHandle: boolean;
handleObject: ts.ObjectLiteralExpression | null;
}
function findHandleReturnObject(sourceFile: ts.SourceFile): HandleReturnObjectSearchResult {
const matches: ts.ObjectLiteralExpression[] = [];
let hasCall = false;
const visit = (node: ts.Node) => {
if (ts.isCallExpression(node) && isUseImperativeHandleCallee(node.expression)) {
hasCall = true;
const handleObject = findReturnedObjectFromFunction(node.arguments[1]);
if (handleObject) {
matches.push(handleObject);
}
}
ts.forEachChild(node, visit);
};
ts.forEachChild(sourceFile, visit);
if (!hasCall) {
return { hasAxureHandle: false, handleObject: null };
}
const withTargetKeys = matches.find((objectLiteral) => objectLiteral.properties.some((property) => {
if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) {
return false;
}
const propertyName = getObjectPropertyName(property.name);
return propertyName != null && AXURE_LIST_KEYS.includes(propertyName as AxureApiListKey);
}));
return {
hasAxureHandle: true,
handleObject: withTargetKeys ?? matches[0] ?? null,
};
}
interface ResolvedExpressionResult {
expression: ts.Expression;
warnings: string[];
}
function resolveExpressionFromConstMap(
expression: ts.Expression,
constants: Map<string, ts.Expression>,
): ResolvedExpressionResult {
const warnings: string[] = [];
const visited = new Set<string>();
let current = expression;
while (ts.isIdentifier(current)) {
const key = current.text;
const next = constants.get(key);
if (!next) {
break;
}
if (visited.has(key)) {
warnings.push(`检测到循环引用: ${key}`);
break;
}
visited.add(key);
current = next;
}
return { expression: current, warnings };
}
interface ExpressionToJsonResult {
value: unknown;
unresolved: boolean;
warnings: string[];
}
function expressionToJson(
expression: ts.Expression,
sourceFile: ts.SourceFile,
constants: Map<string, ts.Expression>,
visitedIdentifiers: Set<string> = new Set<string>(),
): ExpressionToJsonResult {
if (ts.isParenthesizedExpression(expression)) {
return expressionToJson(expression.expression, sourceFile, constants, visitedIdentifiers);
}
if (ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression) || ts.isNonNullExpression(expression) || ts.isSatisfiesExpression(expression)) {
return expressionToJson(expression.expression, sourceFile, constants, visitedIdentifiers);
}
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
return { value: expression.text, unresolved: false, warnings: [] };
}
if (ts.isNumericLiteral(expression)) {
return { value: Number(expression.text), unresolved: false, warnings: [] };
}
if (expression.kind === ts.SyntaxKind.TrueKeyword) {
return { value: true, unresolved: false, warnings: [] };
}
if (expression.kind === ts.SyntaxKind.FalseKeyword) {
return { value: false, unresolved: false, warnings: [] };
}
if (expression.kind === ts.SyntaxKind.NullKeyword) {
return { value: null, unresolved: false, warnings: [] };
}
if (ts.isPrefixUnaryExpression(expression)) {
const inner = expressionToJson(expression.operand, sourceFile, constants, visitedIdentifiers);
if (typeof inner.value === 'number') {
if (expression.operator === ts.SyntaxKind.MinusToken) {
return { value: -inner.value, unresolved: inner.unresolved, warnings: inner.warnings };
}
if (expression.operator === ts.SyntaxKind.PlusToken) {
return { value: Number(inner.value), unresolved: inner.unresolved, warnings: inner.warnings };
}
if (expression.operator === ts.SyntaxKind.ExclamationToken) {
return { value: !inner.value, unresolved: inner.unresolved, warnings: inner.warnings };
}
}
const warning = `无法静态解析前缀表达式: ${expression.getText(sourceFile)}`;
return {
value: { __expr: expression.getText(sourceFile) },
unresolved: true,
warnings: [...inner.warnings, warning],
};
}
if (ts.isArrayLiteralExpression(expression)) {
const warnings: string[] = [];
let unresolved = false;
const value = expression.elements.map((element) => {
if (ts.isSpreadElement(element)) {
unresolved = true;
warnings.push(`数组项包含扩展语法: ${element.getText(sourceFile)}`);
return { __expr: element.getText(sourceFile) };
}
const parsed = expressionToJson(element, sourceFile, constants, new Set(visitedIdentifiers));
unresolved = unresolved || parsed.unresolved;
warnings.push(...parsed.warnings);
return parsed.value;
});
return { value, unresolved, warnings };
}
if (ts.isObjectLiteralExpression(expression)) {
const warnings: string[] = [];
let unresolved = false;
const value: Record<string, unknown> = {};
expression.properties.forEach((property, index) => {
if (ts.isPropertyAssignment(property)) {
const propertyName = getObjectPropertyName(property.name);
if (!propertyName) {
unresolved = true;
warnings.push(`对象属性名无法静态解析: ${property.getText(sourceFile)}`);
value[`__expr_${index}`] = property.getText(sourceFile);
return;
}
const parsed = expressionToJson(property.initializer, sourceFile, constants, new Set(visitedIdentifiers));
unresolved = unresolved || parsed.unresolved;
warnings.push(...parsed.warnings);
value[propertyName] = parsed.value;
return;
}
if (ts.isShorthandPropertyAssignment(property)) {
const propertyName = property.name.text;
if (visitedIdentifiers.has(propertyName)) {
unresolved = true;
warnings.push(`检测到循环引用: ${propertyName}`);
value[propertyName] = { __expr: property.getText(sourceFile) };
return;
}
const initializer = constants.get(propertyName);
if (!initializer) {
unresolved = true;
warnings.push(`未找到简写属性引用: ${propertyName}`);
value[propertyName] = { __expr: property.getText(sourceFile) };
return;
}
const nextVisited = new Set(visitedIdentifiers);
nextVisited.add(propertyName);
const parsed = expressionToJson(initializer, sourceFile, constants, nextVisited);
unresolved = unresolved || parsed.unresolved;
warnings.push(...parsed.warnings);
value[propertyName] = parsed.value;
return;
}
unresolved = true;
warnings.push(`对象包含无法静态解析的属性: ${property.getText(sourceFile)}`);
value[`__expr_${index}`] = property.getText(sourceFile);
});
return { value, unresolved, warnings };
}
if (ts.isIdentifier(expression)) {
const identifier = expression.text;
if (visitedIdentifiers.has(identifier)) {
return {
value: { __expr: expression.getText(sourceFile) },
unresolved: true,
warnings: [`检测到循环引用: ${identifier}`],
};
}
const next = constants.get(identifier);
if (!next) {
return {
value: { __expr: expression.getText(sourceFile) },
unresolved: true,
warnings: [`无法静态解析标识符: ${identifier}`],
};
}
const nextVisited = new Set(visitedIdentifiers);
nextVisited.add(identifier);
return expressionToJson(next, sourceFile, constants, nextVisited);
}
return {
value: { __expr: expression.getText(sourceFile) },
unresolved: true,
warnings: [`无法静态解析表达式: ${expression.getText(sourceFile)}`],
};
}
function toSerializableRecordArray(value: unknown): Array<Record<string, unknown>> {
if (!Array.isArray(value)) {
return [];
}
return value.map((item) => {
if (item && typeof item === 'object' && !Array.isArray(item)) {
return item as Record<string, unknown>;
}
return { value: item };
});
}
function normalizeWarnings(warnings: string[]): string[] {
const seen = new Set<string>();
const deduped: string[] = [];
for (const warning of warnings) {
const normalized = String(warning || '').trim();
if (!normalized || seen.has(normalized)) {
continue;
}
seen.add(normalized);
deduped.push(normalized);
}
return deduped;
}
function buildListPreview(
key: AxureApiListKey,
handleObject: ts.ObjectLiteralExpression | null,
sourceFile: ts.SourceFile,
constants: Map<string, ts.Expression>,
): AxureApiListPreview {
if (!handleObject) {
return createMissingListPreview();
}
const property = handleObject.properties.find((node) => {
if (!ts.isPropertyAssignment(node) && !ts.isShorthandPropertyAssignment(node)) {
return false;
}
const propertyName = getObjectPropertyName(node.name);
return propertyName === key;
});
if (!property) {
return createMissingListPreview();
}
const rawExpression = ts.isPropertyAssignment(property)
? property.initializer
: property.name;
const sourceKey = ts.isIdentifier(rawExpression) ? rawExpression.text : null;
const resolved = resolveExpressionFromConstMap(rawExpression, constants);
const parsed = expressionToJson(resolved.expression, sourceFile, constants);
const warnings = normalizeWarnings([...resolved.warnings, ...parsed.warnings]);
if (!Array.isArray(parsed.value)) {
return {
sourceKey,
raw: resolved.expression.getText(sourceFile),
items: [],
parseStatus: 'raw',
warnings: normalizeWarnings([...warnings, `字段 ${key} 不是数组字面量`]),
};
}
const parseStatus: AxureApiListPreview['parseStatus'] = parsed.unresolved || warnings.length > 0 ? 'raw' : 'parsed';
return {
sourceKey,
raw: resolved.expression.getText(sourceFile),
items: toSerializableRecordArray(parsed.value),
parseStatus,
warnings,
};
}
export function extractAxureApiPreviewFromContent(content: string, filePath: string): AxureApiPreviewResult {
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const constants = collectTopLevelConstInitializers(sourceFile);
const handleResult = findHandleReturnObject(sourceFile);
const result = createEmptyAxureApiPreview(filePath, true);
result.hasAxureHandle = handleResult.hasAxureHandle;
AXURE_LIST_KEYS.forEach((key) => {
result.lists[key] = buildListPreview(key, handleResult.handleObject, sourceFile, constants);
});
return result;
}
export function getAxureApiPreviewFromFile(filePath: string): AxureApiPreviewResult {
if (!fs.existsSync(filePath)) {
return createEmptyAxureApiPreview(filePath, false);
}
try {
const content = fs.readFileSync(filePath, 'utf8');
return extractAxureApiPreviewFromContent(content, filePath);
} catch {
return createEmptyAxureApiPreview(filePath, false);
}
}
function isSafeRelativeTargetPath(targetPath: string): boolean {
const normalized = String(targetPath || '');
if (!normalized) return false;
if (normalized.includes('..')) return false;
if (normalized.startsWith('/')) return false;
if (normalized.startsWith('\\')) return false;
if (path.isAbsolute(normalized)) return false;
return true;
}
/**
* 代码检查插件
*/
export function codeReviewPlugin(): Plugin {
return {
name: 'code-review-plugin',
configureServer(server: any) {
const parseBody = (req: any): Promise<any> => new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
req.on('data', (chunk: Buffer) => {
chunks.push(chunk);
});
req.on('end', () => {
try {
const bodyText = Buffer.concat(chunks).toString('utf8').trim();
resolve(bodyText ? JSON.parse(bodyText) : {});
} catch (error: any) {
reject(new Error(error?.message || 'Invalid JSON body'));
}
});
req.on('error', reject);
});
const sendJson = (res: any, statusCode: number, data: any) => {
res.statusCode = statusCode;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify(data));
};
server.middlewares.use(async (req: any, res: any, next: any) => {
const url = req.url || '';
const isCodeReviewRoute = req.method === 'POST' && url === '/api/code-review';
const isApiPreviewRoute = req.method === 'POST' && url === '/api/axure-api-preview';
if (!isCodeReviewRoute && !isApiPreviewRoute) {
return next();
}
try {
const body = await parseBody(req);
const targetPath = String(body.path || '').trim();
if (!targetPath) {
sendJson(res, 400, { error: 'Missing path parameter' });
return;
}
if (!isSafeRelativeTargetPath(targetPath)) {
sendJson(res, 403, { error: 'Invalid path' });
return;
}
const filePath = path.resolve(process.cwd(), 'src', targetPath, 'index.tsx');
if (isCodeReviewRoute) {
const enforceComponentExportName = body.enforceComponentExportName === true;
const result = reviewFile(filePath, { enforceComponentExportName });
sendJson(res, 200, result);
return;
}
const result = getAxureApiPreviewFromFile(filePath);
sendJson(res, 200, result);
} catch (error: any) {
console.error('Code review error:', error);
sendJson(res, 500, { error: error?.message || 'Server error' });
}
});
}
};
}