scripts/generatePodUsage.ts
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bun
/**
* Regenerates ios/pod-usage.json from the installed Pods directory.
*
* checkUnusedPods.ts cannot resolve a pod's module name on its own: the name a
* pod is imported under is decided by its podspec, not by its Podfile entry
* (GzipSwift exports a module called Gzip), and Objective-C pods that ship no
* module are reachable only through their public headers. Both live under
* ios/Pods, which is gitignored, so they are resolved here once and committed to
* the manifest.
*
* Run this after `pod install` whenever the Podfile changes, then review the
* diff — a pod this script cannot find any reference for is written out as
* `exempt` with an empty reason, which the check rejects until you fill it in.
*
* Usage: npx bun scripts/generatePodUsage.ts
*/
import fs from 'fs';
import path from 'path';
import type {PodUsageManifest} from './podUsageShared';
import {IOS_DIR, MANIFEST_PATH, countReferences, readDeclaredPods, readManifest, readSources, writeManifest} from './podUsageShared';
const TARGET_SUPPORT_FILES = path.join(IOS_DIR, 'Pods', 'Target Support Files');
const PUBLIC_HEADERS = path.join(IOS_DIR, 'Pods', 'Headers', 'Public');
function readModuleName(pod: string): string | undefined {
const dir = path.join(TARGET_SUPPORT_FILES, pod);
if (!fs.existsSync(dir)) {
return undefined;
}
for (const entry of fs.readdirSync(dir)) {
if (!entry.endsWith('.modulemap')) {
continue;
}
const match = /^\s*(?:framework\s+)?module\s+([A-Za-z0-9_]+)/m.exec(fs.readFileSync(path.join(dir, entry), 'utf8'));
if (match) {
return match[1];
}
}
return undefined;
}
function readPublicHeaders(pod: string): string[] {
const dir = path.join(PUBLIC_HEADERS, pod);
if (!fs.existsSync(dir)) {
return [];
}
const out: string[] = [];
const walk = (current: string) => {
for (const entry of fs.readdirSync(current, {withFileTypes: true})) {
const full = path.join(current, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (entry.name.endsWith('.h')) {
out.push(entry.name);
}
}
};
walk(dir);
return out.sort();
}
function main() {
if (!fs.existsSync(TARGET_SUPPORT_FILES)) {
console.error(`${TARGET_SUPPORT_FILES} is missing. Run \`bundle exec pod install\` in ios/ first.`);
process.exit(1);
}
const sources = readSources();
const previous: PodUsageManifest = fs.existsSync(MANIFEST_PATH) ? readManifest() : {};
const manifest: PodUsageManifest = {};
for (const pod of readDeclaredPods()) {
const moduleName = readModuleName(pod) ?? pod.replaceAll(/[^A-Za-z0-9_]/g, '_');
if (countReferences(sources, {usage: 'module', module: moduleName}) > 0) {
manifest[pod] = {usage: 'module', module: moduleName};
continue;
}
const headers = readPublicHeaders(pod);
if (headers.length && countReferences(sources, {usage: 'headers', headers}, pod) > 0) {
manifest[pod] = {usage: 'headers', headers};
continue;
}
// Nothing in the sources reaches this pod. It is either dead or alive by a
// route the sources do not show — a build phase, a linker flag, another pod
// depending on it. Keep whatever reason was already recorded so a
// regeneration does not wipe it.
const existing = previous[pod];
manifest[pod] = existing?.usage === 'exempt' ? existing : {usage: 'exempt', reason: '', reviewed: ''};
}
writeManifest(manifest);
const blank = Object.entries(manifest).filter(([, entry]) => entry.usage === 'exempt' && !entry.reason);
console.log(`Wrote ${Object.keys(manifest).length} pods to ${path.relative(process.cwd(), MANIFEST_PATH)}.`);
if (blank.length) {
console.log(`\n${blank.length} pod(s) need a reason before the check will pass:`);
for (const [pod] of blank) {
console.log(` ${pod}`);
}
}
}
main();