diff --git a/.gitignore b/.gitignore index 7427fa6..9b23667 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +dist/ node_modules/ public/client.js diff --git a/difftool.sh b/difftool.sh index ae365d8..ad058b7 100755 --- a/difftool.sh +++ b/difftool.sh @@ -32,7 +32,7 @@ if [ ${#ARGS[@]} -lt 3 ]; then fi script_dir="$(cd "$(dirname "$0")" && pwd)" -node_args=("$script_dir/server.js") +node_args=("$script_dir/dist/server.cjs") [ -n "$STATIC" ] && node_args+=("$STATIC") [ -n "$OUTFILE" ] && node_args+=("--out" "$OUTFILE") node_args+=("${ARGS[@]}") diff --git a/package-lock.json b/package-lock.json index b14b5d4..fb8a97f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,13 +7,14 @@ "": { "name": "difftool", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { - "@pierre/diffs": "^1.2.7", - "@pierre/trees": "^1.0.0-beta.4", - "express": "^5.1.0" + "@pierre/diffs": "1.2.7", + "express": "5.2.1" }, "devDependencies": { - "esbuild": "^0.28.0" + "@pierre/trees": "1.0.0-beta.4", + "esbuild": "0.28.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -489,6 +490,7 @@ "version": "1.0.0-beta.4", "resolved": "https://registry.npmjs.org/@pierre/trees/-/trees-1.0.0-beta.4.tgz", "integrity": "sha512-OfT1yk9ne8Te5+GB5zUY8yqE6B8BqjBHQJleH4lu8ltwNpoocZl4vXt1AzlEExpxI/pp+AFX5QG+lR3JjtTEag==", + "dev": true, "license": "apache-2.0", "dependencies": { "preact": "11.0.0-beta.0", @@ -1460,6 +1462,7 @@ "version": "11.0.0-beta.0", "resolved": "https://registry.npmjs.org/preact/-/preact-11.0.0-beta.0.tgz", "integrity": "sha512-IcODoASASYwJ9kxz7+MJeiJhvLriwSb4y4mHIyxdgaRZp6kPUud7xytrk/6GZw8U3y6EFJaRb5wi9SrEK+8+lg==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -1470,6 +1473,7 @@ "version": "6.6.5", "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.6.5.tgz", "integrity": "sha512-O6MHzYNIKYaiSX3bOw0gGZfEbOmlIDtDfWwN1JJdc/T3ihzRT6tGGSEWE088dWrEDGa1u7101q+6fzQnO9XCPA==", + "dev": true, "license": "MIT", "peerDependencies": { "preact": ">=10 || >= 11.0.0-0" diff --git a/package.json b/package.json index 9c73910..e1f021c 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,17 @@ "type": "module", "private": true, "dependencies": { - "@pierre/diffs": "^1.2.7", - "@pierre/trees": "^1.0.0-beta.4", - "express": "^5.1.0" + "@pierre/diffs": "1.2.7", + "express": "5.2.1" }, "scripts": { - "build": "esbuild src/client.js --bundle --outfile=public/client.js --format=esm", + "build:client": "esbuild src/client.js --bundle --outfile=public/client.js --format=esm", + "build:server": "esbuild server.js --bundle --platform=node --target=node20 --outfile=dist/server.cjs --format=cjs && mkdir -p dist/public && cp -r public/. dist/public", + "build": "npm run build:server && npm run build:client", "postinstall": "npm run build" }, "devDependencies": { - "esbuild": "^0.28.0" + "@pierre/trees": "1.0.0-beta.4", + "esbuild": "0.28.0" } } diff --git a/server.js b/server.js index ccfab08..098d499 100644 --- a/server.js +++ b/server.js @@ -7,7 +7,9 @@ import { fileURLToPath } from 'url'; import { parsePatchFiles, DEFAULT_THEMES } from '@pierre/diffs'; import { preloadFileDiff } from '@pierre/diffs/ssr'; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const __dirname = typeof __filename !== 'undefined' + ? dirname(__filename) + : dirname(fileURLToPath(import.meta.url)); // --- arg parsing --- @@ -86,76 +88,75 @@ if (files.length === 0) { process.exit(0); } -// --- SSR diffs --- +// --- SSR diffs + helpers + mode dispatch --- -const ssrOptions = { - theme: DEFAULT_THEMES, - themeType: 'system', - diffStyle: 'unified', - useTokenTransformer: true, -}; +(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 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, + }); + } } -} -// --- helpers --- + 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, '_'); + } -// --- HTML template --- + 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}
View raw patch
`; - }).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 ` @@ -262,21 +263,19 @@ function pageHtml(opts) { ${extraScript} `; -} + } -// --- static HTML generation (shared by --static and --out) --- - -function buildStaticHtml() { - const sidebarHtml = ` + function buildStaticHtml() { + const sidebarHtml = ` `; - const escapedPatch = escapeHtml(patch); + 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`); -} - -// --- mode dispatch --- - -if (outFile) { - // -- out mode: write HTML to specified path, exit -- - const html = buildStaticHtml(); - writeFileSync(outFile, html, 'utf-8'); - console.log(`Written to ${outFile}`); - process.exit(0); -} - -if (isStatic) { - // -- static mode: generate self-contained HTML file, open in browser, exit -- - 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 { - // -- server mode: start Express -- - const app = express(); - app.use(express.static(join(__dirname, 'public'))); + // --- mode dispatch --- - app.get('/patch', (req, res) => { - res.type('text/plain').send(patch); - }); + if (outFile) { + const html = buildStaticHtml(); + writeFileSync(outFile, html, 'utf-8'); + console.log(`Written to ${outFile}`); + process.exit(0); + } - app.get('/', (req, res) => { - const fileDataJson = JSON.stringify(renderedFiles.map(f => ({ name: f.name }))) - .replace(/<\/script>/gi, '<\\/script>'); + if (isStatic) { + const html = buildStaticHtml(); + const tmpFile = join(tmpdir(), `difftool-${Date.now()}.html`); + writeFileSync(tmpFile, html, 'utf-8'); - const sidebarHtml = ''; + 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 extraScript = ` + 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); - }); + 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`); + }); + } +})();