Files
qiming/qimingclaw/crates/agent-electron-client/scripts/tools/check-import-boundaries.js

236 lines
6.9 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
const fs = require('fs');
const path = require('path');
const PROJECT_ROOT = process.env.QIMING_BOUNDARY_PROJECT_ROOT
? path.resolve(process.env.QIMING_BOUNDARY_PROJECT_ROOT)
: path.resolve(__dirname, '..', '..');
const SRC_ROOT = path.join(PROJECT_ROOT, 'src');
const MAIN_ROOT = path.join(SRC_ROOT, 'main');
const RENDERER_ROOT = path.join(SRC_ROOT, 'renderer');
const PRELOAD_ROOT = path.join(SRC_ROOT, 'preload');
const BANNED_BRIDGE_FILES = [
'src/main/startup.ts',
'src/main/logConfig.ts',
'src/main/serviceManager.ts',
'src/main/trayManager.ts',
'src/main/autoLaunchManager.ts',
'src/main/preload.ts',
'src/renderer/services/api.ts',
'src/renderer/services/setup.ts',
'src/renderer/services/auth.ts',
'src/renderer/services/ai.ts',
'src/renderer/services/fileServer.ts',
'src/renderer/services/lanproxy.ts',
'src/renderer/services/agentRunner.ts',
'src/renderer/services/sandbox.ts',
'src/renderer/services/permissions.ts',
'src/renderer/services/skills.ts',
'src/renderer/services/im.ts',
'src/renderer/services/scheduler.ts',
'src/renderer/services/logService.ts',
'src/renderer/components/ClientPage.tsx',
'src/renderer/components/SettingsPage.tsx',
'src/renderer/components/DependenciesPage.tsx',
'src/renderer/components/AboutPage.tsx',
'src/renderer/components/PermissionsPage.tsx',
'src/renderer/components/LogViewer.tsx',
'src/renderer/components/MCPSettings.tsx',
'src/renderer/components/AgentSettings.tsx',
'src/renderer/components/AgentRunnerSettings.tsx',
'src/renderer/components/LanproxySettings.tsx',
'src/renderer/components/SkillsSync.tsx',
'src/renderer/components/IMSettings.tsx',
'src/renderer/components/TaskSettings.tsx',
'src/renderer/components/SetupWizard.tsx',
'src/renderer/components/SetupDependencies.tsx',
'src/renderer/components/PermissionModal.tsx',
];
const BANNED_BRIDGE_PATHS = new Set(
BANNED_BRIDGE_FILES.map((p) => path.normalize(path.join(PROJECT_ROOT, p)))
);
function isSourceFile(filePath) {
return filePath.endsWith('.ts') || filePath.endsWith('.tsx');
}
function walk(dirPath) {
const out = [];
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'node_modules' || entry.name === 'dist') continue;
out.push(...walk(fullPath));
continue;
}
if (isSourceFile(fullPath)) out.push(fullPath);
}
return out;
}
function existsFile(candidate) {
try {
return fs.statSync(candidate).isFile();
} catch {
return false;
}
}
function resolveImport(specifier, fromFile) {
let basePath = null;
if (specifier.startsWith('.')) {
basePath = path.resolve(path.dirname(fromFile), specifier);
} else if (specifier.startsWith('@/')) {
basePath = path.join(SRC_ROOT, specifier.slice(2));
} else if (specifier.startsWith('@main/')) {
basePath = path.join(MAIN_ROOT, specifier.slice('@main/'.length));
} else if (specifier.startsWith('@renderer/')) {
basePath = path.join(RENDERER_ROOT, specifier.slice('@renderer/'.length));
} else if (specifier.startsWith('@preload/')) {
basePath = path.join(PRELOAD_ROOT, specifier.slice('@preload/'.length));
} else if (specifier.startsWith('@shared/')) {
basePath = path.join(SRC_ROOT, 'shared', specifier.slice('@shared/'.length));
} else {
return null;
}
const candidates = [
basePath,
`${basePath}.ts`,
`${basePath}.tsx`,
`${basePath}.js`,
path.join(basePath, 'index.ts'),
path.join(basePath, 'index.tsx'),
path.join(basePath, 'index.js'),
];
for (const candidate of candidates) {
if (existsFile(candidate)) return path.normalize(candidate);
}
return null;
}
function lineFromIndex(content, index) {
let line = 1;
for (let i = 0; i < index; i += 1) {
if (content.charCodeAt(i) === 10) line += 1;
}
return line;
}
function collectImports(fileContent) {
const specs = [];
const patterns = [
/\bimport\s+(?:[^'"]+from\s+)?['"]([^'"]+)['"]/g,
/\bexport\s+[^'"]*from\s+['"]([^'"]+)['"]/g,
/\brequire\(\s*['"]([^'"]+)['"]\s*\)/g,
/\bimport\(\s*['"]([^'"]+)['"]\s*\)/g,
];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(fileContent)) !== null) {
specs.push({ specifier: match[1], index: match.index });
}
}
return specs;
}
function isWithin(filePath, rootPath) {
const rel = path.relative(rootPath, filePath);
return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
}
function checkFile(filePath) {
const violations = [];
const content = fs.readFileSync(filePath, 'utf8');
const imports = collectImports(content);
for (const imp of imports) {
const resolved = resolveImport(imp.specifier, filePath);
const line = lineFromIndex(content, imp.index);
if (resolved && BANNED_BRIDGE_PATHS.has(resolved)) {
violations.push({
line,
rule: 'no-bridge-imports',
message: `Do not import bridge file '${imp.specifier}'. Import the new grouped path directly.`,
});
continue;
}
const inRenderer = isWithin(filePath, RENDERER_ROOT);
const inMain = isWithin(filePath, MAIN_ROOT);
if (inRenderer) {
const directAliasViolation = imp.specifier.startsWith('@main/') || imp.specifier.startsWith('@preload/');
const resolvedViolation = resolved && (isWithin(resolved, MAIN_ROOT) || isWithin(resolved, PRELOAD_ROOT));
if (directAliasViolation || resolvedViolation) {
violations.push({
line,
rule: 'renderer-process-boundary',
message: `Renderer must not import main/preload code directly ('${imp.specifier}'). Use IPC bridge only.`,
});
}
}
if (inMain) {
const directAliasViolation = imp.specifier.startsWith('@renderer/');
const resolvedViolation = resolved && isWithin(resolved, RENDERER_ROOT);
if (directAliasViolation || resolvedViolation) {
violations.push({
line,
rule: 'main-process-boundary',
message: `Main process must not import renderer code ('${imp.specifier}').`,
});
}
}
}
return violations;
}
function runBoundaryCheck() {
const files = walk(SRC_ROOT);
const allViolations = [];
for (const file of files) {
const violations = checkFile(file);
for (const v of violations) {
allViolations.push({
file: path.relative(PROJECT_ROOT, file),
...v,
});
}
}
return allViolations;
}
function main() {
const allViolations = runBoundaryCheck();
if (allViolations.length > 0) {
console.error('Import boundary check failed:\n');
for (const v of allViolations) {
console.error(`- ${v.file}:${v.line} [${v.rule}] ${v.message}`);
}
console.error(`\nTotal violations: ${allViolations.length}`);
process.exit(1);
}
console.log('Import boundary check passed.');
}
if (require.main === module) {
main();
}
module.exports = {
runBoundaryCheck,
};