afterbefore 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/logger.ts","../src/utils/git.ts","../src/pipeline.ts","../src/cleanup.ts","../src/utils/fs.ts","../src/utils/port.ts","../src/stages/diff.ts","../src/stages/classify.ts","../src/stages/graph.ts","../src/stages/resolve.ts","../src/utils/nextjs.ts","../src/stages/impact.ts","../src/stages/worktree.ts","../src/utils/pm.ts","../src/stages/server.ts","../src/stages/capture.ts","../src/stages/compare.ts","../src/stages/report.ts","../src/templates/report.html.ts","../src/templates/slider.html.ts","../src/templates/summary.md.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { logger } from \"./logger.js\";\nimport { isGitRepo } from \"./utils/git.js\";\nimport { runPipeline } from \"./pipeline.js\";\nimport type { PipelineOptions } from \"./types.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"afterbefore\")\n .description(\n \"Automatic before/after screenshot capture for PRs. Git diff is the config.\"\n )\n .version(\"0.1.0\")\n .option(\"--base <ref>\", \"Base branch or ref to compare against\", \"main\")\n .option(\"--output <dir>\", \"Output directory for screenshots\", \".afterbefore\")\n .option(\"--post\", \"Post results as a PR comment via gh CLI\", false)\n .option(\n \"--threshold <percent>\",\n \"Diff threshold percentage (changes below this are ignored)\",\n \"0.1\"\n )\n .action(async (opts) => {\n const cwd = process.cwd();\n\n // Validate: must be in a git repo\n if (!isGitRepo(cwd)) {\n logger.error(\"Not a git repository. Run this from inside a git repo.\");\n process.exit(1);\n }\n\n const options: PipelineOptions = {\n base: opts.base,\n output: opts.output,\n post: opts.post,\n threshold: parseFloat(opts.threshold),\n cwd,\n };\n\n try {\n await runPipeline(options);\n } catch (err) {\n logger.error(\n err instanceof Error ? err.message : `Unexpected error: ${String(err)}`\n );\n process.exit(1);\n }\n });\n\nprogram.parse();\n","import chalk from \"chalk\";\nimport ora, { type Ora } from \"ora\";\n\nexport class Logger {\n private spinner: Ora | null = null;\n\n info(message: string): void {\n this.clearSpinner();\n console.log(chalk.blue(\"ℹ\"), message);\n }\n\n success(message: string): void {\n this.clearSpinner();\n console.log(chalk.green(\"✔\"), message);\n }\n\n warn(message: string): void {\n this.clearSpinner();\n console.log(chalk.yellow(\"⚠\"), message);\n }\n\n error(message: string): void {\n this.clearSpinner();\n console.error(chalk.red(\"✖\"), message);\n }\n\n dim(message: string): void {\n this.clearSpinner();\n console.log(chalk.dim(message));\n }\n\n spin(message: string): Ora {\n this.clearSpinner();\n this.spinner = ora(message).start();\n return this.spinner;\n }\n\n stopSpinner(): void {\n this.clearSpinner();\n }\n\n private clearSpinner(): void {\n if (this.spinner) {\n this.spinner.stop();\n this.spinner = null;\n }\n }\n}\n\nexport const logger = new Logger();\n","import { execSync } from \"child_process\";\n\nexport function git(args: string, cwd?: string): string {\n return execSync(`git ${args}`, {\n cwd,\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }).trim();\n}\n\nexport function isGitRepo(cwd?: string): boolean {\n try {\n git(\"rev-parse --is-inside-work-tree\", cwd);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function getMergeBase(base: string, cwd?: string): string {\n return git(`merge-base ${base} HEAD`, cwd);\n}\n\nexport function getDiffNameStatus(base: string, cwd?: string): string {\n const mergeBase = getMergeBase(base, cwd);\n return git(`diff --name-status ${mergeBase}`, cwd);\n}\n\nexport function getCurrentBranch(cwd?: string): string {\n return git(\"rev-parse --abbrev-ref HEAD\", cwd);\n}\n\nexport function getRepoRoot(cwd?: string): string {\n return git(\"rev-parse --show-toplevel\", cwd);\n}\n","import { resolve } from \"path\";\nimport type { PipelineOptions } from \"./types.js\";\nimport { logger } from \"./logger.js\";\nimport { cleanupRegistry } from \"./cleanup.js\";\nimport { ensureDir } from \"./utils/fs.js\";\nimport { findAvailablePort } from \"./utils/port.js\";\nimport { getChangedFiles } from \"./stages/diff.js\";\nimport { classifyFiles, filterVisuallyRelevant } from \"./stages/classify.js\";\nimport { buildImportGraph } from \"./stages/graph.js\";\nimport { findAffectedRoutes } from \"./stages/impact.js\";\nimport { createWorktree } from \"./stages/worktree.js\";\nimport { startServer, stopServer } from \"./stages/server.js\";\nimport { captureRoutes } from \"./stages/capture.js\";\nimport { compareScreenshots } from \"./stages/compare.js\";\nimport { generateReport } from \"./stages/report.js\";\n\nexport async function runPipeline(options: PipelineOptions): Promise<void> {\n const { base, output, post, cwd } = options;\n const outputDir = resolve(cwd, output);\n\n try {\n // 1. Get changed files\n const spinner = logger.spin(\"Analyzing git diff...\");\n const diffFiles = getChangedFiles(base, cwd);\n spinner.stop();\n\n if (diffFiles.length === 0) {\n logger.success(\"No changed files detected. Nothing to do.\");\n return;\n }\n\n logger.info(`Found ${diffFiles.length} changed file(s)`);\n\n // 2. Classify files\n const classified = classifyFiles(diffFiles);\n const visualFiles = filterVisuallyRelevant(classified);\n const impactfulFiles = classified.filter(\n (f) => f.category !== \"test\" && f.category !== \"other\"\n );\n\n if (impactfulFiles.length === 0) {\n logger.success(\n \"No visually relevant changes detected (only test/other files changed).\"\n );\n return;\n }\n\n logger.info(\n `${visualFiles.length} visually relevant file(s), ${impactfulFiles.length} potentially impactful`\n );\n\n // 3. Build import graph\n const graphSpinner = logger.spin(\"Building import graph...\");\n const graph = await buildImportGraph(cwd);\n graphSpinner.stop();\n\n // 4. Find affected routes (use all impactful files, not just visual ones)\n const changedPaths = impactfulFiles.map((f) => f.path);\n const affectedRoutes = findAffectedRoutes(changedPaths, graph, cwd);\n\n if (affectedRoutes.length === 0) {\n logger.success(\n \"No affected routes found. Changed files don't impact any pages.\"\n );\n return;\n }\n\n logger.info(\n `Affected routes: ${affectedRoutes.map((r) => r.route).join(\", \")}`\n );\n\n // 5. Create output directory\n await ensureDir(outputDir);\n\n // 6. Create worktree for base version\n const worktree = await createWorktree(base, cwd);\n\n // 7. Start dev servers\n const beforePort = await findAvailablePort();\n const afterPort = await findAvailablePort(new Set([beforePort]));\n\n const beforeServer = await startServer(worktree.path, beforePort);\n cleanupRegistry.register(() => stopServer(beforeServer));\n\n const afterServer = await startServer(cwd, afterPort);\n cleanupRegistry.register(() => stopServer(afterServer));\n\n // 8. Capture screenshots\n const routes = affectedRoutes.map((r) => r.route);\n const captures = await captureRoutes(\n routes,\n beforeServer.url,\n afterServer.url,\n outputDir\n );\n\n // 9. Compare screenshots\n const results = await compareScreenshots(captures, outputDir, options.threshold);\n\n // 10. Generate report\n await generateReport(results, outputDir, { post });\n\n // Summary\n const changedCount = results.filter((r) => r.changed).length;\n logger.success(\n `Done! ${results.length} route(s) captured, ${changedCount} with visual changes.`\n );\n logger.info(`Output: ${outputDir}`);\n logger.dim(`Open ${resolve(outputDir, \"index.html\")} in your browser to view the report.`);\n } finally {\n await cleanupRegistry.runAll();\n }\n}\n","import { logger } from \"./logger.js\";\n\ntype CleanupFn = () => Promise<void> | void;\n\nexport class CleanupRegistry {\n private cleanups: CleanupFn[] = [];\n private registered = false;\n\n register(fn: CleanupFn): void {\n this.cleanups.push(fn);\n this.ensureSignalHandlers();\n }\n\n async runAll(): Promise<void> {\n // LIFO order — tear down in reverse\n const fns = [...this.cleanups].reverse();\n this.cleanups = [];\n\n for (const fn of fns) {\n try {\n await fn();\n } catch (err) {\n logger.warn(\n `Cleanup failed: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n }\n\n private ensureSignalHandlers(): void {\n if (this.registered) return;\n this.registered = true;\n\n const handler = async (signal: string) => {\n logger.dim(`\\nReceived ${signal}, cleaning up...`);\n await this.runAll();\n process.exit(signal === \"SIGINT\" ? 130 : 143);\n };\n\n process.on(\"SIGINT\", () => handler(\"SIGINT\"));\n process.on(\"SIGTERM\", () => handler(\"SIGTERM\"));\n process.on(\"exit\", () => {\n // Synchronous fallback — can't await here but catches missed cleanups\n if (this.cleanups.length > 0) {\n for (const fn of [...this.cleanups].reverse()) {\n try {\n fn();\n } catch {\n // Best effort\n }\n }\n this.cleanups = [];\n }\n });\n }\n}\n\nexport const cleanupRegistry = new CleanupRegistry();\n","import { mkdir } from \"fs/promises\";\n\nexport async function ensureDir(dir: string): Promise<void> {\n await mkdir(dir, { recursive: true });\n}\n","import { createServer } from \"net\";\n\nfunction findPort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = createServer();\n server.listen(0, () => {\n const addr = server.address();\n if (!addr || typeof addr === \"string\") {\n server.close();\n reject(new Error(\"Failed to get port\"));\n return;\n }\n const port = addr.port;\n server.close(() => resolve(port));\n });\n server.on(\"error\", reject);\n });\n}\n\nexport async function findAvailablePort(\n exclude?: Set<number>\n): Promise<number> {\n for (let i = 0; i < 5; i++) {\n const port = await findPort();\n if (!exclude || !exclude.has(port)) return port;\n }\n throw new Error(\"Failed to find available port after 5 attempts\");\n}\n","import type { DiffFile, DiffStatus } from \"../types.js\";\nimport { getDiffNameStatus } from \"../utils/git.js\";\n\nconst VALID_STATUSES = new Set([\"A\", \"M\", \"D\", \"R\", \"C\"]);\n\n/**\n * Parse git diff --name-status output into structured DiffFile array.\n */\nexport function parseDiffOutput(raw: string): DiffFile[] {\n if (!raw.trim()) return [];\n\n const results: DiffFile[] = [];\n\n for (const line of raw.trim().split(\"\\n\")) {\n const parts = line.split(\"\\t\");\n const statusRaw = parts[0].charAt(0);\n\n if (!VALID_STATUSES.has(statusRaw)) continue;\n\n const status = statusRaw as DiffStatus;\n\n if (status === \"R\" || status === \"C\") {\n results.push({ status, oldPath: parts[1], path: parts[2] });\n } else {\n results.push({ status, path: parts[1] });\n }\n }\n\n return results;\n}\n\n/**\n * Get changed files between base and HEAD.\n */\nexport function getChangedFiles(base: string, cwd?: string): DiffFile[] {\n const raw = getDiffNameStatus(base, cwd);\n return parseDiffOutput(raw);\n}\n","import type { ClassifiedFile, DiffFile, FileCategory } from \"../types.js\";\n\n/** Categories that can affect visual output */\nconst VISUAL_CATEGORIES: Set<FileCategory> = new Set([\n \"page\",\n \"component\",\n \"style\",\n \"layout\",\n]);\n\n/**\n * Classify a file path into a category based on patterns.\n */\nexport function classifyFile(filePath: string): FileCategory {\n const p = filePath.replace(/^src\\//, \"\");\n\n // Test files\n if (\n /\\.(test|spec)\\.[tj]sx?$/.test(p) ||\n /^tests?\\//.test(p) ||\n /\\/__tests__\\//.test(p) ||\n p.includes(\".test.\") ||\n p.includes(\".spec.\")\n ) {\n return \"test\";\n }\n\n // Config files\n if (\n /^(tsconfig|next\\.config|tailwind\\.config|postcss\\.config|\\.eslint|\\.prettier|vitest\\.config|jest\\.config)/.test(\n p\n ) ||\n p === \"package.json\" ||\n p === \"package-lock.json\" ||\n p.endsWith(\".config.ts\") ||\n p.endsWith(\".config.js\") ||\n p.endsWith(\".config.mjs\")\n ) {\n return \"config\";\n }\n\n // Style files\n if (/\\.(css|scss|sass|less)$/.test(p) || p === \"tailwind.config.ts\") {\n return \"style\";\n }\n\n // Layout files (Next.js app router)\n if (/\\/layout\\.[tj]sx?$/.test(p) || /^app\\/layout\\.[tj]sx?$/.test(p)) {\n return \"layout\";\n }\n\n // Page files (Next.js app router)\n if (/\\/page\\.[tj]sx?$/.test(p) || /^app\\/page\\.[tj]sx?$/.test(p)) {\n return \"page\";\n }\n\n // Component files\n if (\n /^(components|app)\\//.test(p) &&\n /\\.[tj]sx?$/.test(p)\n ) {\n return \"component\";\n }\n\n // Utility files (lib, utils, hooks, etc.)\n if (\n /^(lib|utils|hooks|helpers|services|api)\\//.test(p) &&\n /\\.[tj]sx?$/.test(p)\n ) {\n return \"utility\";\n }\n\n // TypeScript/JavaScript files not in known dirs\n if (/\\.[tj]sx?$/.test(p)) {\n return \"component\";\n }\n\n return \"other\";\n}\n\n/**\n * Classify all diff files and return with categories.\n */\nexport function classifyFiles(files: DiffFile[]): ClassifiedFile[] {\n return files.map((f) => ({\n ...f,\n category: classifyFile(f.path),\n }));\n}\n\n/**\n * Filter to only visually relevant files.\n */\nexport function filterVisuallyRelevant(\n files: ClassifiedFile[]\n): ClassifiedFile[] {\n return files.filter((f) => VISUAL_CATEGORIES.has(f.category));\n}\n","import { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport { init, parse } from \"es-module-lexer\";\nimport type { ImportGraph } from \"../types.js\";\nimport { logger } from \"../logger.js\";\nimport { createResolver } from \"./resolve.js\";\n\nconst SOURCE_EXTENSIONS = new Set([\".ts\", \".tsx\", \".js\", \".jsx\"]);\nconst SOURCE_DIRS = [\"app\", \"src\", \"components\", \"lib\"];\n\n/**\n * Recursively collect all source files from a directory.\n */\nfunction collectFiles(dir: string): string[] {\n const results: string[] = [];\n\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return results;\n }\n\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n\n if (entry.isDirectory()) {\n // Skip node_modules, .next, dist, etc.\n if (entry.name.startsWith(\".\") || entry.name === \"node_modules\" || entry.name === \"dist\") {\n continue;\n }\n results.push(...collectFiles(fullPath));\n } else if (entry.isFile()) {\n const ext = entry.name.slice(entry.name.lastIndexOf(\".\"));\n if (SOURCE_EXTENSIONS.has(ext)) {\n results.push(fullPath);\n }\n }\n }\n\n return results;\n}\n\n/**\n * Parse imports from a file using es-module-lexer.\n */\nfunction parseImports(filePath: string): string[] {\n let source: string;\n try {\n source = readFileSync(filePath, \"utf-8\");\n } catch {\n return [];\n }\n\n try {\n // Strip TypeScript-specific syntax that es-module-lexer can't handle\n // Replace type imports/exports so the lexer can parse the rest\n const cleaned = source\n .replace(/import\\s+type\\s+/g, \"import \")\n .replace(/export\\s+type\\s+/g, \"export \");\n\n const [imports] = parse(cleaned);\n return imports\n .map((imp) => imp.n)\n .filter((n): n is string => n !== undefined && n !== \"\");\n } catch {\n // If parsing fails, fall back to regex-based extraction\n const specifiers: string[] = [];\n const importRegex = /(?:import|from)\\s+[\"']([^\"']+)[\"']/g;\n let match;\n while ((match = importRegex.exec(source)) !== null) {\n specifiers.push(match[1]);\n }\n return specifiers;\n }\n}\n\n/**\n * Build a bidirectional import graph for the project.\n */\nexport async function buildImportGraph(projectRoot: string): Promise<ImportGraph> {\n await init;\n\n const resolve = createResolver(projectRoot);\n\n // Collect all source files\n const allFiles: string[] = [];\n for (const dir of SOURCE_DIRS) {\n const fullDir = join(projectRoot, dir);\n allFiles.push(...collectFiles(fullDir));\n }\n\n // Deduplicate (src/ might contain app/, components/, lib/)\n const fileSet = new Set(allFiles);\n\n const forward = new Map<string, Set<string>>();\n const reverse = new Map<string, Set<string>>();\n\n for (const filePath of fileSet) {\n const relPath = relative(projectRoot, filePath);\n const specifiers = parseImports(filePath);\n\n const deps = new Set<string>();\n\n for (const spec of specifiers) {\n const resolved = resolve(spec, filePath);\n if (!resolved) continue;\n\n const relResolved = relative(projectRoot, resolved);\n deps.add(relResolved);\n\n // Add reverse edge\n if (!reverse.has(relResolved)) {\n reverse.set(relResolved, new Set());\n }\n reverse.get(relResolved)!.add(relPath);\n }\n\n forward.set(relPath, deps);\n }\n\n logger.dim(`Import graph: ${fileSet.size} files, ${countEdges(forward)} edges`);\n\n return { forward, reverse };\n}\n\nfunction countEdges(forward: Map<string, Set<string>>): number {\n let count = 0;\n for (const deps of forward.values()) {\n count += deps.size;\n }\n return count;\n}\n","import { existsSync, readFileSync, statSync } from \"node:fs\";\nimport { resolve, dirname, join } from \"node:path\";\nimport { logger } from \"../logger.js\";\n\ninterface PathMapping {\n prefix: string;\n targets: string[];\n}\n\nconst EXTENSIONS = [\".ts\", \".tsx\", \".js\", \".jsx\"];\n\n/**\n * Create an import specifier resolver for a project using its tsconfig.json paths.\n */\nexport function createResolver(\n projectRoot: string\n): (specifier: string, fromFile: string) => string | null {\n const mappings = loadPathMappings(projectRoot);\n const existsCache = new Map<string, boolean>();\n\n function cachedExists(p: string): boolean {\n const cached = existsCache.get(p);\n if (cached !== undefined) return cached;\n const result = existsSync(p);\n existsCache.set(p, result);\n return result;\n }\n\n function tryResolve(candidate: string): string | null {\n // Exact file\n if (cachedExists(candidate) && !isDirectory(candidate)) return candidate;\n\n // Try extensions\n for (const ext of EXTENSIONS) {\n const withExt = candidate + ext;\n if (cachedExists(withExt)) return withExt;\n }\n\n // Try index files\n for (const ext of EXTENSIONS) {\n const indexFile = join(candidate, `index${ext}`);\n if (cachedExists(indexFile)) return indexFile;\n }\n\n return null;\n }\n\n function isDirectory(p: string): boolean {\n try {\n return statSync(p).isDirectory();\n } catch {\n return false;\n }\n }\n\n return (specifier: string, fromFile: string): string | null => {\n // Relative imports\n if (specifier.startsWith(\".\")) {\n const dir = dirname(fromFile);\n const candidate = resolve(dir, specifier);\n return tryResolve(candidate);\n }\n\n // Try each path mapping\n for (const mapping of mappings) {\n if (!specifier.startsWith(mapping.prefix)) continue;\n\n const rest = specifier.slice(mapping.prefix.length);\n\n for (const target of mapping.targets) {\n const candidate = resolve(projectRoot, target + rest);\n const result = tryResolve(candidate);\n if (result) return result;\n }\n }\n\n // Not a relative or aliased import (node_module etc.)\n return null;\n };\n}\n\nfunction loadPathMappings(projectRoot: string): PathMapping[] {\n const tsconfigPath = join(projectRoot, \"tsconfig.json\");\n if (!existsSync(tsconfigPath)) {\n logger.dim(\"No tsconfig.json found, skipping path alias resolution\");\n return [];\n }\n\n try {\n const raw = readFileSync(tsconfigPath, \"utf-8\");\n // Strip comments (tsconfig allows them)\n const cleaned = raw.replace(/\\/\\/.*$/gm, \"\").replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n const config = JSON.parse(cleaned);\n\n const paths = config?.compilerOptions?.paths;\n if (!paths) return [];\n\n const baseUrl = config?.compilerOptions?.baseUrl || \".\";\n const mappings: PathMapping[] = [];\n\n for (const [pattern, targets] of Object.entries(paths)) {\n // Convert \"@ /*\" → prefix \"@/\"\n const prefix = pattern.replace(/\\*$/, \"\");\n const resolvedTargets = (targets as string[]).map((t) =>\n t.replace(/\\*$/, \"\")\n );\n // Prepend baseUrl to targets\n const absoluteTargets = resolvedTargets.map((t) =>\n join(baseUrl, t)\n );\n mappings.push({ prefix, targets: absoluteTargets });\n }\n\n return mappings;\n } catch (e) {\n logger.warn(`Failed to parse tsconfig.json: ${e}`);\n return [];\n }\n}\n","import { logger } from \"../logger.js\";\n\n/**\n * Convert a Next.js app router page path to a URL route.\n * e.g. \"app/about/page.tsx\" → \"/about\"\n * \"app/(marketing)/pricing/page.tsx\" → \"/pricing\"\n * \"app/page.tsx\" → \"/\"\n */\nexport function pagePathToRoute(pagePath: string): string | null {\n // Strip leading src/ if present\n let p = pagePath.replace(/^src\\//, \"\");\n\n // Must be under app/ and end with page.tsx/jsx/ts/js\n const match = p.match(/^app\\/(.*)\\/page\\.[tj]sx?$/);\n if (!match) {\n // Root page\n if (/^app\\/page\\.[tj]sx?$/.test(p)) return \"/\";\n return null;\n }\n\n let route = match[1];\n\n // Check for dynamic segments\n if (/\\[.*\\]/.test(route)) {\n logger.warn(`Skipping dynamic route: /${route} (dynamic segments not supported)`);\n return null;\n }\n\n // Strip route groups like (marketing)\n route = route\n .split(\"/\")\n .filter((seg) => !seg.startsWith(\"(\"))\n .join(\"/\");\n\n return `/${route}` || \"/\";\n}\n\n/**\n * Check if a file path is a Next.js page file.\n */\nexport function isPageFile(filePath: string): boolean {\n const p = filePath.replace(/^src\\//, \"\");\n return /^app\\/(.+\\/)?page\\.[tj]sx?$/.test(p);\n}\n\n/**\n * Check if a file path is a Next.js layout file.\n */\nexport function isLayoutFile(filePath: string): boolean {\n const p = filePath.replace(/^src\\//, \"\");\n return /^app\\/(.+\\/)?layout\\.[tj]sx?$/.test(p);\n}\n\n/**\n * Get the directory a layout file covers.\n * e.g. \"app/about/layout.tsx\" → \"app/about/\"\n * \"app/layout.tsx\" → \"app/\"\n */\nexport function getLayoutDir(filePath: string): string {\n const p = filePath.replace(/^src\\//, \"\");\n const match = p.match(/^(app\\/.*\\/)layout\\.[tj]sx?$/);\n if (!match) return \"app/\"; // root layout\n return match[1];\n}\n","import type { AffectedRoute, ImportGraph } from \"../types.js\";\nimport {\n isPageFile,\n isLayoutFile,\n getLayoutDir,\n pagePathToRoute,\n} from \"../utils/nextjs.js\";\nimport { logger } from \"../logger.js\";\n\nconst MAX_DEPTH = 3;\n\n/**\n * BFS up the reverse import graph to find affected page routes.\n * Also handles layout files — a layout change affects all pages\n * in the same directory and its descendants.\n */\nexport function findAffectedRoutes(\n changedFiles: string[],\n graph: ImportGraph,\n projectRoot: string\n): AffectedRoute[] {\n const routeMap = new Map<string, AffectedRoute>();\n\n for (const file of changedFiles) {\n // BFS from this changed file up the reverse graph\n const visited = new Set<string>();\n const queue: Array<{ path: string; depth: number }> = [\n { path: file, depth: 0 },\n ];\n visited.add(file);\n\n while (queue.length > 0) {\n const { path, depth } = queue.shift()!;\n\n // Check if this is a page file\n if (isPageFile(path)) {\n const route = pagePathToRoute(path);\n if (route !== null && !routeMap.has(route)) {\n routeMap.set(route, {\n pagePath: path,\n route,\n reason: depth === 0 ? \"direct\" : \"transitive\",\n depth,\n });\n }\n }\n\n // Don't BFS beyond max depth\n if (depth >= MAX_DEPTH) continue;\n\n // Walk reverse edges\n const importers = graph.reverse.get(path);\n if (!importers) continue;\n\n for (const importer of importers) {\n if (visited.has(importer)) continue;\n visited.add(importer);\n queue.push({ path: importer, depth: depth + 1 });\n }\n }\n }\n\n // Handle layout files: a layout wraps all pages in its directory subtree\n // via Next.js filesystem convention (no import required)\n for (const file of changedFiles) {\n if (!isLayoutFile(file)) continue;\n const layoutDir = getLayoutDir(file);\n\n for (const knownFile of graph.forward.keys()) {\n if (knownFile.startsWith(layoutDir) && isPageFile(knownFile)) {\n const route = pagePathToRoute(knownFile);\n if (route !== null && !routeMap.has(route)) {\n routeMap.set(route, {\n pagePath: knownFile,\n route,\n reason: \"transitive\",\n depth: 1,\n });\n }\n }\n }\n }\n\n const routes = Array.from(routeMap.values());\n logger.dim(`Found ${routes.length} affected route(s)`);\n return routes;\n}\n","import { execSync } from \"child_process\";\nimport { rm, mkdtemp } from \"fs/promises\";\nimport { join } from \"path\";\nimport { tmpdir } from \"os\";\nimport { logger } from \"../logger.js\";\nimport { cleanupRegistry } from \"../cleanup.js\";\nimport { detectPackageManager } from \"../utils/pm.js\";\nimport type { WorktreeInfo } from \"../types.js\";\n\nexport async function createWorktree(\n base: string,\n cwd: string\n): Promise<WorktreeInfo> {\n const worktreeDir = await mkdtemp(join(tmpdir(), \"afterbefore-wt-\"));\n\n logger.info(`Creating worktree for ${base} at ${worktreeDir}`);\n execSync(`git worktree add \"${worktreeDir}\" \"${base}\"`, {\n cwd,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n const pm = detectPackageManager(cwd);\n logger.info(`Installing dependencies with ${pm}`);\n execSync(`${pm} install`, {\n cwd: worktreeDir,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n const cleanup = async () => {\n logger.dim(`Cleaning up worktree at ${worktreeDir}`);\n try {\n execSync(`git worktree remove --force \"${worktreeDir}\"`, {\n cwd,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n } catch {\n try {\n await rm(worktreeDir, { recursive: true, force: true });\n execSync(\"git worktree prune\", {\n cwd,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n } catch {\n logger.warn(`Failed to clean up worktree at ${worktreeDir}`);\n }\n }\n };\n\n cleanupRegistry.register(cleanup);\n\n return { path: worktreeDir, ref: base, cleanup };\n}\n","import { existsSync } from \"fs\";\nimport { join } from \"path\";\n\nexport type PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\n\nexport function detectPackageManager(dir: string): PackageManager {\n if (existsSync(join(dir, \"bun.lockb\")) || existsSync(join(dir, \"bun.lock\")))\n return \"bun\";\n if (existsSync(join(dir, \"pnpm-lock.yaml\"))) return \"pnpm\";\n if (existsSync(join(dir, \"yarn.lock\"))) return \"yarn\";\n return \"npm\";\n}\n\n/** Get the exec command for running a package binary (like `next dev`) */\nexport function pmExec(pm: PackageManager): string {\n switch (pm) {\n case \"bun\":\n return \"bunx\";\n case \"pnpm\":\n return \"pnpm exec\";\n case \"yarn\":\n return \"yarn\";\n default:\n return \"npx\";\n }\n}\n","import { spawn } from \"child_process\";\nimport { logger } from \"../logger.js\";\nimport { detectPackageManager, pmExec } from \"../utils/pm.js\";\nimport type { ServerInfo } from \"../types.js\";\n\nfunction waitForServer(url: string, timeoutMs: number): Promise<void> {\n const start = Date.now();\n\n return new Promise((resolve, reject) => {\n const poll = async () => {\n if (Date.now() - start > timeoutMs) {\n reject(\n new Error(\n `Server at ${url} did not respond within ${timeoutMs / 1000}s. ` +\n `Make sure 'next dev' starts correctly in your project.`\n )\n );\n return;\n }\n\n try {\n await fetch(url);\n resolve();\n } catch {\n setTimeout(poll, 500);\n }\n };\n\n poll();\n });\n}\n\nexport async function startServer(\n projectDir: string,\n port: number\n): Promise<ServerInfo> {\n const url = `http://localhost:${port}`;\n const pm = detectPackageManager(projectDir);\n const exec = pmExec(pm);\n const [cmd, ...baseArgs] = exec.split(\" \");\n\n logger.info(`Starting Next.js dev server on ${url} (using ${pm})`);\n\n const child = spawn(cmd, [...baseArgs, \"next\", \"dev\", \"-p\", String(port)], {\n cwd: projectDir,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n detached: false,\n });\n\n // Log stderr for debugging but don't fail on it\n child.stderr?.on(\"data\", (data: Buffer) => {\n const msg = data.toString().trim();\n if (msg) logger.dim(`[next:${port}] ${msg}`);\n });\n\n await waitForServer(url, 60_000);\n logger.success(`Server ready at ${url}`);\n\n return { port, process: child, url };\n}\n\nexport async function stopServer(server: ServerInfo): Promise<void> {\n logger.dim(`Stopping server on port ${server.port}`);\n server.process.kill(\"SIGTERM\");\n\n // Wait briefly for graceful shutdown\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n server.process.kill(\"SIGKILL\");\n resolve();\n }, 5_000);\n\n server.process.on(\"exit\", () => {\n clearTimeout(timeout);\n resolve();\n });\n });\n}\n","import { join } from \"path\";\nimport { chromium } from \"playwright\";\nimport { logger } from \"../logger.js\";\nimport { ensureDir } from \"../utils/fs.js\";\nimport type { CaptureResult } from \"../types.js\";\n\nfunction routeToDir(route: string): string {\n if (route === \"/\") return \"_root\";\n return route.replace(/^\\//, \"\").replace(/\\//g, \"_\");\n}\n\nexport async function captureRoutes(\n routes: string[],\n beforeUrl: string,\n afterUrl: string,\n outputDir: string\n): Promise<CaptureResult[]> {\n logger.info(`Capturing ${routes.length} route(s)`);\n\n const browser = await chromium.launch();\n const results: CaptureResult[] = [];\n\n try {\n const context = await browser.newContext({\n viewport: { width: 1280, height: 720 },\n });\n const page = await context.newPage();\n\n for (const route of routes) {\n const dirName = routeToDir(route);\n const routeDir = join(outputDir, dirName);\n await ensureDir(routeDir);\n\n const beforePath = join(routeDir, \"before.png\");\n const afterPath = join(routeDir, \"after.png\");\n\n // Capture before\n logger.dim(`Capturing before: ${route}`);\n await page.goto(`${beforeUrl}${route}`, { waitUntil: \"networkidle\" });\n await page.waitForTimeout(500);\n await page.screenshot({ path: beforePath, fullPage: true });\n\n // Capture after\n logger.dim(`Capturing after: ${route}`);\n await page.goto(`${afterUrl}${route}`, { waitUntil: \"networkidle\" });\n await page.waitForTimeout(500);\n await page.screenshot({ path: afterPath, fullPage: true });\n\n results.push({ route, beforePath, afterPath });\n logger.success(`Captured ${route}`);\n }\n\n await context.close();\n } finally {\n await browser.close();\n }\n\n return results;\n}\n","import { readFileSync } from \"fs\";\nimport { writeFileSync } from \"fs\";\nimport { join, dirname } from \"path\";\nimport { PNG } from \"pngjs\";\nimport pixelmatch from \"pixelmatch\";\nimport sharp from \"sharp\";\nimport { logger } from \"../logger.js\";\nimport { ensureDir } from \"../utils/fs.js\";\nimport type { CaptureResult, CompareResult } from \"../types.js\";\n\nfunction readPng(filePath: string): PNG {\n const buffer = readFileSync(filePath);\n return PNG.sync.read(buffer);\n}\n\nfunction normalizeDimensions(img1: PNG, img2: PNG): [PNG, PNG] {\n const width = Math.max(img1.width, img2.width);\n const height = Math.max(img1.height, img2.height);\n\n const pad = (src: PNG): PNG => {\n if (src.width === width && src.height === height) return src;\n\n const padded = new PNG({ width, height });\n // Fill with white\n for (let i = 0; i < padded.data.length; i += 4) {\n padded.data[i] = 255;\n padded.data[i + 1] = 255;\n padded.data[i + 2] = 255;\n padded.data[i + 3] = 255;\n }\n // Copy source pixels\n PNG.bitblt(src, padded, 0, 0, src.width, src.height, 0, 0);\n return padded;\n };\n\n return [pad(img1), pad(img2)];\n}\n\nasync function generateSideBySide(\n beforePath: string,\n afterPath: string,\n outputPath: string\n): Promise<void> {\n const labelHeight = 32;\n const gap = 4;\n\n const beforeImg = sharp(beforePath);\n const afterImg = sharp(afterPath);\n const [beforeMeta, afterMeta] = await Promise.all([\n beforeImg.metadata(),\n afterImg.metadata(),\n ]);\n\n const bw = beforeMeta.width!;\n const bh = beforeMeta.height!;\n const aw = afterMeta.width!;\n const ah = afterMeta.height!;\n const totalWidth = bw + gap + aw;\n const totalHeight = labelHeight + Math.max(bh, ah);\n\n // Create label SVGs\n const labelSvg = (text: string, w: number) =>\n Buffer.from(\n `<svg width=\"${w}\" height=\"${labelHeight}\">\n <rect width=\"${w}\" height=\"${labelHeight}\" fill=\"#f3f4f6\"/>\n <text x=\"${w / 2}\" y=\"${labelHeight / 2 + 5}\" text-anchor=\"middle\"\n font-family=\"sans-serif\" font-size=\"14\" font-weight=\"bold\" fill=\"#374151\">${text}</text>\n </svg>`\n );\n\n const beforeLabel = await sharp(labelSvg(\"Before\", bw))\n .png()\n .toBuffer();\n const afterLabel = await sharp(labelSvg(\"After\", aw))\n .png()\n .toBuffer();\n\n // Composite before and after images with labels\n const beforeWithLabel = await sharp({\n create: {\n width: bw,\n height: labelHeight + bh,\n channels: 4,\n background: { r: 255, g: 255, b: 255, alpha: 1 },\n },\n })\n .composite([\n { input: beforeLabel, top: 0, left: 0 },\n { input: await sharp(beforePath).png().toBuffer(), top: labelHeight, left: 0 },\n ])\n .png()\n .toBuffer();\n\n const afterWithLabel = await sharp({\n create: {\n width: aw,\n height: labelHeight + ah,\n channels: 4,\n background: { r: 255, g: 255, b: 255, alpha: 1 },\n },\n })\n .composite([\n { input: afterLabel, top: 0, left: 0 },\n { input: await sharp(afterPath).png().toBuffer(), top: labelHeight, left: 0 },\n ])\n .png()\n .toBuffer();\n\n await sharp({\n create: {\n width: totalWidth,\n height: totalHeight,\n channels: 4,\n background: { r: 229, g: 231, b: 235, alpha: 1 },\n },\n })\n .composite([\n { input: beforeWithLabel, top: 0, left: 0 },\n { input: afterWithLabel, top: 0, left: bw + gap },\n ])\n .png()\n .toFile(outputPath);\n}\n\nexport async function compareScreenshots(\n captures: CaptureResult[],\n outputDir: string,\n threshold: number = 0.1\n): Promise<CompareResult[]> {\n logger.info(`Comparing ${captures.length} route(s)`);\n const results: CompareResult[] = [];\n\n for (const capture of captures) {\n const routeDir = dirname(capture.beforePath);\n const diffPath = join(routeDir, \"diff.png\");\n const sideBySidePath = join(routeDir, \"side-by-side.png\");\n const sliderPath = join(routeDir, \"slider.html\");\n\n await ensureDir(routeDir);\n\n // Read and normalize images\n const beforeImg = readPng(capture.beforePath);\n const afterImg = readPng(capture.afterPath);\n const [normBefore, normAfter] = normalizeDimensions(beforeImg, afterImg);\n\n const { width, height } = normBefore;\n const totalPixels = width * height;\n\n // Create diff image\n const diffImg = new PNG({ width, height });\n const diffPixels = pixelmatch(\n normBefore.data,\n normAfter.data,\n diffImg.data,\n width,\n height,\n { threshold: 0.1 }\n );\n\n writeFileSync(diffPath, PNG.sync.write(diffImg));\n\n // Generate side-by-side\n await generateSideBySide(\n capture.beforePath,\n capture.afterPath,\n sideBySidePath\n );\n\n const diffPercentage = (diffPixels / totalPixels) * 100;\n const changed = diffPercentage > threshold;\n\n if (changed) {\n logger.warn(\n `${capture.route}: ${diffPercentage.toFixed(2)}% changed (${diffPixels} pixels)`\n );\n } else {\n logger.success(`${capture.route}: no visual changes`);\n }\n\n results.push({\n route: capture.route,\n beforePath: capture.beforePath,\n afterPath: capture.afterPath,\n diffPath,\n sideBySidePath,\n sliderPath,\n diffPixels,\n totalPixels,\n diffPercentage,\n changed,\n });\n }\n\n return results;\n}\n","import { writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { execSync } from \"child_process\";\nimport { logger } from \"../logger.js\";\nimport { ensureDir } from \"../utils/fs.js\";\nimport { generateReportHtml } from \"../templates/report.html.js\";\nimport { generateSliderHtml } from \"../templates/slider.html.js\";\nimport { generateSummaryMd } from \"../templates/summary.md.js\";\nimport type { CompareResult } from \"../types.js\";\n\nconst COMMENT_MARKER = \"<!-- afterbefore -->\";\n\nfunction findPrNumber(): string | null {\n try {\n const output = execSync(\"gh pr view --json number -q .number\", {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }).trim();\n return output || null;\n } catch {\n return null;\n }\n}\n\nfunction findExistingCommentId(prNumber: string): string | null {\n try {\n const output = execSync(\n `gh api repos/{owner}/{repo}/issues/${prNumber}/comments --jq '.[] | select(.body | contains(\"${COMMENT_MARKER}\")) | .id'`,\n { encoding: \"utf-8\", stdio: [\"pipe\", \"pipe\", \"pipe\"] }\n ).trim();\n const ids = output.split(\"\\n\").filter(Boolean);\n return ids[0] || null;\n } catch {\n return null;\n }\n}\n\nfunction postOrUpdateComment(prNumber: string, body: string): void {\n const existingId = findExistingCommentId(prNumber);\n\n if (existingId) {\n logger.info(`Updating existing PR comment (id: ${existingId})`);\n execSync(\n `gh api repos/{owner}/{repo}/issues/comments/${existingId} -X PATCH -f body=@-`,\n {\n input: body,\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }\n );\n } else {\n logger.info(`Creating new PR comment`);\n execSync(\n `gh api repos/{owner}/{repo}/issues/${prNumber}/comments -f body=@-`,\n {\n input: body,\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }\n );\n }\n}\n\nexport async function generateReport(\n results: CompareResult[],\n outputDir: string,\n options: { post: boolean }\n): Promise<void> {\n await ensureDir(outputDir);\n\n // Write summary.md\n const summaryMd = generateSummaryMd(results);\n const summaryPath = join(outputDir, \"summary.md\");\n writeFileSync(summaryPath, summaryMd, \"utf-8\");\n logger.success(`Written summary to ${summaryPath}`);\n\n // Write index.html\n const reportHtml = generateReportHtml(results);\n const indexPath = join(outputDir, \"index.html\");\n writeFileSync(indexPath, reportHtml, \"utf-8\");\n logger.success(`Written report to ${indexPath}`);\n\n // Write per-route slider.html files\n for (const result of results) {\n if (!result.changed) continue;\n const sliderHtml = generateSliderHtml(result);\n writeFileSync(result.sliderPath, sliderHtml, \"utf-8\");\n logger.dim(`Written slider for ${result.route}`);\n }\n\n // Post to PR if requested\n if (options.post) {\n const prNumber = findPrNumber();\n if (!prNumber) {\n logger.warn(\n \"Could not find PR number. Make sure you are on a branch with an open PR and `gh` is authenticated.\"\n );\n return;\n }\n\n postOrUpdateComment(prNumber, summaryMd);\n logger.success(`Posted results to PR #${prNumber}`);\n }\n}\n","import { readFileSync } from \"fs\";\nimport type { CompareResult } from \"../types.js\";\n\nfunction toBase64(filePath: string): string {\n return readFileSync(filePath).toString(\"base64\");\n}\n\nfunction imgSrc(filePath: string): string {\n return `data:image/png;base64,${toBase64(filePath)}`;\n}\n\nexport function generateReportHtml(results: CompareResult[]): string {\n const changed = results.filter((r) => r.changed);\n const unchanged = results.filter((r) => !r.changed);\n\n const card = (r: CompareResult) => `\n <div class=\"card ${r.changed ? \"changed\" : \"unchanged\"}\">\n <div class=\"card-header\">\n <span class=\"route\">${r.route}</span>\n <span class=\"badge ${r.changed ? \"badge-changed\" : \"badge-unchanged\"}\">\n ${r.changed ? `${r.diffPercentage.toFixed(2)}% changed` : \"No change\"}\n </span>\n </div>\n <div class=\"images\">\n <div class=\"img-col\">\n <div class=\"label\">Before</div>\n <img src=\"${imgSrc(r.beforePath)}\" alt=\"Before\" />\n </div>\n <div class=\"img-col\">\n <div class=\"label\">After</div>\n <img src=\"${imgSrc(r.afterPath)}\" alt=\"After\" />\n </div>\n <div class=\"img-col\">\n <div class=\"label\">Diff</div>\n <img src=\"${imgSrc(r.diffPath)}\" alt=\"Diff\" />\n </div>\n </div>\n ${r.changed ? `<a class=\"slider-link\" href=\"${r.sliderPath}\">Open slider view</a>` : \"\"}\n </div>`;\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>afterbefore Report</title>\n<style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f9fafb; color: #111827; padding: 24px; }\n h1 { font-size: 24px; margin-bottom: 8px; }\n .summary { color: #6b7280; margin-bottom: 24px; font-size: 14px; }\n .grid { display: grid; grid-template-columns: 1fr; gap: 24px; }\n .card { background: #fff; border-radius: 8px; border: 1px solid #e5e7eb; overflow: hidden; }\n .card.changed { border-color: #fbbf24; }\n .card-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid #e5e7eb; }\n .route { font-weight: 600; font-size: 16px; font-family: monospace; }\n .badge { font-size: 12px; padding: 2px 8px; border-radius: 9999px; font-weight: 500; }\n .badge-changed { background: #fef3c7; color: #92400e; }\n .badge-unchanged { background: #d1fae5; color: #065f46; }\n .images { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px; background: #e5e7eb; }\n .img-col { background: #fff; padding: 8px; }\n .img-col img { width: 100%; height: auto; display: block; border-radius: 4px; }\n .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; margin-bottom: 4px; font-weight: 600; }\n .slider-link { display: block; padding: 8px 16px; text-align: center; font-size: 13px; color: #2563eb; text-decoration: none; border-top: 1px solid #e5e7eb; }\n .slider-link:hover { background: #eff6ff; }\n .section-title { font-size: 18px; font-weight: 600; margin: 24px 0 12px; }\n</style>\n</head>\n<body>\n<h1>afterbefore Report</h1>\n<p class=\"summary\">${results.length} route(s) captured, ${changed.length} with visual changes.</p>\n\n${changed.length > 0 ? `<h2 class=\"section-title\">Changed (${changed.length})</h2><div class=\"grid\">${changed.map(card).join(\"\")}</div>` : \"\"}\n${unchanged.length > 0 ? `<h2 class=\"section-title\">Unchanged (${unchanged.length})</h2><div class=\"grid\">${unchanged.map(card).join(\"\")}</div>` : \"\"}\n\n</body>\n</html>`;\n}\n","import { readFileSync } from \"fs\";\nimport type { CompareResult } from \"../types.js\";\n\nfunction toBase64(filePath: string): string {\n return readFileSync(filePath).toString(\"base64\");\n}\n\nfunction imgSrc(filePath: string): string {\n return `data:image/png;base64,${toBase64(filePath)}`;\n}\n\nexport function generateSliderHtml(result: CompareResult): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>afterbefore Slider - ${result.route}</title>\n<style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f9fafb; color: #111827; padding: 24px; }\n h1 { font-size: 20px; margin-bottom: 4px; }\n .meta { color: #6b7280; font-size: 13px; margin-bottom: 16px; }\n .container { position: relative; overflow: hidden; border-radius: 8px; border: 1px solid #e5e7eb; user-select: none; cursor: ew-resize; }\n .container img { display: block; width: 100%; height: auto; }\n .before-wrap { position: absolute; top: 0; left: 0; height: 100%; overflow: hidden; }\n .before-wrap img { display: block; height: 100%; width: auto; min-width: 100%; object-fit: cover; }\n .slider-line { position: absolute; top: 0; width: 3px; height: 100%; background: #2563eb; cursor: ew-resize; z-index: 10; }\n .slider-handle { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 36px; height: 36px; background: #2563eb; border-radius: 50%; border: 3px solid #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; }\n .slider-handle::before { content: '\\\\2194'; color: #fff; font-size: 18px; }\n .labels { display: flex; justify-content: space-between; margin-top: 8px; font-size: 12px; color: #6b7280; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }\n</style>\n</head>\n<body>\n<h1>${result.route}</h1>\n<p class=\"meta\">${result.diffPercentage.toFixed(2)}% changed (${result.diffPixels.toLocaleString()} pixels)</p>\n\n<div class=\"container\" id=\"slider\">\n <img src=\"${imgSrc(result.afterPath)}\" alt=\"After\" draggable=\"false\" />\n <div class=\"before-wrap\" id=\"beforeWrap\">\n <img src=\"${imgSrc(result.beforePath)}\" alt=\"Before\" draggable=\"false\" />\n </div>\n <div class=\"slider-line\" id=\"sliderLine\">\n <div class=\"slider-handle\"></div>\n </div>\n</div>\n<div class=\"labels\"><span>Before</span><span>After</span></div>\n\n<script>\n(function() {\n const container = document.getElementById('slider');\n const beforeWrap = document.getElementById('beforeWrap');\n const sliderLine = document.getElementById('sliderLine');\n let dragging = false;\n\n function setPosition(x) {\n const rect = container.getBoundingClientRect();\n let pct = ((x - rect.left) / rect.width) * 100;\n pct = Math.max(0, Math.min(100, pct));\n beforeWrap.style.width = pct + '%';\n sliderLine.style.left = pct + '%';\n }\n\n // Start at 50%\n beforeWrap.style.width = '50%';\n sliderLine.style.left = '50%';\n\n container.addEventListener('mousedown', function(e) { dragging = true; setPosition(e.clientX); });\n window.addEventListener('mousemove', function(e) { if (dragging) setPosition(e.clientX); });\n window.addEventListener('mouseup', function() { dragging = false; });\n\n container.addEventListener('touchstart', function(e) { dragging = true; setPosition(e.touches[0].clientX); }, { passive: true });\n window.addEventListener('touchmove', function(e) { if (dragging) setPosition(e.touches[0].clientX); }, { passive: true });\n window.addEventListener('touchend', function() { dragging = false; });\n})();\n</script>\n</body>\n</html>`;\n}\n","import type { CompareResult } from \"../types.js\";\n\nexport function generateSummaryMd(results: CompareResult[]): string {\n const changed = results.filter((r) => r.changed);\n const lines: string[] = [];\n\n lines.push(\"<!-- afterbefore -->\");\n lines.push(\"\");\n lines.push(\"## afterbefore Report\");\n lines.push(\"\");\n\n if (changed.length === 0) {\n lines.push(\"No visual changes detected.\");\n lines.push(\"\");\n lines.push(`${results.length} route(s) captured, all unchanged.`);\n return lines.join(\"\\n\");\n }\n\n lines.push(\n `${results.length} route(s) captured, **${changed.length}** with visual changes.`\n );\n lines.push(\"\");\n lines.push(\"| Route | Diff % | Status |\");\n lines.push(\"|-------|--------|--------|\");\n\n for (const r of results) {\n const status = r.changed ? \"Changed\" : \"Unchanged\";\n const pct = r.changed ? `${r.diffPercentage.toFixed(2)}%` : \"0%\";\n lines.push(`| \\`${r.route}\\` | ${pct} | ${status} |`);\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":["resolve","resolve","readFileSync","join","join","readFileSync","resolve","execSync","join","existsSync","join","join","execSync","resolve","join","join","readFileSync","join","dirname","readFileSync","dirname","join","writeFileSync","join","execSync","readFileSync","readFileSync","toBase64","imgSrc","execSync","join","writeFileSync","resolve"]}
@@ -0,0 +1,71 @@
1
+ import * as child_process from 'child_process';
2
+
3
+ type DiffStatus = "A" | "M" | "D" | "R" | "C";
4
+ interface DiffFile {
5
+ status: DiffStatus;
6
+ path: string;
7
+ oldPath?: string;
8
+ }
9
+ type FileCategory = "page" | "component" | "style" | "layout" | "utility" | "config" | "test" | "other";
10
+ interface ClassifiedFile extends DiffFile {
11
+ category: FileCategory;
12
+ }
13
+ interface ImportGraph {
14
+ /** file → set of files it imports */
15
+ forward: Map<string, Set<string>>;
16
+ /** file → set of files that import it */
17
+ reverse: Map<string, Set<string>>;
18
+ }
19
+ interface AffectedRoute {
20
+ /** File system path to the page file (e.g. app/about/page.tsx) */
21
+ pagePath: string;
22
+ /** URL route (e.g. /about) */
23
+ route: string;
24
+ /** How the page was affected — direct change or via dependency chain */
25
+ reason: "direct" | "transitive";
26
+ /** Depth in import graph (0 = direct change to page file) */
27
+ depth: number;
28
+ }
29
+ interface CaptureResult {
30
+ route: string;
31
+ beforePath: string;
32
+ afterPath: string;
33
+ }
34
+ interface CompareResult {
35
+ route: string;
36
+ beforePath: string;
37
+ afterPath: string;
38
+ diffPath: string;
39
+ sideBySidePath: string;
40
+ sliderPath: string;
41
+ diffPixels: number;
42
+ totalPixels: number;
43
+ diffPercentage: number;
44
+ changed: boolean;
45
+ }
46
+ interface PipelineOptions {
47
+ /** Base branch/ref to compare against (default: "main") */
48
+ base: string;
49
+ /** Output directory (default: ".afterbefore") */
50
+ output: string;
51
+ /** Post results as PR comment */
52
+ post: boolean;
53
+ /** Working directory (default: process.cwd()) */
54
+ cwd: string;
55
+ /** Diff threshold percentage below which changes are considered negligible (default: 0.1) */
56
+ threshold: number;
57
+ }
58
+ interface ServerInfo {
59
+ port: number;
60
+ process: child_process.ChildProcess;
61
+ url: string;
62
+ }
63
+ interface WorktreeInfo {
64
+ path: string;
65
+ ref: string;
66
+ cleanup: () => Promise<void>;
67
+ }
68
+
69
+ declare function runPipeline(options: PipelineOptions): Promise<void>;
70
+
71
+ export { type AffectedRoute, type CaptureResult, type ClassifiedFile, type CompareResult, type DiffFile, type DiffStatus, type FileCategory, type ImportGraph, type PipelineOptions, type ServerInfo, type WorktreeInfo, runPipeline };