import express from "express"; import { execSync, spawn } from "child_process"; import { readFileSync, writeFileSync } from "fs"; import { join, dirname } from "path"; import { tmpdir } from "os"; import { fileURLToPath } from "url"; import { parsePatchFiles, DEFAULT_THEMES } from "@pierre/diffs"; import { preloadFileDiff } from "@pierre/diffs/ssr"; const __dirname = typeof __filename !== "undefined" ? dirname(__filename) : dirname(fileURLToPath(import.meta.url)); // --- arg parsing --- const outIdx = process.argv.indexOf("--out"); const outFile = outIdx !== -1 ? process.argv[outIdx + 1] : null; const isStatic = process.argv.includes("--static"); const positional = []; let skipNext = false; for (const arg of process.argv.slice(2)) { if (skipNext) { skipNext = false; continue; } if (arg === "--static") continue; if (arg === "--out") { skipNext = true; continue; } positional.push(arg); } const [projectDir, mainBranch, featureBranch, portArg] = positional; const port = parseInt(portArg, 10) || 3000; if (!projectDir || !mainBranch || !featureBranch) { console.error( "Usage: difftool.sh [--static] [--out ]
[port]", ); process.exit(1); } // --- git --- function run(cmd) { try { return execSync(cmd, { encoding: "utf-8", cwd: projectDir, }).trim(); } catch { return ""; } } const branchDiff = run(`git diff ${mainBranch} ${featureBranch} --no-color`); const stagedDiff = run("git diff --cached --no-color"); const unstagedDiff = run("git diff --no-color"); const untrackedFiles = run("git ls-files --others --exclude-standard") .split("\n") .filter(Boolean); let untrackedPatch = ""; for (const file of untrackedFiles) { let content; try { content = readFileSync(join(projectDir, file), "utf-8"); } catch { continue; } const lines = content.split("\n"); const lineCount = content.endsWith("\n") ? lines.length - 1 : lines.length; untrackedPatch += `diff --git a/${file} b/${file}\n`; untrackedPatch += `new file mode 100644\n`; untrackedPatch += `--- /dev/null\n`; untrackedPatch += `+++ b/${file}\n`; untrackedPatch += `@@ -0,0 +1,${lineCount} @@\n`; for (let i = 0; i < lineCount; i++) { untrackedPatch += `+${lines[i]}\n`; } } const patch = [branchDiff, stagedDiff, unstagedDiff, untrackedPatch] .filter(Boolean) .join("\n"); if (!patch.trim()) { console.log("No differences found"); process.exit(0); } const parsedEntries = parsePatchFiles(patch); const files = parsedEntries.flatMap((e) => e.files ?? []); if (files.length === 0) { console.log("No file diffs found in patch"); process.exit(0); } // --- SSR diffs + helpers + mode dispatch --- (async () => { const ssrOptions = { theme: DEFAULT_THEMES, themeType: "system", diffStyle: "unified", useTokenTransformer: true, }; const renderedFiles = []; for (const fileDiff of files) { try { const result = await preloadFileDiff({ fileDiff, options: ssrOptions, }); renderedFiles.push({ name: fileDiff.name || "unknown", prerenderedHTML: result.prerenderedHTML, }); } catch (err) { console.warn( `Failed to SSR-render ${fileDiff.name || "unknown"}: ${err.message}`, ); renderedFiles.push({ name: fileDiff.name || "unknown", prerenderedHTML: null, }); } } const finalPatch = patch; function escapeHtml(str) { return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function fileId(name) { return "DF-" + name.replace(/[^a-zA-Z0-9_-]/g, "_"); } function pageHtml(opts) { const { mode, sidebarHtml, extraScript } = opts; const fileSections = renderedFiles .map((f) => { const escapedName = escapeHtml(f.name); const id = fileId(f.name); if (f.prerenderedHTML !== null) { return `
${escapedName}
`; } return `
${escapedName}
`; }) .join("\n"); const hasLocal = stagedDiff || unstagedDiff || untrackedPatch; const subtitle = hasLocal ? `${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)} (includes local changes)` : `${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)}`; const title = `Diff: ${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)}`; const rawLink = mode === "server" ? 'View raw patch \u2192' : ""; return ` ${title}

Diff

${subtitle} ${rawLink}
${sidebarHtml}
${fileSections}
${extraScript} `; } function buildStaticHtml() { const sidebarHtml = ` `; const escapedPatch = escapeHtml(finalPatch); const extraScript = ` `; return pageHtml({ mode: "static", sidebarHtml, extraScript, }).replace( "", `
View raw patch
${escapedPatch}
\n`, ); } // --- mode dispatch --- if (outFile) { const html = buildStaticHtml(); writeFileSync(outFile, html, "utf-8"); console.log(`Written to ${outFile}`); process.exit(0); } if (isStatic) { const html = buildStaticHtml(); const tmpFile = join(tmpdir(), `difftool-${Date.now()}.html`); writeFileSync(tmpFile, html, "utf-8"); const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; console.log(`Opening ${tmpFile}`); try { spawn(openCmd, [tmpFile], { detached: true, stdio: "ignore", }).unref(); } catch { console.log(`HTML file saved to ${tmpFile}`); } } else { const app = express(); app.use(express.static(join(__dirname, "public"))); app.get("/patch", (req, res) => { res.type("text/plain").send(finalPatch); }); app.get("/", (req, res) => { const fileDataJson = JSON.stringify( renderedFiles.map((f) => ({ name: f.name })), ).replace(/<\/script>/gi, "<\\/script>"); const sidebarHtml = ''; const extraScript = ` `; const html = pageHtml({ mode: "server", sidebarHtml, extraScript, }); res.type("html").send(html); }); app.listen(port, () => { console.log( `Difftool running at http://localhost:${port}`, ); console.log( `Patch endpoint: http://localhost:${port}/patch`, ); }); } })();