git-diff-view 0.0.1
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.
- package/CLAUDE.md +106 -0
- package/README.md +49 -0
- package/frontend.tsx +58 -0
- package/index.html +12 -0
- package/index.ts +150 -0
- package/package.json +25 -0
- package/styles.css +16 -0
- package/tsconfig.json +29 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
|
|
2
|
+
Default to using Bun instead of Node.js.
|
|
3
|
+
|
|
4
|
+
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
5
|
+
- Use `bun test` instead of `jest` or `vitest`
|
|
6
|
+
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
7
|
+
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
8
|
+
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
9
|
+
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
|
10
|
+
- Bun automatically loads .env, so don't use dotenv.
|
|
11
|
+
|
|
12
|
+
## APIs
|
|
13
|
+
|
|
14
|
+
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
15
|
+
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
16
|
+
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
17
|
+
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
18
|
+
- `WebSocket` is built-in. Don't use `ws`.
|
|
19
|
+
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
20
|
+
- Bun.$`ls` instead of execa.
|
|
21
|
+
|
|
22
|
+
## Testing
|
|
23
|
+
|
|
24
|
+
Use `bun test` to run tests.
|
|
25
|
+
|
|
26
|
+
```ts#index.test.ts
|
|
27
|
+
import { test, expect } from "bun:test";
|
|
28
|
+
|
|
29
|
+
test("hello world", () => {
|
|
30
|
+
expect(1).toBe(1);
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Frontend
|
|
35
|
+
|
|
36
|
+
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
37
|
+
|
|
38
|
+
Server:
|
|
39
|
+
|
|
40
|
+
```ts#index.ts
|
|
41
|
+
import index from "./index.html"
|
|
42
|
+
|
|
43
|
+
Bun.serve({
|
|
44
|
+
routes: {
|
|
45
|
+
"/": index,
|
|
46
|
+
"/api/users/:id": {
|
|
47
|
+
GET: (req) => {
|
|
48
|
+
return new Response(JSON.stringify({ id: req.params.id }));
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
// optional websocket support
|
|
53
|
+
websocket: {
|
|
54
|
+
open: (ws) => {
|
|
55
|
+
ws.send("Hello, world!");
|
|
56
|
+
},
|
|
57
|
+
message: (ws, message) => {
|
|
58
|
+
ws.send(message);
|
|
59
|
+
},
|
|
60
|
+
close: (ws) => {
|
|
61
|
+
// handle close
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
development: {
|
|
65
|
+
hmr: true,
|
|
66
|
+
console: true,
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
72
|
+
|
|
73
|
+
```html#index.html
|
|
74
|
+
<html>
|
|
75
|
+
<body>
|
|
76
|
+
<h1>Hello, world!</h1>
|
|
77
|
+
<script type="module" src="./frontend.tsx"></script>
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
With the following `frontend.tsx`:
|
|
83
|
+
|
|
84
|
+
```tsx#frontend.tsx
|
|
85
|
+
import React from "react";
|
|
86
|
+
import { createRoot } from "react-dom/client";
|
|
87
|
+
|
|
88
|
+
// import .css files directly and it works
|
|
89
|
+
import './index.css';
|
|
90
|
+
|
|
91
|
+
const root = createRoot(document.body);
|
|
92
|
+
|
|
93
|
+
export default function Frontend() {
|
|
94
|
+
return <h1>Hello, world!</h1>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
root.render(<Frontend />);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then, run index.ts
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
bun --hot ./index.ts
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# git-diff-view
|
|
2
|
+
|
|
3
|
+
A beautiful git diff viewer that opens in your browser, powered by [@pierre/diffs](https://diffs.com/).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎨 **Auto theme detection** - matches your system light/dark mode
|
|
8
|
+
- 📊 **Stacked (unified) layout** - easy to read diffs
|
|
9
|
+
- 📏 **Styled bars** - visual indicators for additions/deletions
|
|
10
|
+
- 🔢 **Line numbers** - always visible
|
|
11
|
+
- ✨ **Word-level highlighting** - see exactly what changed
|
|
12
|
+
- 📁 **Multi-file support** - all changes in one view
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun install
|
|
18
|
+
./index.ts install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This installs a global git alias `dv`.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
git dv # unstaged changes
|
|
27
|
+
git dv --staged # staged changes
|
|
28
|
+
git dv HEAD~3 # last 3 commits
|
|
29
|
+
git dv main..HEAD # compare branches
|
|
30
|
+
git dv -- file.ts # specific file
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
./index.ts install # install git alias
|
|
37
|
+
./index.ts uninstall # remove git alias
|
|
38
|
+
./index.ts run # view diff (used by alias)
|
|
39
|
+
./index.ts --help # show help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Requirements
|
|
43
|
+
|
|
44
|
+
- [Bun](https://bun.sh) runtime
|
|
45
|
+
- macOS (for `open` command and theme detection)
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
package/frontend.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { FileDiff } from "@pierre/diffs/react";
|
|
2
|
+
import { parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs";
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
|
|
6
|
+
interface DiffData {
|
|
7
|
+
patch: string;
|
|
8
|
+
theme: "pierre-dark" | "pierre-light";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const App = () => {
|
|
12
|
+
const [data, setData] = useState<DiffData | null>(null);
|
|
13
|
+
const [files, setFiles] = useState<FileDiffMetadata[]>([]);
|
|
14
|
+
const [error, setError] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
fetch("/api/diff")
|
|
18
|
+
.then((res) => res.json())
|
|
19
|
+
.then((data: DiffData) => {
|
|
20
|
+
setData(data);
|
|
21
|
+
const parsed = parsePatchFiles(data.patch);
|
|
22
|
+
const allFiles = parsed.flatMap((p) => p.files);
|
|
23
|
+
setFiles(allFiles);
|
|
24
|
+
})
|
|
25
|
+
.catch((err) => setError(err.message));
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
if (error) {
|
|
29
|
+
return <div style={{ color: "red", padding: 20 }}>Error: {error}</div>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!data) {
|
|
33
|
+
return <div style={{ color: "#888", padding: 20 }}>Loading...</div>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (files.length === 0) {
|
|
37
|
+
return <div style={{ color: "#888", padding: 20 }}>No changes</div>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
42
|
+
{files.map((file, i) => (
|
|
43
|
+
<FileDiff
|
|
44
|
+
key={i}
|
|
45
|
+
fileDiff={file}
|
|
46
|
+
options={{
|
|
47
|
+
theme: data.theme,
|
|
48
|
+
diffStyle: "unified",
|
|
49
|
+
diffIndicators: "bars",
|
|
50
|
+
lineDiffType: "word",
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
createRoot(document.getElementById("diff")!).render(<App />);
|
package/index.html
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
import index from "./index.html";
|
|
5
|
+
|
|
6
|
+
const SCRIPT_PATH = import.meta.path;
|
|
7
|
+
|
|
8
|
+
const isDarkMode = async (): Promise<boolean> => {
|
|
9
|
+
if (process.platform !== "darwin") return true;
|
|
10
|
+
try {
|
|
11
|
+
const result = await $`defaults read -g AppleInterfaceStyle`.quiet();
|
|
12
|
+
return result.text().trim() === "Dark";
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const getPatch = async (args: string[]): Promise<string> => {
|
|
19
|
+
try {
|
|
20
|
+
const proc = Bun.spawn(["git", "diff", ...args], {
|
|
21
|
+
stdout: "pipe",
|
|
22
|
+
stderr: "pipe",
|
|
23
|
+
});
|
|
24
|
+
const stdout = await new Response(proc.stdout).text();
|
|
25
|
+
const stderr = await new Response(proc.stderr).text();
|
|
26
|
+
const exitCode = await proc.exited;
|
|
27
|
+
|
|
28
|
+
if (exitCode !== 0) {
|
|
29
|
+
if (stderr.includes("Not a git repository")) {
|
|
30
|
+
console.error("Error: Not a git repository");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
console.error(stderr);
|
|
34
|
+
process.exit(exitCode);
|
|
35
|
+
}
|
|
36
|
+
return stdout;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const install = async () => {
|
|
43
|
+
const aliasValue = `!${SCRIPT_PATH} run`;
|
|
44
|
+
await $`git config --global alias.dv ${aliasValue}`;
|
|
45
|
+
console.log("✓ Installed git alias 'dv'");
|
|
46
|
+
console.log(" Usage: git dv [options]");
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const uninstall = async () => {
|
|
50
|
+
try {
|
|
51
|
+
await $`git config --global --unset alias.dv`.quiet();
|
|
52
|
+
console.log("✓ Uninstalled git alias 'dv'");
|
|
53
|
+
} catch {
|
|
54
|
+
console.log("Alias 'dv' was not installed");
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const run = async (args: string[]) => {
|
|
59
|
+
const patch = await getPatch(args);
|
|
60
|
+
if (!patch.trim()) {
|
|
61
|
+
console.log("No diff to display");
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const dark = await isDarkMode();
|
|
66
|
+
const theme = dark ? "pierre-dark" : "pierre-light";
|
|
67
|
+
|
|
68
|
+
const diffData = JSON.stringify({ patch, theme });
|
|
69
|
+
|
|
70
|
+
const server = Bun.serve({
|
|
71
|
+
port: 0,
|
|
72
|
+
routes: {
|
|
73
|
+
"/": index,
|
|
74
|
+
"/api/diff": new Response(diffData, {
|
|
75
|
+
headers: { "Content-Type": "application/json" },
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
development: {
|
|
79
|
+
hmr: true,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const url = `http://localhost:${server.port}`;
|
|
84
|
+
console.log(`Opening diff at ${url}`);
|
|
85
|
+
|
|
86
|
+
await $`open ${url}`;
|
|
87
|
+
|
|
88
|
+
process.on("SIGINT", () => {
|
|
89
|
+
server.stop();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
console.log("Press Ctrl+C to exit");
|
|
94
|
+
await Bun.sleep(Number.MAX_SAFE_INTEGER);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const showHelp = () => {
|
|
98
|
+
console.log(`git-diff-view - Beautiful git diffs in your browser
|
|
99
|
+
|
|
100
|
+
Usage:
|
|
101
|
+
git-diff-view <command> [options]
|
|
102
|
+
|
|
103
|
+
Commands:
|
|
104
|
+
install Install the 'git dv' alias globally
|
|
105
|
+
uninstall Remove the 'git dv' alias
|
|
106
|
+
run View diff in browser (used by the alias)
|
|
107
|
+
|
|
108
|
+
Examples:
|
|
109
|
+
git-diff-view install
|
|
110
|
+
git-diff-view run
|
|
111
|
+
git-diff-view run --staged
|
|
112
|
+
git-diff-view run HEAD~3
|
|
113
|
+
|
|
114
|
+
After installing, use:
|
|
115
|
+
git dv # unstaged changes
|
|
116
|
+
git dv --staged # staged changes
|
|
117
|
+
git dv HEAD~3 # last 3 commits
|
|
118
|
+
`);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const main = async () => {
|
|
122
|
+
const args = process.argv.slice(2);
|
|
123
|
+
const command = args[0];
|
|
124
|
+
|
|
125
|
+
switch (command) {
|
|
126
|
+
case "install":
|
|
127
|
+
await install();
|
|
128
|
+
break;
|
|
129
|
+
case "uninstall":
|
|
130
|
+
await uninstall();
|
|
131
|
+
break;
|
|
132
|
+
case "run":
|
|
133
|
+
await run(args.slice(1));
|
|
134
|
+
break;
|
|
135
|
+
case "--help":
|
|
136
|
+
case "-h":
|
|
137
|
+
showHelp();
|
|
138
|
+
break;
|
|
139
|
+
default:
|
|
140
|
+
if (!command) {
|
|
141
|
+
showHelp();
|
|
142
|
+
} else {
|
|
143
|
+
console.error(`Unknown command: ${command}`);
|
|
144
|
+
console.error("Run 'git-diff-view --help' for usage");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-diff-view",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"git-diff-view": "./index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "bun run index.ts"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/bun": "latest"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"typescript": "^5"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@pierre/diffs": "^1.0.6",
|
|
20
|
+
"@types/react": "^19.2.8",
|
|
21
|
+
"@types/react-dom": "^19.2.3",
|
|
22
|
+
"react": "^19.2.3",
|
|
23
|
+
"react-dom": "^19.2.3"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/styles.css
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
body {
|
|
6
|
+
margin: 0;
|
|
7
|
+
padding: 20px;
|
|
8
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
9
|
+
background: var(--bg-color, #ffffff);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@media (prefers-color-scheme: dark) {
|
|
13
|
+
body {
|
|
14
|
+
background: #1a1a1a;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|