formatting

This commit is contained in:
Stefan Stefanov 2026-06-05 21:32:33 +03:00
parent 249c88da83
commit d8918f8b36
2 changed files with 225 additions and 168 deletions

172
server.js
View file

@ -1,35 +1,44 @@
import express from 'express'; import express from "express";
import { execSync, spawn } from 'child_process'; import { execSync, spawn } from "child_process";
import { readFileSync, writeFileSync } from 'fs'; import { readFileSync, writeFileSync } from "fs";
import { join, dirname } from 'path'; import { join, dirname } from "path";
import { tmpdir } from 'os'; import { tmpdir } from "os";
import { fileURLToPath } from 'url'; import { fileURLToPath } from "url";
import { parsePatchFiles, DEFAULT_THEMES } from '@pierre/diffs'; import { parsePatchFiles, DEFAULT_THEMES } from "@pierre/diffs";
import { preloadFileDiff } from '@pierre/diffs/ssr'; import { preloadFileDiff } from "@pierre/diffs/ssr";
const __dirname = typeof __filename !== 'undefined' const __dirname =
typeof __filename !== "undefined"
? dirname(__filename) ? dirname(__filename)
: dirname(fileURLToPath(import.meta.url)); : dirname(fileURLToPath(import.meta.url));
// --- arg parsing --- // --- arg parsing ---
const outIdx = process.argv.indexOf('--out'); const outIdx = process.argv.indexOf("--out");
const outFile = outIdx !== -1 ? process.argv[outIdx + 1] : null; const outFile = outIdx !== -1 ? process.argv[outIdx + 1] : null;
const isStatic = process.argv.includes('--static'); const isStatic = process.argv.includes("--static");
const positional = []; const positional = [];
let skipNext = false; let skipNext = false;
for (const arg of process.argv.slice(2)) { for (const arg of process.argv.slice(2)) {
if (skipNext) { skipNext = false; continue; } if (skipNext) {
if (arg === '--static') continue; skipNext = false;
if (arg === '--out') { skipNext = true; continue; } continue;
}
if (arg === "--static") continue;
if (arg === "--out") {
skipNext = true;
continue;
}
positional.push(arg); positional.push(arg);
} }
const [projectDir, mainBranch, featureBranch, portArg] = positional; const [projectDir, mainBranch, featureBranch, portArg] = positional;
const port = parseInt(portArg, 10) || 3000; const port = parseInt(portArg, 10) || 3000;
if (!projectDir || !mainBranch || !featureBranch) { if (!projectDir || !mainBranch || !featureBranch) {
console.error('Usage: difftool.sh [--static] [--out <file>] <git-project-dir> <main branch> <feature branch> [port]'); console.error(
"Usage: difftool.sh [--static] [--out <file>] <git-project-dir> <main branch> <feature branch> [port]",
);
process.exit(1); process.exit(1);
} }
@ -37,30 +46,35 @@ if (!projectDir || !mainBranch || !featureBranch) {
function run(cmd) { function run(cmd) {
try { try {
return execSync(cmd, { encoding: 'utf-8', cwd: projectDir }).trim(); return execSync(cmd, {
encoding: "utf-8",
cwd: projectDir,
}).trim();
} catch { } catch {
return ''; return "";
} }
} }
const branchDiff = run(`git diff ${mainBranch} ${featureBranch} --no-color`); const branchDiff = run(`git diff ${mainBranch} ${featureBranch} --no-color`);
const stagedDiff = run('git diff --cached --no-color'); const stagedDiff = run("git diff --cached --no-color");
const unstagedDiff = run('git diff --no-color'); const unstagedDiff = run("git diff --no-color");
const untrackedFiles = run('git ls-files --others --exclude-standard') const untrackedFiles = run("git ls-files --others --exclude-standard")
.split('\n') .split("\n")
.filter(Boolean); .filter(Boolean);
let untrackedPatch = ''; let untrackedPatch = "";
for (const file of untrackedFiles) { for (const file of untrackedFiles) {
let content; let content;
try { try {
content = readFileSync(join(projectDir, file), 'utf-8'); content = readFileSync(join(projectDir, file), "utf-8");
} catch { } catch {
continue; continue;
} }
const lines = content.split('\n'); const lines = content.split("\n");
const lineCount = content.endsWith('\n') ? lines.length - 1 : lines.length; const lineCount = content.endsWith("\n")
? lines.length - 1
: lines.length;
untrackedPatch += `diff --git a/${file} b/${file}\n`; untrackedPatch += `diff --git a/${file} b/${file}\n`;
untrackedPatch += `new file mode 100644\n`; untrackedPatch += `new file mode 100644\n`;
untrackedPatch += `--- /dev/null\n`; untrackedPatch += `--- /dev/null\n`;
@ -73,18 +87,18 @@ for (const file of untrackedFiles) {
const patch = [branchDiff, stagedDiff, unstagedDiff, untrackedPatch] const patch = [branchDiff, stagedDiff, unstagedDiff, untrackedPatch]
.filter(Boolean) .filter(Boolean)
.join('\n'); .join("\n");
if (!patch.trim()) { if (!patch.trim()) {
console.log('No differences found'); console.log("No differences found");
process.exit(0); process.exit(0);
} }
const parsedEntries = parsePatchFiles(patch); const parsedEntries = parsePatchFiles(patch);
const files = parsedEntries.flatMap(e => e.files ?? []); const files = parsedEntries.flatMap((e) => e.files ?? []);
if (files.length === 0) { if (files.length === 0) {
console.log('No file diffs found in patch'); console.log("No file diffs found in patch");
process.exit(0); process.exit(0);
} }
@ -93,23 +107,28 @@ if (files.length === 0) {
(async () => { (async () => {
const ssrOptions = { const ssrOptions = {
theme: DEFAULT_THEMES, theme: DEFAULT_THEMES,
themeType: 'system', themeType: "system",
diffStyle: 'unified', diffStyle: "unified",
useTokenTransformer: true, useTokenTransformer: true,
}; };
const renderedFiles = []; const renderedFiles = [];
for (const fileDiff of files) { for (const fileDiff of files) {
try { try {
const result = await preloadFileDiff({ fileDiff, options: ssrOptions }); const result = await preloadFileDiff({
fileDiff,
options: ssrOptions,
});
renderedFiles.push({ renderedFiles.push({
name: fileDiff.name || 'unknown', name: fileDiff.name || "unknown",
prerenderedHTML: result.prerenderedHTML, prerenderedHTML: result.prerenderedHTML,
}); });
} catch (err) { } catch (err) {
console.warn(`Failed to SSR-render ${fileDiff.name || 'unknown'}: ${err.message}`); console.warn(
`Failed to SSR-render ${fileDiff.name || "unknown"}: ${err.message}`,
);
renderedFiles.push({ renderedFiles.push({
name: fileDiff.name || 'unknown', name: fileDiff.name || "unknown",
prerenderedHTML: null, prerenderedHTML: null,
}); });
} }
@ -118,17 +137,22 @@ if (files.length === 0) {
const finalPatch = patch; const finalPatch = patch;
function escapeHtml(str) { function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
} }
function fileId(name) { function fileId(name) {
return 'DF-' + name.replace(/[^a-zA-Z0-9_-]/g, '_'); return "DF-" + name.replace(/[^a-zA-Z0-9_-]/g, "_");
} }
function pageHtml(opts) { function pageHtml(opts) {
const { mode, sidebarHtml, extraScript } = opts; const { mode, sidebarHtml, extraScript } = opts;
const fileSections = renderedFiles.map((f) => { const fileSections = renderedFiles
.map((f) => {
const escapedName = escapeHtml(f.name); const escapedName = escapeHtml(f.name);
const id = fileId(f.name); const id = fileId(f.name);
if (f.prerenderedHTML !== null) { if (f.prerenderedHTML !== null) {
@ -145,16 +169,18 @@ if (files.length === 0) {
<div class="diff-file-header">${escapedName}</div> <div class="diff-file-header">${escapedName}</div>
<div class="diff-fallback"><a href="/patch">View raw patch</a></div> <div class="diff-fallback"><a href="/patch">View raw patch</a></div>
</div>`; </div>`;
}).join('\n'); })
.join("\n");
const hasLocal = stagedDiff || unstagedDiff || untrackedPatch; const hasLocal = stagedDiff || unstagedDiff || untrackedPatch;
const subtitle = hasLocal const subtitle = hasLocal
? `${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)} (includes local changes)` ? `${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)} (includes local changes)`
: `${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)}`; : `${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)}`;
const title = `Diff: ${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)}`; const title = `Diff: ${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)}`;
const rawLink = mode === 'server' const rawLink =
mode === "server"
? '<a class="raw-link" href="/patch">View raw patch \u2192</a>' ? '<a class="raw-link" href="/patch">View raw patch \u2192</a>'
: ''; : "";
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -269,7 +295,7 @@ ${extraScript}
const sidebarHtml = ` const sidebarHtml = `
<div class="sidebar"> <div class="sidebar">
<div class="file-list"> <div class="file-list">
${renderedFiles.map(f => ` <a class="file-item" href="#${fileId(f.name)}">${escapeHtml(f.name)}</a>`).join('\n')} ${renderedFiles.map((f) => ` <a class="file-item" href="#${fileId(f.name)}">${escapeHtml(f.name)}</a>`).join("\n")}
</div> </div>
</div>`; </div>`;
@ -292,18 +318,24 @@ ${renderedFiles.map(f => ` <a class="file-item" href="#${fileId(f.name)
}); });
</script>`; </script>`;
return pageHtml({ mode: 'static', sidebarHtml, extraScript }) return pageHtml({
.replace('</body>', ` <details style="padding:0 24px 16px"> mode: "static",
sidebarHtml,
extraScript,
}).replace(
"</body>",
` <details style="padding:0 24px 16px">
<summary id="patch-toggle" style="cursor:pointer;font-size:0.8125rem;color:#2563eb">View raw patch</summary> <summary id="patch-toggle" style="cursor:pointer;font-size:0.8125rem;color:#2563eb">View raw patch</summary>
<pre id="patch-content" class="raw-patch">${escapedPatch}</pre> <pre id="patch-content" class="raw-patch">${escapedPatch}</pre>
</details>\n</body>`); </details>\n</body>`,
);
} }
// --- mode dispatch --- // --- mode dispatch ---
if (outFile) { if (outFile) {
const html = buildStaticHtml(); const html = buildStaticHtml();
writeFileSync(outFile, html, 'utf-8'); writeFileSync(outFile, html, "utf-8");
console.log(`Written to ${outFile}`); console.log(`Written to ${outFile}`);
process.exit(0); process.exit(0);
} }
@ -311,42 +343,58 @@ ${renderedFiles.map(f => ` <a class="file-item" href="#${fileId(f.name)
if (isStatic) { if (isStatic) {
const html = buildStaticHtml(); const html = buildStaticHtml();
const tmpFile = join(tmpdir(), `difftool-${Date.now()}.html`); const tmpFile = join(tmpdir(), `difftool-${Date.now()}.html`);
writeFileSync(tmpFile, html, 'utf-8'); writeFileSync(tmpFile, html, "utf-8");
const openCmd = process.platform === 'darwin' ? 'open' const openCmd =
: process.platform === 'win32' ? 'start' process.platform === "darwin"
: 'xdg-open'; ? "open"
: process.platform === "win32"
? "start"
: "xdg-open";
console.log(`Opening ${tmpFile}`); console.log(`Opening ${tmpFile}`);
try { try {
spawn(openCmd, [tmpFile], { detached: true, stdio: 'ignore' }).unref(); spawn(openCmd, [tmpFile], {
detached: true,
stdio: "ignore",
}).unref();
} catch { } catch {
console.log(`HTML file saved to ${tmpFile}`); console.log(`HTML file saved to ${tmpFile}`);
} }
} else { } else {
const app = express(); const app = express();
app.use(express.static(join(__dirname, 'public'))); app.use(express.static(join(__dirname, "public")));
app.get('/patch', (req, res) => { app.get("/patch", (req, res) => {
res.type('text/plain').send(finalPatch); res.type("text/plain").send(finalPatch);
}); });
app.get('/', (req, res) => { app.get("/", (req, res) => {
const fileDataJson = JSON.stringify(renderedFiles.map(f => ({ name: f.name }))) const fileDataJson = JSON.stringify(
.replace(/<\/script>/gi, '<\\/script>'); renderedFiles.map((f) => ({ name: f.name })),
).replace(/<\/script>/gi, "<\\/script>");
const sidebarHtml = '<div class="sidebar"><div id="file-tree"></div></div>'; const sidebarHtml =
'<div class="sidebar"><div id="file-tree"></div></div>';
const extraScript = ` const extraScript = `
<script id="diff-files-data" type="application/json">${fileDataJson}</script> <script id="diff-files-data" type="application/json">${fileDataJson}</script>
<script type="module" src="/client.js"></script>`; <script type="module" src="/client.js"></script>`;
const html = pageHtml({ mode: 'server', sidebarHtml, extraScript }); const html = pageHtml({
res.type('html').send(html); mode: "server",
sidebarHtml,
extraScript,
});
res.type("html").send(html);
}); });
app.listen(port, () => { app.listen(port, () => {
console.log(`Difftool running at http://localhost:${port}`); console.log(
console.log(`Patch endpoint: http://localhost:${port}/patch`); `Difftool running at http://localhost:${port}`,
);
console.log(
`Patch endpoint: http://localhost:${port}/patch`,
);
}); });
} }
})(); })();

View file

@ -1,23 +1,32 @@
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 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'); const mount = document.getElementById("file-tree");
mount.style.height = (window.innerHeight - mount.getBoundingClientRect().top) + 'px'; mount.style.height =
window.innerHeight - mount.getBoundingClientRect().top + "px";
const tree = new FileTree({ const tree = new FileTree({
paths, paths,
initialExpansion: 'open', initialExpansion: "open",
onSelectionChange: (selectedPaths) => { onSelectionChange: (selectedPaths) => {
if (selectedPaths.length !== 1) return; if (selectedPaths.length !== 1) return;
const id = 'DF-' + selectedPaths[0].replace(/[^a-zA-Z0-9_-]/g, '_'); const id =
"DF-" +
selectedPaths[0].replace(/[^a-zA-Z0-9_-]/g, "_");
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) { if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' }); el.scrollIntoView({
el.classList.add('diff-highlight'); behavior: "smooth",
setTimeout(() => el.classList.remove('diff-highlight'), 1500); block: "start",
});
el.classList.add("diff-highlight");
setTimeout(
() => el.classList.remove("diff-highlight"),
1500,
);
} }
}, },
}); });