feat(GOV-010): 建立前端 API 分域边界
This commit is contained in:
@@ -48,7 +48,8 @@ function collectModuleSpecifiers(sourceFile) {
|
||||
}
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
||||
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
||||
(ts.isIdentifier(node.expression) && node.expression.text === 'require')) &&
|
||||
node.arguments.length === 1 &&
|
||||
ts.isStringLiteralLike(node.arguments[0])
|
||||
) {
|
||||
@@ -61,6 +62,74 @@ function collectModuleSpecifiers(sourceFile) {
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
function isTestSource(relativeSourcePath) {
|
||||
return /(?:^|\/)__tests__(?:\/|$)|\.(?:test|spec)\.[^.]+$/.test(relativeSourcePath);
|
||||
}
|
||||
|
||||
function collectHttpBoundaryViolations(sourceFile, relativeSourcePath, moduleSpecifiers) {
|
||||
if (isTestSource(relativeSourcePath)) return [];
|
||||
|
||||
const normalizedPath = relativeSourcePath.split(path.sep).join('/');
|
||||
const isRequestBoundary = normalizedPath === 'utils/request.ts';
|
||||
const isGeneratedContract = normalizedPath.startsWith('api/generated/');
|
||||
const isBusinessApi = normalizedPath.startsWith('api/') && !isGeneratedContract;
|
||||
const importsAxios = moduleSpecifiers.includes('axios');
|
||||
const importsSharedRequest = moduleSpecifiers.includes('@/utils/request');
|
||||
const violations = [];
|
||||
let declaresBusinessApiPath = false;
|
||||
|
||||
if (importsAxios && !isRequestBoundary) {
|
||||
violations.push(`Web 运行时代码禁止直接使用 Axios:${normalizedPath}`);
|
||||
}
|
||||
|
||||
function containsBusinessApiPath(node) {
|
||||
if (ts.isStringLiteralLike(node) && /^\/api(?:\/|$)/.test(node.text)) return true;
|
||||
let found = false;
|
||||
ts.forEachChild(node, (child) => {
|
||||
if (!found && containsBusinessApiPath(child)) found = true;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
function visit(node) {
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callsFetch =
|
||||
(ts.isIdentifier(node.expression) && node.expression.text === 'fetch') ||
|
||||
(ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'fetch');
|
||||
if (callsFetch) {
|
||||
violations.push(`Web 运行时代码禁止直接调用 fetch:${normalizedPath}`);
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isPropertyAccessExpression(node.expression) &&
|
||||
ts.isIdentifier(node.expression.expression) &&
|
||||
node.expression.expression.text === 'axios'
|
||||
) {
|
||||
if (!isRequestBoundary) {
|
||||
violations.push(`Web 运行时代码禁止直接使用 Axios:${normalizedPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
const hasBusinessApiArgument = node.arguments.some(containsBusinessApiPath);
|
||||
if (hasBusinessApiArgument) {
|
||||
declaresBusinessApiPath = true;
|
||||
if (!isBusinessApi) {
|
||||
violations.push(`业务 API 路径只能声明在 web/src/api:${normalizedPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
|
||||
if (isBusinessApi && declaresBusinessApiPath && !importsSharedRequest) {
|
||||
violations.push(`业务 API 文件必须使用 @/utils/request:${normalizedPath}`);
|
||||
}
|
||||
|
||||
return [...new Set(violations)];
|
||||
}
|
||||
|
||||
function resolvesInsideServer(specifier, sourcePath, repositoryRoot, compilerOptions) {
|
||||
const serverRoot = path.join(repositoryRoot, 'server');
|
||||
const resolved = ts.resolveModuleName(specifier, sourcePath, compilerOptions, ts.sys)
|
||||
@@ -96,13 +165,21 @@ export async function verifyProjectBoundaries(repositoryRoot) {
|
||||
true,
|
||||
ts.ScriptKind.TS,
|
||||
);
|
||||
for (const specifier of collectModuleSpecifiers(sourceFile)) {
|
||||
const moduleSpecifiers = collectModuleSpecifiers(sourceFile);
|
||||
for (const specifier of moduleSpecifiers) {
|
||||
if (resolvesInsideServer(specifier, sourcePath, repositoryRoot, compilerOptions)) {
|
||||
violations.push(
|
||||
`Web 禁止依赖 Server:${path.relative(repositoryRoot, sourcePath)} -> ${specifier}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
violations.push(
|
||||
...collectHttpBoundaryViolations(
|
||||
sourceFile,
|
||||
path.relative(webSourceRoot, sourcePath),
|
||||
moduleSpecifiers,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const generatedSchemaPath = path.join(webSourceRoot, 'api', 'generated', 'schema.ts');
|
||||
|
||||
Reference in New Issue
Block a user