{"version":3,"file":"prune.cjs","names":["join","resolve","dirname","existsSync","statSync","readdirSync","relative","isAbsolute","isFrameworkOwnedSyncFile","readFileSync"],"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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"}