UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

220 lines (218 loc) • 7.92 kB
const require_sync = require('./sync.cjs'); let node_fs = require("node:fs"); let node_path = require("node:path"); //#region src/cli/prune.ts const EXTENSIONS = [ "", ".ts", ".tsx", ".css", ".json", ".png", ".gif", ".svg", ".jpg", ".webp", ".ico", ".mjs" ]; function splitNamedImports(clause) { let wholeModule = false; const names = []; const withoutNamed = clause.replace(/\{[^}]*\}/g, " ").replace(/^\s*(?:import|export)\b/, ""); for (const piece of withoutNamed.split(",")) { const token = piece.trim(); if (!token) continue; if (token.includes("*")) { wholeModule = true; continue; } if (/^[A-Za-z_$][\w$]*$/.test(token)) wholeModule = true; } for (const named of clause.matchAll(/\{([^}]*)\}/g)) for (const piece of named[1].split(",")) { const name = piece.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim(); if (name && name !== "default") names.push(name); } return { names, wholeModule }; } function collectImportRefs(content) { const refs = []; for (const match of content.matchAll(/(?:import|export)\s+[\s\S]*?from\s*["']([^"']+)["']/g)) { const { names, wholeModule } = splitNamedImports(match[0].slice(0, match[0].lastIndexOf("from"))); refs.push({ names: wholeModule ? null : names, source: match[1] }); } for (const match of content.matchAll(/import\s*["']([^"']+)["']/g)) refs.push({ names: null, source: match[1] }); for (const match of content.matchAll(/import\(\s*["']([^"']+)["']\s*\)/g)) refs.push({ names: null, source: match[1] }); return refs; } function resolveSpecifier(specifier, fromFile, uiDir) { let base = null; if (specifier.startsWith("@/")) base = (0, node_path.join)(uiDir, specifier.slice(2)); else if (specifier.startsWith("./") || specifier.startsWith("../")) base = (0, node_path.resolve)((0, node_path.dirname)(fromFile), specifier); else return null; const candidates = []; for (const ext of EXTENSIONS) candidates.push(`${base}${ext}`); for (const ext of EXTENSIONS) candidates.push((0, node_path.join)(base, `index${ext}`)); for (const candidate of candidates) if ((0, node_fs.existsSync)(candidate) && (0, node_fs.statSync)(candidate).isFile()) return candidate; return null; } function parseBarrelExports(content, barrelPath, srcDir) { const exports = []; for (const match of content.matchAll(/export\s+\{([^}]*)\}\s*from\s*["']([^"']+)["']/g)) for (const piece of match[1].split(",")) { const name = piece.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim(); if (!name || name === "default") continue; exports.push({ name, source: resolveSpecifier(match[2], barrelPath, srcDir) }); } return exports; } function listFilesRecursive(dir, out = []) { if (!(0, node_fs.existsSync)(dir)) return out; for (const entry of (0, node_fs.readdirSync)(dir)) { const full = (0, node_path.join)(dir, entry); if ((0, node_fs.statSync)(full).isDirectory()) listFilesRecursive(full, out); else out.push(full); } return out; } function isProtected(relPath, srcDir) { const withinSrc = (0, node_path.relative)(srcDir, relPath); if (withinSrc.startsWith("..") || (0, node_path.isAbsolute)(withinSrc)) return true; if (!withinSrc.includes("/")) return true; if (withinSrc.startsWith("routes/")) return true; if (/\.gen\.(ts|tsx|json)$/.test(withinSrc)) return true; if (require_sync.isFrameworkOwnedSyncFile((0, node_path.join)("ui/src", withinSrc))) return true; return false; } function subjectOfTestFile(file) { const match = file.match(/^(?:(?<dir>.+)\/)?(?<name>.+)\.test\.tsx?$/); if (!match?.groups) return null; const base = match.groups.dir ? `${match.groups.dir}/${match.groups.name}` : match.groups.name; for (const ext of [".tsx", ".ts"]) if ((0, node_fs.existsSync)(`${base}${ext}`)) return `${base}${ext}`; return null; } /** * Prune copied ui source files that no copied route or framework-owned file * can reach via static imports. Named imports through the components barrel * keep only the named exports' files alive; a namespace/default import of the * barrel keeps everything it exports. Framework-owned files, generated types, * and route files are never pruned; a test file lives or dies with the file * it tests. */ function pruneUnusedUiFiles(targetDir, options = {}) { const srcDir = (0, node_path.join)(targetDir, "ui", "src"); if (!(0, node_fs.existsSync)(srcDir)) return { pruned: [], reachable: 0 }; const allFiles = listFilesRecursive(srcDir); const barrelPath = (0, node_path.join)(srcDir, "components", "index.ts"); const barrelExports = (0, node_fs.existsSync)(barrelPath) ? parseBarrelExports((0, node_fs.readFileSync)(barrelPath, "utf-8"), barrelPath, srcDir) : []; let barrelSaturated = false; const reachable = /* @__PURE__ */ new Set(); const queue = []; const saturateBarrel = () => { for (const exp of barrelExports) if (exp.source) visit(exp.source, null); }; function visit(file, wantedNames) { if (file === barrelPath) { if (wantedNames === null) barrelSaturated = true; if (reachable.has(file)) { if (wantedNames === null) saturateBarrel(); return; } reachable.add(file); if (wantedNames === null) saturateBarrel(); else for (const exp of barrelExports) if (exp.source && wantedNames.includes(exp.name)) visit(exp.source, null); return; } if (reachable.has(file)) return; reachable.add(file); queue.push(file); } for (const file of allFiles) if (isProtected(file, srcDir)) visit(file, null); while (queue.length > 0) { const file = queue.shift(); if (!(0, node_fs.existsSync)(file)) continue; if (file === barrelPath) continue; const content = (0, node_fs.readFileSync)(file, "utf-8"); for (const ref of collectImportRefs(content)) { const resolved = resolveSpecifier(ref.source, file, srcDir); if (!resolved) continue; visit(resolved, ref.names); } } if (barrelSaturated) saturateBarrel(); const removed = []; for (const file of allFiles) { if (reachable.has(file)) continue; if (isProtected(file, srcDir)) continue; const subject = subjectOfTestFile(file); if (subject && (reachable.has(subject) || isProtected(subject, srcDir))) continue; (0, node_fs.rmSync)(file); removed.push((0, node_path.relative)(targetDir, file)); } if ((0, node_fs.existsSync)(barrelPath) && removed.length > 0) { const content = (0, node_fs.readFileSync)(barrelPath, "utf-8"); const spans = []; for (const match of content.matchAll(/export\s*\{[^}]*\}\s*from\s*["'][^"']+["'];?/g)) { const fromMatch = match[0].match(/from\s*["']([^"']+)["']/); if (!fromMatch) continue; const target = resolveSpecifier(fromMatch[1], barrelPath, srcDir); if (!target || !(0, node_fs.existsSync)(target)) spans.push([match.index, match.index + match[0].length]); } if (spans.length > 0) { let next = ""; let cursor = 0; for (const [start, end] of spans) { next += content.slice(cursor, start); cursor = end; } next += content.slice(cursor); next = next.replace(/\n{3,}/g, "\n\n"); (0, node_fs.writeFileSync)(barrelPath, next); } } for (const candidate of [ "components", "lib", "hooks", "providers", "integrations" ]) removeIfEmpty((0, node_path.join)(srcDir, candidate)); if (removed.length > 0) options.log?.(`pruned ${removed.length} unreferenced ui file(s)`); return { pruned: removed, reachable: reachable.size }; } function removeIfEmpty(dir) { if (!(0, node_fs.existsSync)(dir)) return; const entries = (0, node_fs.readdirSync)(dir); if (entries.length === 0) { (0, node_fs.rmSync)(dir, { recursive: true }); return; } for (const entry of entries) { const full = (0, node_path.join)(dir, entry); if ((0, node_fs.statSync)(full).isDirectory()) removeIfEmpty(full); } if ((0, node_fs.readdirSync)(dir).length === 0) (0, node_fs.rmSync)(dir, { recursive: true }); } //#endregion exports.pruneUnusedUiFiles = pruneUnusedUiFiles; //# sourceMappingURL=prune.cjs.map