Files
tutti/tools/scripts/check-renderer-feature-boundaries.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

265 lines
6.7 KiB
JavaScript

import { execFileSync } from "node:child_process";
import { readFile, readdir } from "node:fs/promises";
import { dirname, join, normalize, relative, sep } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const defaultWorkspaceRoot = join(scriptDirectory, "..", "..");
const workspaceRoot = process.env.TUTTI_WORKSPACE_ROOT ?? defaultWorkspaceRoot;
const rendererRoot =
process.env.TUTTI_RENDERER_ROOT ?? "apps/desktop/src/renderer/src";
const ignoredDirectories = new Set([
".git",
".turbo",
"dist",
"node_modules",
"out"
]);
const stagedOnly = process.argv.includes("--staged");
const textExtensions = new Set([".js", ".jsx", ".mjs", ".ts", ".tsx"]);
const violations = [];
if (stagedOnly) {
await scanStagedFiles();
} else {
await walk(join(workspaceRoot, rendererRoot));
}
if (violations.length > 0) {
console.error("Found renderer feature boundary violations:");
for (const violation of violations) {
console.error(
`- [${violation.rule}] ${violation.file}:${violation.line} ${violation.message}`
);
}
process.exitCode = 1;
} else {
console.log("renderer feature boundary check passed");
}
async function walk(directory) {
const entries = await readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const path = join(directory, entry.name);
if (entry.isDirectory()) {
if (!ignoredDirectories.has(entry.name)) {
await walk(path);
}
continue;
}
if (!entry.isFile()) {
continue;
}
const relativePath = toPosixPath(relative(workspaceRoot, path));
await inspectFile(relativePath, path);
}
}
async function scanStagedFiles() {
const output = execFileSync(
"git",
["diff", "--cached", "--name-only", "--diff-filter=ACMR"],
{
cwd: workspaceRoot,
encoding: "utf8"
}
);
const files = output
.split("\n")
.map((file) => file.trim())
.filter(Boolean)
.filter((file) => file.startsWith(`${rendererRoot}/`));
for (const relativePath of files) {
await inspectFile(relativePath, join(workspaceRoot, relativePath));
}
}
async function inspectFile(relativePath, absolutePath) {
if (!hasTextExtension(relativePath)) {
return;
}
const content = await readFile(absolutePath, "utf8");
inspectCodeFile(relativePath, content);
}
function inspectCodeFile(relativePath, content) {
const lines = content.split("\n");
for (let index = 0; index < lines.length; index += 1) {
for (const specifier of matchImportSpecifiers(lines[index])) {
inspectImport(relativePath, specifier, index + 1);
}
inspectWindowTuttiAccess(relativePath, lines[index], index + 1);
inspectWorkspaceLaunchCoordinatorMap(relativePath, lines[index], index + 1);
}
}
function inspectWorkspaceLaunchCoordinatorMap(relativePath, lineContent, line) {
if (!isWorkspaceLaunchCoordinator(relativePath)) {
return;
}
if (!/\bnew\s+Map\s*(?:<|\()/u.test(lineContent)) {
return;
}
violations.push({
file: relativePath,
line,
message:
"workspace launch coordinators must use WorkspaceScopedRegistrationRegistry instead of owning a Map",
rule: "workspace-launch-coordinator-private-map"
});
}
function isWorkspaceLaunchCoordinator(relativePath) {
const servicesRoot = `${rendererRoot}/features/workspace-workbench/services/`;
if (!relativePath.startsWith(servicesRoot)) {
return false;
}
const servicePath = relativePath.slice(servicesRoot.length);
if (servicePath.includes("/")) {
return false;
}
return (
servicePath.endsWith("LaunchCoordinator.ts") ||
servicePath === "workspaceMessageCenterCoordinator.ts"
);
}
function inspectImport(importerPath, specifier, line) {
const targetPath = resolveRendererSpecifier(importerPath, specifier);
if (!targetPath) {
return;
}
inspectWorkspaceWorkbenchUiInternalImport(
importerPath,
targetPath,
specifier,
line
);
const internalMatch = targetPath.match(
/^apps\/desktop\/src\/renderer\/src\/features\/([^/]+)\/services\/internal(?:\/|$)/
);
if (!internalMatch) {
return;
}
const targetFeature = internalMatch[1];
const allowedPrefix = `apps/desktop/src/renderer/src/features/${targetFeature}/`;
if (importerPath.startsWith(allowedPrefix)) {
return;
}
violations.push({
file: importerPath,
line,
message: `cross-feature import of ${targetFeature}/services/internal is not allowed: ${specifier}`,
rule: "feature-internal-import"
});
}
function inspectWorkspaceWorkbenchUiInternalImport(
importerPath,
targetPath,
specifier,
line
) {
const workbenchFeatureRoot = `${rendererRoot}/features/workspace-workbench/`;
if (!importerPath.startsWith(`${workbenchFeatureRoot}ui/`)) {
return;
}
if (!targetPath.startsWith(`${workbenchFeatureRoot}services/internal/`)) {
return;
}
violations.push({
file: importerPath,
line,
message: `workspace workbench UI must use public service/controller seams instead of importing services/internal: ${specifier}`,
rule: "workspace-workbench-ui-internal-import"
});
}
function inspectWindowTuttiAccess(relativePath, lineContent, line) {
if (!/\bwindow\s*\.\s*tutti\b/.test(lineContent)) {
return;
}
if (canAccessRendererPreloadApi(relativePath)) {
return;
}
violations.push({
file: relativePath,
line,
message:
"renderer code must receive DesktopApi through window composition instead of reading window.tutti directly",
rule: "renderer-window-tutti-access"
});
}
function canAccessRendererPreloadApi(relativePath) {
if (relativePath === `${rendererRoot}/global.d.ts`) {
return true;
}
const windowsRoot = `${rendererRoot}/app/windows/`;
if (!relativePath.startsWith(windowsRoot)) {
return false;
}
return /^create[A-Z][^/]*Container\.ts$/.test(
relativePath.slice(windowsRoot.length).split("/").at(-1) ?? ""
);
}
function resolveRendererSpecifier(importerPath, specifier) {
if (specifier.startsWith("@renderer/")) {
return normalizePosix(
`${rendererRoot}/${specifier.slice("@renderer/".length)}`
);
}
if (specifier.startsWith(".")) {
return normalizePosix(join(dirname(importerPath), specifier));
}
return null;
}
function hasTextExtension(path) {
return Array.from(textExtensions).some((extension) =>
path.endsWith(extension)
);
}
function matchImportSpecifiers(line) {
const matches = line.matchAll(
/(?:import|export)\s+(?:type\s+)?(?:[^"'`]*?\s+from\s+)?["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)/g
);
return Array.from(matches, (match) => match[1] ?? match[2]).filter(Boolean);
}
function normalizePosix(path) {
return toPosixPath(normalize(path));
}
function toPosixPath(path) {
return path.split(sep).join("/");
}