everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
1 lines • 15.3 kB
Source Map (JSON)
{"version":3,"file":"prune.mjs","names":[],"sources":["../../src/cli/prune.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, relative, resolve } from \"node:path\";\nimport { isFrameworkOwnedSyncFile } from \"./sync\";\n\nconst EXTENSIONS = [\n \"\",\n \".ts\",\n \".tsx\",\n \".css\",\n \".json\",\n \".png\",\n \".gif\",\n \".svg\",\n \".jpg\",\n \".webp\",\n \".ico\",\n \".mjs\",\n];\n\nexport interface PruneResult {\n pruned: string[];\n reachable: number;\n}\n\ninterface ImportRef {\n names: string[] | null;\n source: string;\n}\n\nfunction splitNamedImports(clause: string): { names: string[]; wholeModule: boolean } {\n let wholeModule = false;\n const names: string[] = [];\n const withoutNamed = clause.replace(/\\{[^}]*\\}/g, \" \").replace(/^\\s*(?:import|export)\\b/, \"\");\n for (const piece of withoutNamed.split(\",\")) {\n const token = piece.trim();\n if (!token) continue;\n if (token.includes(\"*\")) {\n wholeModule = true;\n continue;\n }\n if (/^[A-Za-z_$][\\w$]*$/.test(token)) wholeModule = true;\n }\n const namedRe = /\\{([^}]*)\\}/g;\n for (const named of clause.matchAll(namedRe)) {\n for (const piece of named[1].split(\",\")) {\n const name = piece\n .trim()\n .replace(/^type\\s+/, \"\")\n .split(/\\s+as\\s+/)[0]\n ?.trim();\n if (name && name !== \"default\") names.push(name);\n }\n }\n return { names, wholeModule };\n}\n\nfunction collectImportRefs(content: string): ImportRef[] {\n const refs: ImportRef[] = [];\n const fromRe = /(?:import|export)\\s+[\\s\\S]*?from\\s*[\"']([^\"']+)[\"']/g;\n for (const match of content.matchAll(fromRe)) {\n const clause = match[0].slice(0, match[0].lastIndexOf(\"from\"));\n const { names, wholeModule } = splitNamedImports(clause);\n refs.push({ names: wholeModule ? null : names, source: match[1] });\n }\n for (const match of content.matchAll(/import\\s*[\"']([^\"']+)[\"']/g)) {\n refs.push({ names: null, source: match[1] });\n }\n // Dynamic imports keep the target alive (conservative)\n for (const match of content.matchAll(/import\\(\\s*[\"']([^\"']+)[\"']\\s*\\)/g)) {\n refs.push({ names: null, source: match[1] });\n }\n return refs;\n}\n\nfunction resolveSpecifier(specifier: string, fromFile: string, uiDir: string): string | null {\n let base: string | null = null;\n if (specifier.startsWith(\"@/\")) {\n base = join(uiDir, specifier.slice(2));\n } else if (specifier.startsWith(\"./\") || specifier.startsWith(\"../\")) {\n base = resolve(dirname(fromFile), specifier);\n } else {\n return null;\n }\n const candidates: string[] = [];\n for (const ext of EXTENSIONS) {\n candidates.push(`${base}${ext}`);\n }\n for (const ext of EXTENSIONS) {\n candidates.push(join(base, `index${ext}`));\n }\n for (const candidate of candidates) {\n if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;\n }\n return null;\n}\n\ninterface BarrelExport {\n name: string;\n source: string | null;\n}\n\nfunction parseBarrelExports(content: string, barrelPath: string, srcDir: string): BarrelExport[] {\n const exports: BarrelExport[] = [];\n const re = /export\\s+\\{([^}]*)\\}\\s*from\\s*[\"']([^\"']+)[\"']/g;\n for (const match of content.matchAll(re)) {\n for (const piece of match[1].split(\",\")) {\n const name = piece\n .trim()\n .replace(/^type\\s+/, \"\")\n .split(/\\s+as\\s+/)[0]\n ?.trim();\n if (!name || name === \"default\") continue;\n exports.push({ name, source: resolveSpecifier(match[2], barrelPath, srcDir) });\n }\n }\n return exports;\n}\n\nfunction listFilesRecursive(dir: string, out: string[] = []): string[] {\n if (!existsSync(dir)) return out;\n for (const entry of readdirSync(dir)) {\n const full = join(dir, entry);\n const stat = statSync(full);\n if (stat.isDirectory()) {\n listFilesRecursive(full, out);\n } else {\n out.push(full);\n }\n }\n return out;\n}\n\nfunction isProtected(relPath: string, srcDir: string): boolean {\n const withinSrc = relative(srcDir, relPath);\n if (withinSrc.startsWith(\"..\") || isAbsolute(withinSrc)) return true;\n // Root-level src files (entry, app, compose, styles, globals) are the\n // framework's entry surface — never pruned.\n if (!withinSrc.includes(\"/\")) return true;\n if (withinSrc.startsWith(\"routes/\")) return true;\n if (/\\.gen\\.(ts|tsx|json)$/.test(withinSrc)) return true;\n if (isFrameworkOwnedSyncFile(join(\"ui/src\", withinSrc))) return true;\n return false;\n}\n\nfunction subjectOfTestFile(file: string): string | null {\n const match = file.match(/^(?:(?<dir>.+)\\/)?(?<name>.+)\\.test\\.tsx?$/);\n if (!match?.groups) return null;\n const base = match.groups.dir ? `${match.groups.dir}/${match.groups.name}` : match.groups.name;\n for (const ext of [\".tsx\", \".ts\"]) {\n if (existsSync(`${base}${ext}`)) return `${base}${ext}`;\n }\n return null;\n}\n\n/**\n * Prune copied ui source files that no copied route or framework-owned file\n * can reach via static imports. Named imports through the components barrel\n * keep only the named exports' files alive; a namespace/default import of the\n * barrel keeps everything it exports. Framework-owned files, generated types,\n * and route files are never pruned; a test file lives or dies with the file\n * it tests.\n */\nexport function pruneUnusedUiFiles(\n targetDir: string,\n options: { log?: (message: string) => void } = {},\n): PruneResult {\n const srcDir = join(targetDir, \"ui\", \"src\");\n if (!existsSync(srcDir)) return { pruned: [], reachable: 0 };\n\n const allFiles = listFilesRecursive(srcDir);\n\n const barrelPath = join(srcDir, \"components\", \"index.ts\");\n const barrelExports = existsSync(barrelPath)\n ? parseBarrelExports(readFileSync(barrelPath, \"utf-8\"), barrelPath, srcDir)\n : [];\n let barrelSaturated = false;\n\n const reachable = new Set<string>();\n const queue: string[] = [];\n\n const saturateBarrel = (): void => {\n for (const exp of barrelExports) {\n if (exp.source) visit(exp.source, null);\n }\n };\n\n function visit(file: string, wantedNames: string[] | null): void {\n if (file === barrelPath) {\n if (wantedNames === null) {\n barrelSaturated = true;\n }\n if (reachable.has(file)) {\n if (wantedNames === null) saturateBarrel();\n return;\n }\n reachable.add(file);\n if (wantedNames === null) {\n saturateBarrel();\n } else {\n for (const exp of barrelExports) {\n if (exp.source && wantedNames.includes(exp.name)) visit(exp.source, null);\n }\n }\n return;\n }\n if (reachable.has(file)) return;\n reachable.add(file);\n queue.push(file);\n }\n\n for (const file of allFiles) {\n if (isProtected(file, srcDir)) visit(file, null);\n }\n\n while (queue.length > 0) {\n const file = queue.shift()!;\n if (!existsSync(file)) continue;\n // The barrel's own export statements are traversed only through the\n // wanted-names path in visit() — never as whole-file imports.\n if (file === barrelPath) continue;\n const content = readFileSync(file, \"utf-8\");\n for (const ref of collectImportRefs(content)) {\n const resolved = resolveSpecifier(ref.source, file, srcDir);\n if (!resolved) continue;\n visit(resolved, ref.names);\n }\n }\n if (barrelSaturated) saturateBarrel();\n\n const removed: string[] = [];\n for (const file of allFiles) {\n if (reachable.has(file)) continue;\n if (isProtected(file, srcDir)) continue;\n const subject = subjectOfTestFile(file);\n if (subject && (reachable.has(subject) || isProtected(subject, srcDir))) continue;\n rmSync(file);\n removed.push(relative(targetDir, file));\n }\n\n // Drop barrel export statements pointing at pruned files (statement spans,\n // so multi-line `export { ... } from \"...\"` blocks are removed whole).\n if (existsSync(barrelPath) && removed.length > 0) {\n const content = readFileSync(barrelPath, \"utf-8\");\n const spans: Array<[number, number]> = [];\n for (const match of content.matchAll(/export\\s*\\{[^}]*\\}\\s*from\\s*[\"'][^\"']+[\"'];?/g)) {\n const fromMatch = match[0].match(/from\\s*[\"']([^\"']+)[\"']/);\n if (!fromMatch) continue;\n const target = resolveSpecifier(fromMatch[1], barrelPath, srcDir);\n if (!target || !existsSync(target)) {\n spans.push([match.index, match.index + match[0].length]);\n }\n }\n if (spans.length > 0) {\n let next = \"\";\n let cursor = 0;\n for (const [start, end] of spans) {\n next += content.slice(cursor, start);\n cursor = end;\n }\n next += content.slice(cursor);\n // Collapse blank-line runs left behind by removed statements\n next = next.replace(/\\n{3,}/g, \"\\n\\n\");\n writeFileSync(barrelPath, next);\n }\n }\n\n // Remove directories left empty by the prune (git ignores empty dirs, but\n // the scaffold on disk should not carry hollow folders).\n for (const candidate of [\"components\", \"lib\", \"hooks\", \"providers\", \"integrations\"]) {\n const dir = join(srcDir, candidate);\n removeIfEmpty(dir);\n }\n\n if (removed.length > 0) {\n options.log?.(`pruned ${removed.length} unreferenced ui file(s)`);\n }\n return { pruned: removed, reachable: reachable.size };\n}\n\nfunction removeIfEmpty(dir: string): void {\n if (!existsSync(dir)) return;\n const entries = readdirSync(dir);\n if (entries.length === 0) {\n rmSync(dir, { recursive: true });\n return;\n }\n for (const entry of entries) {\n const full = join(dir, entry);\n if (statSync(full).isDirectory()) removeIfEmpty(full);\n }\n if (readdirSync(dir).length === 0) rmSync(dir, { recursive: true });\n}\n"],"mappings":"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"}