@tanishqkancharla/diffmap 0.1.5 → 0.3.0
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/README.md +64 -57
- package/index.html +18 -0
- package/package.json +7 -5
- package/src/annotations.ts +36 -29
- package/src/changes.ts +235 -0
- package/src/cli.ts +1 -0
- package/src/errors.ts +5 -0
- package/src/gist/GistApp.tsx +38 -17
- package/src/gist/fetchGist.ts +47 -0
- package/src/git.test.ts +402 -0
- package/src/git.ts +498 -0
- package/src/github/GitHubApp.tsx +39 -22
- package/src/github/fetchRepo.test.ts +59 -1
- package/src/github/fetchRepo.ts +99 -43
- package/src/github/patch.test.ts +18 -9
- package/src/github/patch.ts +30 -19
- package/src/host/HostHome.tsx +20 -7
- package/src/host/files.ts +5 -1
- package/src/host/home.md +17 -55
- package/src/host/home.test.ts +4 -3
- package/src/index.ts +1 -0
- package/src/parseFence.ts +6 -6
- package/src/parseViewer.test.ts +140 -0
- package/src/parseViewer.ts +104 -78
- package/src/serve.ts +34 -1
- package/src/share.ts +62 -1
- package/src/viewer/CallStackDiff.tsx +15 -3
- package/src/viewer/ChangesCard.tsx +267 -0
- package/src/viewer/ComarkView.tsx +83 -15
- package/src/viewer/Fence.tsx +9 -1
- package/src/viewer/MermaidBlock.tsx +99 -11
- package/src/viewer/RangePicker.tsx +526 -0
- package/src/viewer/SourceDiffPanel.tsx +254 -116
- package/src/viewer/TableOfContents.tsx +177 -93
- package/src/viewer/ViewerApp.tsx +34 -114
- package/src/viewer/changesSource.ts +91 -0
- package/src/viewer/sourceHeader.test.ts +2 -87
- package/src/viewer/sourceHeader.ts +4 -63
- package/src/viewer/sourceNavigation.ts +0 -43
- package/vite.config.ts +1 -0
- package/vite.gist.config.ts +57 -0
- package/src/viewer/TocButton.tsx +0 -27
package/src/github/fetchRepo.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* fetch `ref=main` / latest for spec markdown, [[path]] targets, excerpts, or
|
|
5
5
|
* hunks. Private repos are out of scope (GitHub 404).
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
8
|
-
import {
|
|
7
|
+
import { WORKING, type Changes, type Rev } from "../changes.ts";
|
|
8
|
+
import { githubChangedFiles, type GitHubFile } from "./patch.ts";
|
|
9
9
|
|
|
10
10
|
const GITHUB_API = "https://api.github.com";
|
|
11
11
|
const GITHUB_JSON = {
|
|
@@ -21,7 +21,8 @@ export type PinnedSpec = {
|
|
|
21
21
|
htmlUrl: string;
|
|
22
22
|
pullNumber?: number;
|
|
23
23
|
pullUrl?: string;
|
|
24
|
-
|
|
24
|
+
/** `base..head` of the pull request, for the Diff panel's default view. */
|
|
25
|
+
pullRev?: Rev;
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
export type GitHubSpecResult =
|
|
@@ -33,6 +34,20 @@ export type GitHubSpecResult =
|
|
|
33
34
|
type PullApi = {
|
|
34
35
|
html_url?: string;
|
|
35
36
|
head?: { sha?: string };
|
|
37
|
+
base?: { sha?: string };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type CommitApi = {
|
|
41
|
+
sha?: string;
|
|
42
|
+
html_url?: string;
|
|
43
|
+
commit?: { message?: string };
|
|
44
|
+
files?: GitHubFile[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type CompareApi = {
|
|
48
|
+
html_url?: string;
|
|
49
|
+
total_commits?: number;
|
|
50
|
+
files?: GitHubFile[];
|
|
36
51
|
};
|
|
37
52
|
|
|
38
53
|
type ContentsApi = {
|
|
@@ -56,6 +71,7 @@ export async function loadPinnedSpec(input: {
|
|
|
56
71
|
let sha = input.sha;
|
|
57
72
|
let pullNumber = input.pull;
|
|
58
73
|
let pullUrl: string | undefined;
|
|
74
|
+
let pullRev: Rev | undefined;
|
|
59
75
|
if (sha === undefined) {
|
|
60
76
|
if (pullNumber === undefined) {
|
|
61
77
|
return { status: "error", message: "Need a pull number or commit SHA." };
|
|
@@ -69,6 +85,10 @@ export async function loadPinnedSpec(input: {
|
|
|
69
85
|
if (resolved.status !== "ok") return resolved;
|
|
70
86
|
sha = resolved.sha;
|
|
71
87
|
pullUrl = resolved.htmlUrl;
|
|
88
|
+
pullRev =
|
|
89
|
+
resolved.baseSha === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: `${resolved.baseSha}..${sha}`;
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
const blob = await fetchPinnedBlob(
|
|
@@ -82,24 +102,6 @@ export async function loadPinnedSpec(input: {
|
|
|
82
102
|
return { status: "error", message: blob.message };
|
|
83
103
|
}
|
|
84
104
|
|
|
85
|
-
let pullDiffs: CodeViewDiffItem[] = [];
|
|
86
|
-
if (pullNumber !== undefined) {
|
|
87
|
-
const files = await fetchPullFiles(
|
|
88
|
-
input.owner,
|
|
89
|
-
input.repo,
|
|
90
|
-
pullNumber,
|
|
91
|
-
input.signal,
|
|
92
|
-
);
|
|
93
|
-
if (files instanceof Error) {
|
|
94
|
-
if (files.message === "forbidden") return { status: "forbidden" };
|
|
95
|
-
if (files.message !== "not-found") {
|
|
96
|
-
return { status: "error", message: files.message };
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
pullDiffs = pullFilesToSourceDiffs(files);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
105
|
return {
|
|
104
106
|
status: "ok",
|
|
105
107
|
spec: {
|
|
@@ -111,7 +113,7 @@ export async function loadPinnedSpec(input: {
|
|
|
111
113
|
htmlUrl: `https://github.com/${input.owner}/${input.repo}/blob/${sha}/${input.path}`,
|
|
112
114
|
pullNumber,
|
|
113
115
|
pullUrl,
|
|
114
|
-
|
|
116
|
+
pullRev,
|
|
115
117
|
},
|
|
116
118
|
};
|
|
117
119
|
}
|
|
@@ -161,7 +163,7 @@ async function resolvePullHead(
|
|
|
161
163
|
pull: number,
|
|
162
164
|
signal?: AbortSignal,
|
|
163
165
|
): Promise<
|
|
164
|
-
| { status: "ok"; sha: string; htmlUrl: string }
|
|
166
|
+
| { status: "ok"; sha: string; baseSha: string | undefined; htmlUrl: string }
|
|
165
167
|
| { status: "not-found" }
|
|
166
168
|
| { status: "forbidden" }
|
|
167
169
|
| { status: "error"; message: string }
|
|
@@ -181,9 +183,11 @@ async function resolvePullHead(
|
|
|
181
183
|
message: "GitHub did not return a pull head SHA.",
|
|
182
184
|
};
|
|
183
185
|
}
|
|
186
|
+
const baseSha = payload?.base?.sha;
|
|
184
187
|
return {
|
|
185
188
|
status: "ok",
|
|
186
189
|
sha,
|
|
190
|
+
baseSha: typeof baseSha === "string" ? baseSha : undefined,
|
|
187
191
|
htmlUrl:
|
|
188
192
|
typeof payload?.html_url === "string"
|
|
189
193
|
? payload.html_url
|
|
@@ -191,31 +195,83 @@ async function resolvePullHead(
|
|
|
191
195
|
};
|
|
192
196
|
}
|
|
193
197
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
signal,
|
|
198
|
+
const changesCache = new Map<string, Promise<Changes | Error>>();
|
|
199
|
+
|
|
200
|
+
/** Commit or compare changes from the GitHub API. Patches only, no full contents. */
|
|
201
|
+
export function fetchGitHubChanges(
|
|
202
|
+
repo: { owner: string; repo: string },
|
|
203
|
+
rev: Rev,
|
|
204
|
+
): Promise<Changes | Error> {
|
|
205
|
+
if (rev === WORKING)
|
|
206
|
+
return Promise.resolve(
|
|
207
|
+
new Error("Uncommitted changes are only available in a local viewer."),
|
|
205
208
|
);
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
const key = `${repo.owner}/${repo.repo}@${rev}`;
|
|
210
|
+
const pending = changesCache.get(key);
|
|
211
|
+
if (pending !== undefined) return pending;
|
|
212
|
+
const request = readGitHubChanges(repo, rev);
|
|
213
|
+
changesCache.set(key, request);
|
|
214
|
+
return request;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** GitHub pages a commit's files up to 3000; a compare lists only its first 300. */
|
|
218
|
+
const COMMIT_FILES_MAX = 3000;
|
|
219
|
+
const COMPARE_FILES_MAX = 300;
|
|
220
|
+
|
|
221
|
+
async function readGitHubChanges(
|
|
222
|
+
repo: { owner: string; repo: string },
|
|
223
|
+
rev: Rev,
|
|
224
|
+
): Promise<Changes | Error> {
|
|
225
|
+
const base = `/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}`;
|
|
226
|
+
const [from, to] = rev.split("..");
|
|
227
|
+
const files: GitHubFile[] = [];
|
|
228
|
+
let first: (CommitApi & CompareApi) | undefined;
|
|
229
|
+
let next: string | undefined =
|
|
230
|
+
to === undefined
|
|
231
|
+
? `${base}/commits/${from!}`
|
|
232
|
+
: `${base}/compare/${from!}...${to}`;
|
|
233
|
+
while (next !== undefined) {
|
|
234
|
+
const response = await githubFetch(next);
|
|
235
|
+
if (response.status === "not-found")
|
|
236
|
+
return new Error(
|
|
237
|
+
`GitHub has no commit ${rev} in ${repo.owner}/${repo.repo}.`,
|
|
238
|
+
);
|
|
239
|
+
if (response.status === "forbidden")
|
|
240
|
+
return new Error("GitHub rate-limited this request. Wait and refresh.");
|
|
208
241
|
if (response.status === "error") return new Error(response.message);
|
|
209
242
|
const payload = (await response.body.json().catch(() => undefined)) as
|
|
210
|
-
|
|
|
243
|
+
| (CommitApi & CompareApi)
|
|
211
244
|
| undefined;
|
|
212
|
-
if (
|
|
213
|
-
return new Error("GitHub returned invalid
|
|
214
|
-
|
|
215
|
-
files.push(...payload);
|
|
216
|
-
|
|
245
|
+
if (payload?.files === undefined)
|
|
246
|
+
return new Error("GitHub returned invalid changes.");
|
|
247
|
+
first ??= payload;
|
|
248
|
+
files.push(...payload.files);
|
|
249
|
+
// Compare pages list commits, not more files.
|
|
250
|
+
next = to === undefined ? nextPage(response.body) : undefined;
|
|
217
251
|
}
|
|
218
|
-
|
|
252
|
+
const subject = first?.commit?.message?.split("\n", 1)[0] ?? "";
|
|
253
|
+
const truncated =
|
|
254
|
+
files.length >= (to === undefined ? COMMIT_FILES_MAX : COMPARE_FILES_MAX);
|
|
255
|
+
return {
|
|
256
|
+
rev,
|
|
257
|
+
title:
|
|
258
|
+
to === undefined
|
|
259
|
+
? subject
|
|
260
|
+
: `${from!.slice(0, 7)}..${to.slice(0, 7)} · ${String(first?.total_commits ?? 0)} commits`,
|
|
261
|
+
...(first?.html_url === undefined ? {} : { url: first.html_url }),
|
|
262
|
+
...(truncated ? { truncated } : {}),
|
|
263
|
+
files: githubChangedFiles(files),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The API path of the `rel="next"` page in a GitHub `Link` header. */
|
|
268
|
+
function nextPage(response: Response) {
|
|
269
|
+
const link = /<([^>]+)>;\s*rel="next"/.exec(
|
|
270
|
+
response.headers.get("link") ?? "",
|
|
271
|
+
);
|
|
272
|
+
if (link === null) return undefined;
|
|
273
|
+
const url = new URL(link[1]!);
|
|
274
|
+
return `${url.pathname}${url.search}`;
|
|
219
275
|
}
|
|
220
276
|
|
|
221
277
|
async function readContents(
|
package/src/github/patch.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { parsePatchFiles } from "@pierre/diffs";
|
|
4
|
-
import {
|
|
4
|
+
import { changedFileItem } from "../changes.ts";
|
|
5
|
+
import { githubChangedFiles, wrapGitHubPatch } from "./patch.ts";
|
|
5
6
|
|
|
6
|
-
test("
|
|
7
|
-
const wrapped =
|
|
7
|
+
test("wrapGitHubPatch builds a unified diff Pierre can parse", () => {
|
|
8
|
+
const wrapped = wrapGitHubPatch({
|
|
8
9
|
filename: "src/cli.ts",
|
|
9
10
|
status: "modified",
|
|
10
11
|
patch: ["@@ -1,3 +1,4 @@", " line", "+added", " line", " line"].join("\n"),
|
|
@@ -15,16 +16,24 @@ test("wrapGitHubPullPatch builds a unified diff Pierre can parse", () => {
|
|
|
15
16
|
assert.ok((patches[0]?.files[0]?.hunks.length ?? 0) > 0);
|
|
16
17
|
});
|
|
17
18
|
|
|
18
|
-
test("
|
|
19
|
-
const
|
|
20
|
-
{ filename: "icon.png", status: "added" },
|
|
19
|
+
test("githubChangedFiles keeps files without a patch as binary, with no diff item", () => {
|
|
20
|
+
const files = githubChangedFiles([
|
|
21
|
+
{ filename: "icon.png", status: "added", additions: 0, deletions: 0 },
|
|
21
22
|
{
|
|
22
23
|
filename: "src/cli.ts",
|
|
23
24
|
status: "modified",
|
|
25
|
+
additions: 1,
|
|
26
|
+
deletions: 0,
|
|
24
27
|
patch: ["@@ -1,2 +1,3 @@", " a", "+b", " c"].join("\n"),
|
|
25
28
|
},
|
|
26
29
|
]);
|
|
27
|
-
assert.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
assert.deepEqual(
|
|
31
|
+
files.map((file) => [file.path, file.binary, file.additions]),
|
|
32
|
+
[
|
|
33
|
+
["icon.png", true, 0],
|
|
34
|
+
["src/cli.ts", false, 1],
|
|
35
|
+
],
|
|
36
|
+
);
|
|
37
|
+
assert.equal(changedFileItem(files[0]!), undefined);
|
|
38
|
+
assert.equal(changedFileItem(files[1]!)?.fileDiff.name, "src/cli.ts");
|
|
30
39
|
});
|
package/src/github/patch.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ChangedFile } from "../changes.ts";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** A file from GitHub's commit, compare, or pull-files APIs. */
|
|
4
|
+
export type GitHubFile = {
|
|
4
5
|
filename: string;
|
|
5
6
|
previous_filename?: string;
|
|
6
7
|
status: string;
|
|
8
|
+
additions?: number;
|
|
9
|
+
deletions?: number;
|
|
7
10
|
patch?: string;
|
|
8
11
|
};
|
|
9
12
|
|
|
10
|
-
/** Wrap a GitHub
|
|
11
|
-
export function
|
|
13
|
+
/** Wrap a GitHub file `patch` (hunks only) as a unified diff Pierre can parse. */
|
|
14
|
+
export function wrapGitHubPatch(file: GitHubFile) {
|
|
12
15
|
if (file.patch === undefined || file.patch.length === 0) return undefined;
|
|
13
16
|
const oldPath = file.previous_filename ?? file.filename;
|
|
14
17
|
const newPath = file.filename;
|
|
@@ -22,19 +25,27 @@ export function wrapGitHubPullPatch(file: GitHubPullFile) {
|
|
|
22
25
|
].join("\n");
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
export function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
export function githubChangedFiles(files: GitHubFile[]): ChangedFile[] {
|
|
29
|
+
return files.map((file) => {
|
|
30
|
+
const patch = wrapGitHubPatch(file);
|
|
31
|
+
return {
|
|
32
|
+
path: file.filename,
|
|
33
|
+
...(file.previous_filename === undefined
|
|
34
|
+
? {}
|
|
35
|
+
: { prevPath: file.previous_filename }),
|
|
36
|
+
status:
|
|
37
|
+
file.status === "added"
|
|
38
|
+
? "added"
|
|
39
|
+
: file.status === "removed"
|
|
40
|
+
? "deleted"
|
|
41
|
+
: file.status === "renamed"
|
|
42
|
+
? "renamed"
|
|
43
|
+
: "modified",
|
|
44
|
+
additions: file.additions ?? 0,
|
|
45
|
+
deletions: file.deletions ?? 0,
|
|
46
|
+
// GitHub omits the patch for binary and very large files.
|
|
47
|
+
binary: patch === undefined,
|
|
48
|
+
...(patch === undefined ? {} : { patch }),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
40
51
|
}
|
package/src/host/HostHome.tsx
CHANGED
|
@@ -5,6 +5,11 @@ import { parseViewerDocument } from "../parseViewer.ts";
|
|
|
5
5
|
import type { ViewerDocument } from "../parseViewer.ts";
|
|
6
6
|
import { ViewerApp } from "../viewer/ViewerApp.tsx";
|
|
7
7
|
import { GistLoading, GistStatus } from "../gist/GistStatus.tsx";
|
|
8
|
+
import { fetchGitHubChanges } from "../github/fetchRepo.ts";
|
|
9
|
+
import {
|
|
10
|
+
ChangesLoaderContext,
|
|
11
|
+
type ChangesLoader,
|
|
12
|
+
} from "../viewer/changesSource.ts";
|
|
8
13
|
import { HostFilesContext } from "./files.ts";
|
|
9
14
|
import homeSource from "./home.md?raw";
|
|
10
15
|
import cliSource from "../cli.ts?raw";
|
|
@@ -26,6 +31,10 @@ const hostFiles = {
|
|
|
26
31
|
"src/github/route.ts": githubRouteSource,
|
|
27
32
|
};
|
|
28
33
|
|
|
34
|
+
/** The homepage walks through diffmap's own commits, read from GitHub. */
|
|
35
|
+
const loadHostChanges: ChangesLoader = (rev) =>
|
|
36
|
+
fetchGitHubChanges({ owner: "tanishqkancharla", repo: "diffmap" }, rev);
|
|
37
|
+
|
|
29
38
|
export function HostHome() {
|
|
30
39
|
const [view, setView] = useState<
|
|
31
40
|
| { kind: "loading" }
|
|
@@ -61,13 +70,17 @@ export function HostHome() {
|
|
|
61
70
|
|
|
62
71
|
return (
|
|
63
72
|
<HostFilesContext.Provider value={hostFiles}>
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
<ChangesLoaderContext.Provider value={loadHostChanges}>
|
|
74
|
+
<ViewerApp
|
|
75
|
+
document={view.document}
|
|
76
|
+
mode="host"
|
|
77
|
+
headerActions={
|
|
78
|
+
<Link href="https://github.com/tanishqkancharla/diffmap">
|
|
79
|
+
GitHub
|
|
80
|
+
</Link>
|
|
81
|
+
}
|
|
82
|
+
/>
|
|
83
|
+
</ChangesLoaderContext.Provider>
|
|
71
84
|
</HostFilesContext.Provider>
|
|
72
85
|
);
|
|
73
86
|
}
|
package/src/host/files.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Files a hosted viewer has without a server: the homepage's bundled sources,
|
|
5
|
+
* or the files `diffmap share` saved in a gist. `[[path]]` and file excerpts
|
|
6
|
+
* read these, not GitHub.
|
|
7
|
+
*/
|
|
4
8
|
export type HostFiles = Record<string, string>;
|
|
5
9
|
|
|
6
10
|
export const HostFilesContext = createContext<HostFiles | undefined>(undefined);
|
package/src/host/home.md
CHANGED
|
@@ -7,7 +7,7 @@ TOC on the left, article here, Diff on the right. Click a stack row or a mermaid
|
|
|
7
7
|
## Install the skill
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npx skills add tanishqkancharla/diffmap --skill generate-
|
|
10
|
+
npx skills add tanishqkancharla/diffmap --skill generate-diffmap
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
That keeps a living spec in `specs/<name>.md`. Same file for the life of the work: what’s already in the tree, and what’s still planned. Don’t start a second walkthrough later.
|
|
@@ -42,13 +42,13 @@ Pinned commit:
|
|
|
42
42
|
|
|
43
43
|
`https://diffmap.dev/<owner>/<repo>/commit/<sha>/specs/<name>.md`
|
|
44
44
|
|
|
45
|
-
The pin is one SHA. The markdown and every `[[path]]`, mermaid `%% ref`,
|
|
45
|
+
The pin is one SHA. The markdown and every `[[path]]`, mermaid `%% ref`, and excerpt load from that commit, not `main`. Commit references load their diffs from the GitHub API. A PR number only chooses the SHA (head at load, then pin). Refresh stays on the commit URL.
|
|
46
46
|
|
|
47
47
|
## What the fences are
|
|
48
48
|
|
|
49
49
|
### System flow
|
|
50
50
|
|
|
51
|
-
Click a node. `%% ref` is a mermaid comment; `[[path]]` and `[[path#
|
|
51
|
+
Click a node. `%% ref` is a mermaid comment; `[[path]]`, `[[path#symbol]]`, and `[[<sha>:path#L12-L30]]` are the same references as on a stack row.
|
|
52
52
|
|
|
53
53
|
```mermaid
|
|
54
54
|
flowchart LR
|
|
@@ -59,7 +59,7 @@ flowchart LR
|
|
|
59
59
|
%% ref node:Serve [[src/cli.ts]]
|
|
60
60
|
%% ref node:Share [[src/share.ts#shareMarkdownFile]]
|
|
61
61
|
%% ref node:Pin [[src/github/route.ts]]
|
|
62
|
-
%% ref edge:0 [[cli
|
|
62
|
+
%% ref edge:0 [[1257fe8:src/cli.ts#L49-L50]]
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
````md
|
|
@@ -72,68 +72,30 @@ flowchart LR
|
|
|
72
72
|
|
|
73
73
|
### Call stacks
|
|
74
74
|
|
|
75
|
-
`-` is current, `+` is proposed. After a phase lands, retarget `[[path]]` to
|
|
75
|
+
`-` is current, `+` is proposed. After a phase lands, commit it and retarget `[[path]]` to the commit: `[[<sha>:path#L12-L30]]`. Line numbers are the file after the commit.
|
|
76
76
|
|
|
77
77
|
```callstack
|
|
78
78
|
ViewerApp
|
|
79
|
-
-└──
|
|
80
|
-
+└── CloseServerButton # local mode only [[
|
|
79
|
+
-└── DoneButton # a loud Done control
|
|
80
|
+
+└── CloseServerButton # quiet, local mode only [[dae6b98:src/viewer/ViewerApp.tsx#L219-L227]] [[dae6b98:src/viewer/CloseServerButton.tsx#L8-L16]]
|
|
81
81
|
└── DiffButton
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
```callstack
|
|
85
85
|
shareMarkdownFile [[src/share.ts#shareMarkdownFile]]
|
|
86
|
-
-└── print gist URL
|
|
87
|
-
+└── gistViewerUrl # https://diffmap.dev/g/<id> [[share
|
|
86
|
+
-└── print the gist URL
|
|
87
|
+
+└── gistViewerUrl # https://diffmap.dev/g/<id> [[d14ae1a:src/share.ts#L55-L59]]
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
###
|
|
90
|
+
### Changes
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
A `changes` fence names a commit. It renders a card with every file that commit changed. **Review** opens the Diff panel on all of them, with a sticky header per file; a row jumps to its file. An empty body means uncommitted changes.
|
|
93
93
|
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
--- a/src/cli.ts
|
|
97
|
-
+++ b/src/cli.ts
|
|
98
|
-
@@ -49,4 +49,4 @@
|
|
99
|
-
const cli = Cli.create("diffmap", {
|
|
100
|
-
- description: "Serve a walkthrough",
|
|
101
|
-
+ description: "Serve a spec locally, or share it as a gist",
|
|
102
|
-
version: "0.1.3",
|
|
103
|
-
args: serveArgs,
|
|
94
|
+
```changes
|
|
95
|
+
dae6b98
|
|
104
96
|
```
|
|
105
97
|
|
|
106
|
-
|
|
107
|
-
diff --git a/src/share.ts b/src/share.ts
|
|
108
|
-
--- a/src/share.ts
|
|
109
|
-
+++ b/src/share.ts
|
|
110
|
-
@@ -59,6 +59,6 @@
|
|
111
|
-
return {
|
|
112
|
-
gistId,
|
|
113
|
-
gistUrl,
|
|
114
|
-
- viewerUrl: gistUrl,
|
|
115
|
-
+ viewerUrl: gistViewerUrl(gistId, undefined, process.env.DIFFMAP_ORIGIN),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
```source-diff:close:src/viewer/ViewerApp.tsx
|
|
121
|
-
diff --git a/src/viewer/ViewerApp.tsx b/src/viewer/ViewerApp.tsx
|
|
122
|
-
--- a/src/viewer/ViewerApp.tsx
|
|
123
|
-
+++ b/src/viewer/ViewerApp.tsx
|
|
124
|
-
@@ -216,3 +216,10 @@
|
|
125
|
-
{props.headerActions}
|
|
126
|
-
- <CloseServerButton onClick={() => setShutDown(true)} />
|
|
127
|
-
+ {props.mode === "local" && (
|
|
128
|
-
+ <CloseServerButton
|
|
129
|
-
+ onClick={() => {
|
|
130
|
-
+ setShutDown(true);
|
|
131
|
-
+ void closeViewer();
|
|
132
|
-
+ }}
|
|
133
|
-
+ />
|
|
134
|
-
+ )}
|
|
135
|
-
</div>
|
|
136
|
-
```
|
|
98
|
+
Commit references must name a file the commit changed, and the range must fit the file after the commit. `serve` and `share` check this at startup.
|
|
137
99
|
|
|
138
100
|
### File excerpts
|
|
139
101
|
|
|
@@ -167,12 +129,12 @@ Local `serve` trusts `html` in the file you pointed at. This host does not. Gist
|
|
|
167
129
|
|
|
168
130
|
## How a spec stays alive
|
|
169
131
|
|
|
170
|
-
Write `specs/<name>.md`. Serve it while you plan. When the plan changes, edit this file. When a phase lands,
|
|
132
|
+
Write `specs/<name>.md`. Serve it while you plan. When the plan changes, edit this file. When a phase lands, commit it, add a `changes` fence, and retarget the links to the commit so the Diff panel opens the change, not just the file.
|
|
171
133
|
|
|
172
134
|
### Phase 1: Spec and serve
|
|
173
135
|
|
|
174
136
|
```callstack
|
|
175
|
-
generate-
|
|
137
|
+
generate-diffmap
|
|
176
138
|
└── specs/name.md
|
|
177
139
|
└── diffmap serve [[src/cli.ts]]
|
|
178
140
|
```
|
|
@@ -186,7 +148,7 @@ Write `specs/<name>.md`. Serve it while you plan. When the plan changes, edit th
|
|
|
186
148
|
```callstack
|
|
187
149
|
specs/name.md
|
|
188
150
|
-└── localhost only
|
|
189
|
-
+├── diffmap share [[share
|
|
151
|
+
+├── diffmap share [[d14ae1a:src/share.ts#L55-L59]]
|
|
190
152
|
+└── GitHub PR URL [[src/github/route.ts]]
|
|
191
153
|
```
|
|
192
154
|
|
package/src/host/home.test.ts
CHANGED
|
@@ -30,7 +30,7 @@ test("homepage markdown is a real viewer document", async () => {
|
|
|
30
30
|
const kinds = new Set(fences.map((fence) => fence.kind));
|
|
31
31
|
assert.equal(kinds.has("mermaid"), true);
|
|
32
32
|
assert.equal(kinds.has("callstack"), true);
|
|
33
|
-
assert.equal(kinds.has("
|
|
33
|
+
assert.equal(kinds.has("changes"), true);
|
|
34
34
|
assert.equal(kinds.has("file"), true);
|
|
35
35
|
assert.equal(kinds.has("html"), true);
|
|
36
36
|
|
|
@@ -63,8 +63,9 @@ test("homepage markdown is a real viewer document", async () => {
|
|
|
63
63
|
assert.equal(levels.has(1), true);
|
|
64
64
|
assert.equal(levels.has(2), true);
|
|
65
65
|
assert.equal(levels.has(3), true);
|
|
66
|
-
assert.ok(
|
|
67
|
-
|
|
66
|
+
assert.ok(
|
|
67
|
+
document.references.filter((ref) => ref.rev !== undefined).length >= 3,
|
|
68
|
+
);
|
|
68
69
|
|
|
69
70
|
const excerpt = fences.find(
|
|
70
71
|
(fence) => fence.kind === "file" && fence.path === "src/cli.ts",
|
package/src/index.ts
CHANGED
package/src/parseFence.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseCallStack, type SourceAnnotation } from "./annotations.js";
|
|
2
|
+
import { WORKING, type Rev } from "./changes.js";
|
|
2
3
|
import {
|
|
3
4
|
parseDiagramAnnotations,
|
|
4
5
|
type DiagramAnnotation,
|
|
@@ -8,7 +9,7 @@ export type Fence =
|
|
|
8
9
|
| { kind: "mermaid"; source: string; annotations: DiagramAnnotation[] }
|
|
9
10
|
| { kind: "html"; source: string }
|
|
10
11
|
| { kind: "callstack"; source: string; lines: SourceAnnotation[] }
|
|
11
|
-
| { kind: "
|
|
12
|
+
| { kind: "changes"; rev: Rev; source: string }
|
|
12
13
|
| { kind: "file"; path: string; start: number; end: number; source: string }
|
|
13
14
|
| { kind: "diff"; path: string | undefined; source: string }
|
|
14
15
|
| { kind: "code"; lang: string; source: string };
|
|
@@ -46,12 +47,11 @@ export function parseFence(lang: string, source: string): Fence {
|
|
|
46
47
|
lines: parseCallStack(trimmed),
|
|
47
48
|
};
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
if (lang === "changes") {
|
|
51
|
+
const rev = trimmed.trim();
|
|
51
52
|
return {
|
|
52
|
-
kind: "
|
|
53
|
-
|
|
54
|
-
path: sourceDiff[2]!,
|
|
53
|
+
kind: "changes",
|
|
54
|
+
rev: rev.length === 0 ? WORKING : rev,
|
|
55
55
|
source: trimmed,
|
|
56
56
|
};
|
|
57
57
|
}
|