diff --git a/server.js b/server.js index 098d499..7159b99 100644 --- a/server.js +++ b/server.js @@ -1,162 +1,188 @@ -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'; +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)); +const __dirname = + typeof __filename !== "undefined" + ? dirname(__filename) + : dirname(fileURLToPath(import.meta.url)); // --- arg parsing --- -const outIdx = process.argv.indexOf('--out'); +const outIdx = process.argv.indexOf("--out"); const outFile = outIdx !== -1 ? process.argv[outIdx + 1] : null; -const isStatic = process.argv.includes('--static'); +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); + 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); + 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 ''; - } + 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 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); +const untrackedFiles = run("git ls-files --others --exclude-standard") + .split("\n") + .filter(Boolean); -let untrackedPatch = ''; +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`; - } + 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'); + .filter(Boolean) + .join("\n"); if (!patch.trim()) { - console.log('No differences found'); - process.exit(0); + console.log("No differences found"); + process.exit(0); } const parsedEntries = parsePatchFiles(patch); -const files = parsedEntries.flatMap(e => e.files ?? []); +const files = parsedEntries.flatMap((e) => e.files ?? []); if (files.length === 0) { - console.log('No file diffs found in patch'); - process.exit(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 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 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; + const finalPatch = patch; - function escapeHtml(str) { - return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - } + function escapeHtml(str) { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + } - function fileId(name) { - return 'DF-' + name.replace(/[^a-zA-Z0-9_-]/g, '_'); - } + function fileId(name) { + return "DF-" + name.replace(/[^a-zA-Z0-9_-]/g, "_"); + } - function pageHtml(opts) { - const { mode, sidebarHtml, extraScript } = opts; + 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 ` + const fileSections = renderedFiles + .map((f) => { + const escapedName = escapeHtml(f.name); + const id = fileId(f.name); + if (f.prerenderedHTML !== null) { + return `
${escapedName}
`; - } - return ` + } + return `
${escapedName}
`; - }).join('\n'); + }) + .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' - : ''; + 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 ` + return ` @@ -263,19 +289,19 @@ if (files.length === 0) { ${extraScript} `; - } + } - function buildStaticHtml() { - const sidebarHtml = ` + function buildStaticHtml() { + const sidebarHtml = ` `; - const escapedPatch = escapeHtml(finalPatch); + const escapedPatch = escapeHtml(finalPatch); - const extraScript = ` `; - return pageHtml({ mode: 'static', sidebarHtml, extraScript }) - .replace('', `
+ return pageHtml({ + mode: "static", + sidebarHtml, + extraScript, + }).replace( + "", + `
View raw patch
${escapedPatch}
-
\n`); - } +
\n`, + ); + } - // --- mode dispatch --- + // --- mode dispatch --- - if (outFile) { - const html = buildStaticHtml(); - writeFileSync(outFile, html, 'utf-8'); - console.log(`Written to ${outFile}`); - process.exit(0); - } + 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'); + 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'))); + 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("/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>'); + app.get("/", (req, res) => { + const fileDataJson = JSON.stringify( + renderedFiles.map((f) => ({ name: f.name })), + ).replace(/<\/script>/gi, "<\\/script>"); - const sidebarHtml = ''; + const sidebarHtml = + ''; - const extraScript = ` + const extraScript = ` `; - const html = pageHtml({ mode: 'server', sidebarHtml, extraScript }); - res.type('html').send(html); - }); + 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`); - }); - } + app.listen(port, () => { + console.log( + `Difftool running at http://localhost:${port}`, + ); + console.log( + `Patch endpoint: http://localhost:${port}/patch`, + ); + }); + } })(); diff --git a/src/client.js b/src/client.js index a1ab9e9..23334a4 100644 --- a/src/client.js +++ b/src/client.js @@ -1,25 +1,34 @@ -import { FileTree } from '@pierre/trees'; +import { FileTree } from "@pierre/trees"; -const dataEl = document.getElementById('diff-files-data'); +const dataEl = document.getElementById("diff-files-data"); const files = JSON.parse(dataEl.textContent); -const paths = files.map(f => f.name); +const paths = files.map((f) => f.name); -const mount = document.getElementById('file-tree'); -mount.style.height = (window.innerHeight - mount.getBoundingClientRect().top) + 'px'; +const mount = document.getElementById("file-tree"); +mount.style.height = + window.innerHeight - mount.getBoundingClientRect().top + "px"; const tree = new FileTree({ - paths, - initialExpansion: 'open', - onSelectionChange: (selectedPaths) => { - if (selectedPaths.length !== 1) return; - const id = 'DF-' + selectedPaths[0].replace(/[^a-zA-Z0-9_-]/g, '_'); - const el = document.getElementById(id); - if (el) { - el.scrollIntoView({ behavior: 'smooth', block: 'start' }); - el.classList.add('diff-highlight'); - setTimeout(() => el.classList.remove('diff-highlight'), 1500); - } - }, + paths, + initialExpansion: "open", + onSelectionChange: (selectedPaths) => { + if (selectedPaths.length !== 1) return; + const id = + "DF-" + + selectedPaths[0].replace(/[^a-zA-Z0-9_-]/g, "_"); + const el = document.getElementById(id); + if (el) { + el.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + el.classList.add("diff-highlight"); + setTimeout( + () => el.classList.remove("diff-highlight"), + 1500, + ); + } + }, }); tree.render({ fileTreeContainer: mount });