Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
437
scripts/root-dependency-ownership-audit.mjs
Normal file
437
scripts/root-dependency-ownership-audit.mjs
Normal file
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Audits root package runtime dependencies against source imports and bundled
|
||||
// plugin ownership so extension-owned deps can move out of root.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { packageNameFromSpecifier } from "./lib/plugin-package-dependencies.mjs";
|
||||
|
||||
const DEFAULT_SCAN_ROOTS = ["src", "extensions", "packages", "ui", "scripts", "test"];
|
||||
const SCANNED_EXTENSIONS = new Set([".cjs", ".cts", ".js", ".jsx", ".mjs", ".mts", ".ts", ".tsx"]);
|
||||
const IMPORT_PATTERNS = [
|
||||
/\bfrom\s*["']([^"']+)["']/g,
|
||||
/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g,
|
||||
/\brequire\s*\(\s*["']([^"']+)["']\s*\)/g,
|
||||
/\b(?:require|[_$A-Za-z][\w$]*require[\w$]*)\.resolve\s*\(\s*["']([^"']+)["']\s*\)/gi,
|
||||
];
|
||||
const STRING_CONSTANT_PATTERN = /\b(?:const|let|var)\s+([_$A-Za-z][\w$]*)\s*=\s*["']([^"']+)["']/g;
|
||||
const DYNAMIC_CONSTANT_IMPORT_PATTERNS = [
|
||||
/\bimport\s*\(\s*([_$A-Za-z][\w$]*)\s*\)/g,
|
||||
/\brequire\s*\(\s*([_$A-Za-z][\w$]*)\s*\)/g,
|
||||
/\b(?:require|[_$A-Za-z][\w$]*require[\w$]*)\.resolve\s*\(\s*([_$A-Za-z][\w$]*)\s*\)/gi,
|
||||
];
|
||||
const PACKAGE_FILE_LOOKUP_PATTERNS = [
|
||||
/\bresolvePackageFileForCommandExplanation\s*\(\s*["']([^"']+)["']/g,
|
||||
];
|
||||
const ROOT_OWNED_EXTENSION_RUNTIME_DEPENDENCIES = new Map([
|
||||
[
|
||||
"@homebridge/ciao",
|
||||
"keep at root; the Bonjour runtime is shipped with packaged startup surfaces even though the bundled plugin also declares it",
|
||||
],
|
||||
[
|
||||
"playwright-core",
|
||||
"keep at root; the internal browser runtime is shipped with core even though downloadable browser-adjacent plugins also declare it",
|
||||
],
|
||||
]);
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
function isScannableSourceFile(fileName) {
|
||||
return SCANNED_EXTENSIONS.has(path.extname(fileName));
|
||||
}
|
||||
|
||||
function shouldSkipDir(dirName) {
|
||||
return dirName === "dist" || dirName === "node_modules" || dirName === ".git";
|
||||
}
|
||||
|
||||
function walkFiles(rootDir) {
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
return [];
|
||||
}
|
||||
const files = [];
|
||||
const queue = [rootDir];
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const fullPath = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (shouldSkipDir(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
queue.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && isScannableSourceFile(entry.name)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function normalizeRelativePath(filePath, repoRoot) {
|
||||
return path.relative(repoRoot, filePath).replaceAll(path.sep, "/");
|
||||
}
|
||||
|
||||
function sectionFor(relativePath) {
|
||||
const [section = "other"] = relativePath.split("/");
|
||||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects static and simple constant-backed package specifiers from source text.
|
||||
*/
|
||||
export function collectModuleSpecifiers(source) {
|
||||
const specifiers = new Set();
|
||||
for (const pattern of IMPORT_PATTERNS) {
|
||||
for (const match of source.matchAll(pattern)) {
|
||||
if (match[1]) {
|
||||
specifiers.add(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const pattern of PACKAGE_FILE_LOOKUP_PATTERNS) {
|
||||
for (const match of source.matchAll(pattern)) {
|
||||
if (match[1]) {
|
||||
specifiers.add(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const stringConstants = new Map();
|
||||
for (const match of source.matchAll(STRING_CONSTANT_PATTERN)) {
|
||||
if (match[1] && match[2]) {
|
||||
stringConstants.set(match[1], match[2]);
|
||||
}
|
||||
}
|
||||
for (const pattern of DYNAMIC_CONSTANT_IMPORT_PATTERNS) {
|
||||
for (const match of source.matchAll(pattern)) {
|
||||
const specifier = match[1] ? stringConstants.get(match[1]) : undefined;
|
||||
if (specifier) {
|
||||
specifiers.add(specifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
function collectExtensionDependencyDeclarations(repoRoot) {
|
||||
const declarations = new Map();
|
||||
const extensionsRoot = path.join(repoRoot, "extensions");
|
||||
if (!fs.existsSync(extensionsRoot)) {
|
||||
return declarations;
|
||||
}
|
||||
|
||||
for (const entry of fs.readdirSync(extensionsRoot, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const packageJsonPath = path.join(extensionsRoot, entry.name, "package.json");
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
continue;
|
||||
}
|
||||
const packageJson = readJson(packageJsonPath);
|
||||
for (const section of [
|
||||
"dependencies",
|
||||
"optionalDependencies",
|
||||
"devDependencies",
|
||||
"peerDependencies",
|
||||
]) {
|
||||
for (const depName of Object.keys(packageJson[section] ?? {})) {
|
||||
const existing = declarations.get(depName) ?? [];
|
||||
existing.push(`${entry.name}:${section}`);
|
||||
declarations.set(depName, existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const values of declarations.values()) {
|
||||
values.sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
return declarations;
|
||||
}
|
||||
|
||||
function collectExcludedPackagedExtensionDirs(rootPackageJson) {
|
||||
const excluded = new Set();
|
||||
for (const entry of rootPackageJson.files ?? []) {
|
||||
if (typeof entry !== "string") {
|
||||
continue;
|
||||
}
|
||||
const match = /^!dist\/extensions\/([^/]+)\/\*\*$/u.exec(entry);
|
||||
if (match?.[1]) {
|
||||
excluded.add(match[1]);
|
||||
}
|
||||
}
|
||||
return excluded;
|
||||
}
|
||||
|
||||
function collectInternalizedBundledExtensionRuntimeDependencies(repoRoot, rootPackageJson) {
|
||||
const dependencies = new Map();
|
||||
const extensionsRoot = path.join(repoRoot, "extensions");
|
||||
if (!fs.existsSync(extensionsRoot)) {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
const excluded = collectExcludedPackagedExtensionDirs(rootPackageJson);
|
||||
for (const entry of fs.readdirSync(extensionsRoot, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || excluded.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
const packageJsonPath = path.join(extensionsRoot, entry.name, "package.json");
|
||||
const manifestPath = path.join(extensionsRoot, entry.name, "openclaw.plugin.json");
|
||||
if (!fs.existsSync(packageJsonPath) || !fs.existsSync(manifestPath)) {
|
||||
continue;
|
||||
}
|
||||
const packageJson = readJson(packageJsonPath);
|
||||
for (const section of ["dependencies", "optionalDependencies"]) {
|
||||
for (const depName of Object.keys(packageJson[section] ?? {})) {
|
||||
const existing = dependencies.get(depName) ?? [];
|
||||
existing.push(`${entry.name}:${section}`);
|
||||
dependencies.set(depName, existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const values of dependencies.values()) {
|
||||
values.sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
function sectionSetContainsCore(sectionSet) {
|
||||
return sectionSet.has("src") || sectionSet.has("packages") || sectionSet.has("ui");
|
||||
}
|
||||
|
||||
function sectionSetIsSubsetOf(sectionSet, allowed) {
|
||||
for (const value of sectionSet) {
|
||||
if (!allowed.has(value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return sectionSet.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies whether a root dependency is core-owned, shared, or extension-local.
|
||||
*/
|
||||
export function classifyRootDependencyOwnership(record) {
|
||||
const sections = new Set(record.sections);
|
||||
|
||||
if (sections.size === 0) {
|
||||
return {
|
||||
category: "unreferenced",
|
||||
recommendation: "investigate removal; no direct source imports found in scanned files",
|
||||
};
|
||||
}
|
||||
|
||||
if (sectionSetIsSubsetOf(sections, new Set(["scripts", "test"]))) {
|
||||
return {
|
||||
category: "script_or_test_only",
|
||||
recommendation: "consider moving from dependencies to devDependencies",
|
||||
};
|
||||
}
|
||||
|
||||
if (sectionSetContainsCore(sections)) {
|
||||
if (sections.has("extensions")) {
|
||||
return {
|
||||
category: "shared_core_and_extension",
|
||||
recommendation:
|
||||
"keep at root until shared code is split or extension/core boundary changes",
|
||||
};
|
||||
}
|
||||
return {
|
||||
category: "core_runtime",
|
||||
recommendation: "keep at root",
|
||||
};
|
||||
}
|
||||
|
||||
const rootOwnedExtensionRuntime = ROOT_OWNED_EXTENSION_RUNTIME_DEPENDENCIES.get(record.depName);
|
||||
if (
|
||||
rootOwnedExtensionRuntime &&
|
||||
sectionSetIsSubsetOf(sections, new Set(["extensions", "test"]))
|
||||
) {
|
||||
return {
|
||||
category: "root_owned_extension_runtime",
|
||||
recommendation: rootOwnedExtensionRuntime,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
record.internalizedBundledRuntimeOwners?.length > 0 &&
|
||||
sectionSetIsSubsetOf(sections, new Set(["extensions", "test"]))
|
||||
) {
|
||||
return {
|
||||
category: "root_owned_extension_runtime",
|
||||
recommendation: `keep at root while bundled plugin runtime dependencies are internalized; owners: ${record.internalizedBundledRuntimeOwners.join(", ")}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (sectionSetIsSubsetOf(sections, new Set(["extensions", "test"]))) {
|
||||
return {
|
||||
category: "extension_only_localizable",
|
||||
recommendation:
|
||||
"remove from root package.json and rely on owning extension manifests plus doctor --fix",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
category: "mixed_noncore",
|
||||
recommendation: "inspect manually; usage spans non-core surfaces",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds dependency ownership records from root package.json and scanned imports.
|
||||
*/
|
||||
export function collectRootDependencyOwnershipAudit(params = {}) {
|
||||
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
||||
const rootPackageJson = readJson(path.join(repoRoot, "package.json"));
|
||||
const rootDependencies = {
|
||||
...rootPackageJson.dependencies,
|
||||
...rootPackageJson.optionalDependencies,
|
||||
};
|
||||
const records = new Map(
|
||||
Object.keys(rootDependencies).map((depName) => [
|
||||
depName,
|
||||
{
|
||||
depName,
|
||||
sections: new Set(),
|
||||
files: new Set(),
|
||||
declaredInExtensions: [],
|
||||
internalizedBundledRuntimeOwners: [],
|
||||
spec: rootDependencies[depName],
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const scanRoots = params.scanRoots ?? DEFAULT_SCAN_ROOTS;
|
||||
for (const scanRoot of scanRoots) {
|
||||
for (const filePath of walkFiles(path.join(repoRoot, scanRoot))) {
|
||||
const relativePath = normalizeRelativePath(filePath, repoRoot);
|
||||
const source = fs.readFileSync(filePath, "utf8");
|
||||
for (const specifier of collectModuleSpecifiers(source)) {
|
||||
const depName = packageNameFromSpecifier(specifier);
|
||||
if (!depName || !records.has(depName)) {
|
||||
continue;
|
||||
}
|
||||
const record = records.get(depName);
|
||||
record.sections.add(sectionFor(relativePath));
|
||||
record.files.add(relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const extensionDeclarations = collectExtensionDependencyDeclarations(repoRoot);
|
||||
for (const [depName, declarations] of extensionDeclarations) {
|
||||
const record = records.get(depName);
|
||||
if (record) {
|
||||
record.declaredInExtensions = declarations;
|
||||
}
|
||||
}
|
||||
|
||||
const internalizedBundledRuntimeDependencies =
|
||||
collectInternalizedBundledExtensionRuntimeDependencies(repoRoot, rootPackageJson);
|
||||
for (const [depName, owners] of internalizedBundledRuntimeDependencies) {
|
||||
const record = records.get(depName);
|
||||
if (record) {
|
||||
record.internalizedBundledRuntimeOwners = owners;
|
||||
}
|
||||
}
|
||||
|
||||
return [...records.values()]
|
||||
.map((record) => {
|
||||
const classification = classifyRootDependencyOwnership({
|
||||
...record,
|
||||
sections: [...record.sections].toSorted((left, right) => left.localeCompare(right)),
|
||||
});
|
||||
return {
|
||||
depName: record.depName,
|
||||
spec: record.spec,
|
||||
sections: [...record.sections].toSorted((left, right) => left.localeCompare(right)),
|
||||
fileCount: record.files.size,
|
||||
sampleFiles: [...record.files].slice(0, 5),
|
||||
declaredInExtensions: record.declaredInExtensions,
|
||||
internalizedBundledRuntimeOwners: record.internalizedBundledRuntimeOwners,
|
||||
category: classification.category,
|
||||
recommendation: classification.recommendation,
|
||||
};
|
||||
})
|
||||
.toSorted((left, right) => left.depName.localeCompare(right.depName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns actionable errors for dependencies that should not remain root-owned.
|
||||
*/
|
||||
export function collectRootDependencyOwnershipCheckErrors(records) {
|
||||
return records
|
||||
.filter((record) => record.category === "extension_only_localizable")
|
||||
.map((record) => {
|
||||
const declaredInExtensions =
|
||||
record.declaredInExtensions.length > 0
|
||||
? `; extension declarations: ${record.declaredInExtensions.join(", ")}`
|
||||
: "";
|
||||
const sampleFiles =
|
||||
record.sampleFiles.length > 0 ? `; sample imports: ${record.sampleFiles.join(", ")}` : "";
|
||||
return (
|
||||
`root dependency '${record.depName}' is extension-owned (${record.recommendation})` +
|
||||
`${declaredInExtensions}${sampleFiles}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function printTextReport(records) {
|
||||
const grouped = new Map();
|
||||
for (const record of records) {
|
||||
const existing = grouped.get(record.category) ?? [];
|
||||
existing.push(record);
|
||||
grouped.set(record.category, existing);
|
||||
}
|
||||
|
||||
for (const category of [...grouped.keys()].toSorted((left, right) => left.localeCompare(right))) {
|
||||
console.log(`\n## ${category}`);
|
||||
for (const record of grouped.get(category)) {
|
||||
const details = [`sections=${record.sections.join(",") || "-"}`, `files=${record.fileCount}`];
|
||||
if (record.declaredInExtensions.length > 0) {
|
||||
details.push(`extensions=${record.declaredInExtensions.join(",")}`);
|
||||
}
|
||||
if (record.internalizedBundledRuntimeOwners.length > 0) {
|
||||
details.push(`internalized=${record.internalizedBundledRuntimeOwners.join(",")}`);
|
||||
}
|
||||
console.log(`- ${record.depName}@${record.spec} :: ${details.join(" | ")}`);
|
||||
console.log(` ${record.recommendation}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main(argv = process.argv.slice(2)) {
|
||||
const asJson = argv.includes("--json");
|
||||
const check = argv.includes("--check");
|
||||
const records = collectRootDependencyOwnershipAudit();
|
||||
if (check) {
|
||||
const errors = collectRootDependencyOwnershipCheckErrors(records);
|
||||
if (errors.length > 0) {
|
||||
for (const error of errors) {
|
||||
console.error(`[root-dependency-ownership] ${error}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (!asJson) {
|
||||
console.error("[root-dependency-ownership] ok");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (asJson) {
|
||||
console.log(JSON.stringify(records, null, 2));
|
||||
return;
|
||||
}
|
||||
printTextReport(records);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
main();
|
||||
}
|
||||
Reference in New Issue
Block a user