Files
tutti/tools/scripts/resolve-component-sources.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

173 lines
5.1 KiB
JavaScript

import { execFile } from "node:child_process";
import { readdir, readFile } from "node:fs/promises";
import { relative, resolve } from "node:path";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const sourceGlobs = [
"*.ts",
"*.tsx",
"!**/*.d.ts",
"!**/*.test.*",
"!**/*.spec.*",
"!**/*.stories.*"
];
export async function resolveComponentSources(componentEntries, sourceRoot) {
const root = resolve(sourceRoot);
const entryCandidates = new Map(
componentEntries.map((entry) => [
entry.name,
componentSymbolCandidates(entry.name)
])
);
const allCandidates = [
...new Set([...entryCandidates.values()].flat())
].sort();
if (allCandidates.length === 0) {
return componentEntries.map((entry) => ({ ...entry, source: null }));
}
const alternation = allCandidates.map(escapeRegex).join("|");
const declarationPattern =
`(?:function|class)\\s+(${alternation})\\b|` +
`(?:const|let|var)\\s+(${alternation})\\s*[:=]`;
const matches = await searchDeclarations(root, declarationPattern);
const matchesBySymbol = new Map();
const declarationRegex = new RegExp(declarationPattern, "u");
for (const match of matches) {
const declaration = declarationRegex.exec(match.text);
const symbol = declaration?.[1] ?? declaration?.[2] ?? null;
if (!symbol) continue;
const values = matchesBySymbol.get(symbol) ?? [];
values.push({
file: relative(root, match.path),
line: match.line,
symbol
});
matchesBySymbol.set(symbol, values);
}
return componentEntries.map((entry) => {
const candidates = entryCandidates.get(entry.name) ?? [];
const locations = uniqueLocations(
candidates.flatMap((candidate) => matchesBySymbol.get(candidate) ?? [])
);
const source =
locations.length > 0 &&
new Set(locations.map((location) => location.file)).size === 1
? locations[0]
: null;
return {
...entry,
source: source ? { ...source, confidence: "static-declaration" } : null,
sourceResolution:
locations.length > 1 && !source
? { status: "ambiguous", candidateCount: locations.length }
: locations.length === 0
? { status: "unresolved" }
: { status: "resolved" }
};
});
}
export function componentSymbolCandidates(componentName) {
const forwardRef = /^ForwardRef\((.+)\)$/u.exec(componentName.trim());
const rawName = forwardRef?.[1] ?? componentName.trim();
if (!/^[A-Z][A-Za-z0-9_$]*$/u.test(rawName)) return [];
const candidates = [rawName];
const withoutCompilerSuffix = rawName.replace(/\d+$/u, "");
if (withoutCompilerSuffix && withoutCompilerSuffix !== rawName) {
candidates.push(withoutCompilerSuffix);
}
return candidates;
}
async function searchDeclarations(root, pattern) {
const args = ["--json", "--line-number"];
for (const glob of sourceGlobs) args.push("--glob", glob);
args.push(pattern, root);
try {
const { stdout } = await execFileAsync("rg", args, {
maxBuffer: 4 * 1024 * 1024
});
return stdout
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line))
.filter((event) => event.type === "match")
.map((event) => ({
path: event.data.path.text,
line: event.data.line_number,
text: event.data.lines.text
}));
} catch (error) {
if (error.code === 1) return [];
if (error.code === "ENOENT") {
return searchDeclarationsWithNode(root, pattern);
}
throw new Error(`component source lookup failed: ${error.message}`);
}
}
async function searchDeclarationsWithNode(root, pattern) {
const regex = new RegExp(pattern, "u");
const matches = [];
for (const path of await collectSourceFiles(root)) {
const source = await readFile(path, "utf8");
const lines = source.split(/\r?\n/u);
lines.forEach((text, index) => {
if (!regex.test(text)) return;
matches.push({
path,
line: index + 1,
text
});
});
}
return matches;
}
async function collectSourceFiles(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const files = [];
for (const entry of entries) {
if (entry.name === "node_modules" || entry.name === ".git") continue;
const path = resolve(directory, entry.name);
if (entry.isDirectory()) {
files.push(...(await collectSourceFiles(path)));
continue;
}
if (!entry.isFile() || !isSearchableSourceFile(entry.name)) continue;
files.push(path);
}
return files;
}
function isSearchableSourceFile(fileName) {
return (
/\.(?:ts|tsx)$/u.test(fileName) &&
!fileName.endsWith(".d.ts") &&
!/\.(?:test|spec|stories)\.[^.]+$/u.test(fileName)
);
}
function uniqueLocations(locations) {
const values = new Map();
for (const location of locations) {
values.set(
`${location.file}:${location.line}:${location.symbol}`,
location
);
}
return [...values.values()].sort(
(left, right) =>
left.file.localeCompare(right.file) || left.line - right.line
);
}
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}