@tanishqkancharla/diffmap 0.2.0 → 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 +10 -3
- package/package.json +1 -1
- package/src/changes.ts +6 -0
- package/src/gist/GistApp.tsx +23 -20
- package/src/gist/fetchGist.ts +42 -18
- package/src/host/files.ts +5 -1
- package/src/parseViewer.test.ts +53 -0
- package/src/parseViewer.ts +1 -1
- package/src/share.ts +50 -7
- package/src/viewer/SourceDiffPanel.tsx +2 -1
package/README.md
CHANGED
|
@@ -170,9 +170,16 @@ Malformed references produce a parse error.
|
|
|
170
170
|
|
|
171
171
|
On a GitHub-hosted spec, commit diffs come from the GitHub API. It returns patch
|
|
172
172
|
hunks only, so only lines near changes are reachable, and the Diff button opens
|
|
173
|
-
the pull request's full diff.
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
the pull request's full diff.
|
|
174
|
+
|
|
175
|
+
A shared gist is a snapshot taken when you run `diffmap share`. It saves every
|
|
176
|
+
referenced commit and every `changes` card, including uncommitted ones, to
|
|
177
|
+
`diffmap-changes.json`. It saves the current contents of every file that a file
|
|
178
|
+
excerpt or a reference without a commit reads to `diffmap-files.json`. In the
|
|
179
|
+
gist, those references open the saved file; symbols resolve to their first
|
|
180
|
+
matching line, and Cmd-click definitions only work in a local viewer. Commit
|
|
181
|
+
your work and point references at the commit before sharing, so the gist shows
|
|
182
|
+
the diff instead of a bare file.
|
|
176
183
|
|
|
177
184
|
Cmd-click (or Ctrl-click) a TypeScript or JavaScript symbol in the source panel
|
|
178
185
|
to open its definition. Use Back to retrace navigation and return to the reference.
|
package/package.json
CHANGED
package/src/changes.ts
CHANGED
|
@@ -159,6 +159,12 @@ export type Changes = {
|
|
|
159
159
|
/** Gist file holding the changes a shared spec references, keyed by rev. */
|
|
160
160
|
export const GIST_CHANGES_FILE = "diffmap-changes.json";
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Gist file holding the current contents of files a shared spec reads live,
|
|
164
|
+
* keyed by path: file excerpts and references without a commit.
|
|
165
|
+
*/
|
|
166
|
+
export const GIST_FILES_FILE = "diffmap-files.json";
|
|
167
|
+
|
|
162
168
|
export type ChangesResponse = { changes?: Changes; error?: string };
|
|
163
169
|
|
|
164
170
|
/** jsdiff's default lines of context around each change. */
|
package/src/gist/GistApp.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { ViewerApp } from "../viewer/ViewerApp.tsx";
|
|
|
13
13
|
import { loadGistMarkdown, type GistDocument } from "./fetchGist.ts";
|
|
14
14
|
import { GistPinContext } from "./pin.ts";
|
|
15
15
|
import { HostHome } from "../host/HostHome.tsx";
|
|
16
|
+
import { HostFilesContext } from "../host/files.ts";
|
|
16
17
|
import { GistLoading, GistStatus } from "./GistStatus.tsx";
|
|
17
18
|
import {
|
|
18
19
|
gistPath,
|
|
@@ -166,26 +167,28 @@ export function GistApp() {
|
|
|
166
167
|
htmlUrl: view.gist.htmlUrl,
|
|
167
168
|
}}
|
|
168
169
|
>
|
|
169
|
-
<
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
170
|
+
<HostFilesContext.Provider value={view.gist.sourceFiles}>
|
|
171
|
+
<ChangesLoaderContext.Provider value={view.loadChanges}>
|
|
172
|
+
<ViewerApp
|
|
173
|
+
key={`${view.gist.id}:${view.file}`}
|
|
174
|
+
document={view.document}
|
|
175
|
+
mode="gist"
|
|
176
|
+
headerActions={
|
|
177
|
+
<GistHeader
|
|
178
|
+
gistId={view.gist.id}
|
|
179
|
+
htmlUrl={view.gist.htmlUrl}
|
|
180
|
+
files={view.gist.files}
|
|
181
|
+
current={view.file}
|
|
182
|
+
onSelect={(file) => {
|
|
183
|
+
const next = gistPath(view.gist.id, file);
|
|
184
|
+
window.history.pushState({}, "", next);
|
|
185
|
+
setRoute(parseGistPath(next));
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
}
|
|
189
|
+
/>
|
|
190
|
+
</ChangesLoaderContext.Provider>
|
|
191
|
+
</HostFilesContext.Provider>
|
|
189
192
|
</GistPinContext.Provider>
|
|
190
193
|
);
|
|
191
194
|
}
|
package/src/gist/fetchGist.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
GIST_CHANGES_FILE,
|
|
3
|
+
GIST_FILES_FILE,
|
|
4
|
+
type Changes,
|
|
5
|
+
type Rev,
|
|
6
|
+
} from "../changes.ts";
|
|
7
|
+
import type { HostFiles } from "../host/files.ts";
|
|
2
8
|
import { githubGistUrl, markdownFileNames, pickMarkdownFile } from "./route.ts";
|
|
3
9
|
|
|
4
10
|
export type GistMarkdownFile = {
|
|
@@ -11,8 +17,10 @@ export type GistDocument = {
|
|
|
11
17
|
htmlUrl: string;
|
|
12
18
|
files: GistMarkdownFile[];
|
|
13
19
|
chosen: GistMarkdownFile;
|
|
14
|
-
/** Snapshot `diffmap share` saved for each commit the spec references. */
|
|
20
|
+
/** Snapshot `diffmap share` saved for each commit and changes card the spec references. */
|
|
15
21
|
changes: Record<Rev, Changes>;
|
|
22
|
+
/** Files excerpts and references without a commit read, as they were when shared. */
|
|
23
|
+
sourceFiles: HostFiles;
|
|
16
24
|
};
|
|
17
25
|
|
|
18
26
|
export type GistLoadResult =
|
|
@@ -98,23 +106,21 @@ export async function loadGistMarkdown(
|
|
|
98
106
|
return { status: "missing-file", htmlUrl, files };
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return { status: "error", message: snapshot.message };
|
|
109
|
+
// SAFETY: diffmap share writes this file from readDocumentChanges.
|
|
110
|
+
const changes = (await readSnapshot(apiFiles, GIST_CHANGES_FILE, signal)) as
|
|
111
|
+
| Record<Rev, Changes>
|
|
112
|
+
| Error;
|
|
113
|
+
if (changes instanceof Error) {
|
|
114
|
+
return { status: "error", message: changes.message };
|
|
108
115
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
};
|
|
116
|
+
// SAFETY: diffmap share writes this file as path to contents.
|
|
117
|
+
const sourceFiles = (await readSnapshot(
|
|
118
|
+
apiFiles,
|
|
119
|
+
GIST_FILES_FILE,
|
|
120
|
+
signal,
|
|
121
|
+
)) as HostFiles | Error;
|
|
122
|
+
if (sourceFiles instanceof Error) {
|
|
123
|
+
return { status: "error", message: sourceFiles.message };
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
return {
|
|
@@ -125,10 +131,28 @@ export async function loadGistMarkdown(
|
|
|
125
131
|
files,
|
|
126
132
|
chosen,
|
|
127
133
|
changes,
|
|
134
|
+
sourceFiles,
|
|
128
135
|
},
|
|
129
136
|
};
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
/** A JSON file `diffmap share` wrote, or `{}` when the gist predates it. */
|
|
140
|
+
async function readSnapshot(
|
|
141
|
+
apiFiles: Record<string, GistApiFile>,
|
|
142
|
+
name: string,
|
|
143
|
+
signal?: AbortSignal,
|
|
144
|
+
): Promise<unknown> {
|
|
145
|
+
const file = apiFiles[name];
|
|
146
|
+
if (file === undefined) return {};
|
|
147
|
+
const text = await readGistFile(file, signal);
|
|
148
|
+
if (text instanceof Error) return text;
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(text) as unknown;
|
|
151
|
+
} catch {
|
|
152
|
+
return new Error(`${name} is not valid JSON.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
132
156
|
async function readGistFile(file: GistApiFile, signal?: AbortSignal) {
|
|
133
157
|
const name = file.filename ?? "file";
|
|
134
158
|
if (file.truncated !== true && typeof file.content === "string") {
|
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/parseViewer.test.ts
CHANGED
|
@@ -234,6 +234,59 @@ test("shareMarkdownFile does not call gh when mermaid is invalid", async () => {
|
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
test("shareMarkdownFile uploads the files excerpts and rev-less references read", async () => {
|
|
238
|
+
const spec = [
|
|
239
|
+
"# Shared",
|
|
240
|
+
"",
|
|
241
|
+
"See [[src/share.ts#shareMarkdownFile]].",
|
|
242
|
+
"",
|
|
243
|
+
"```1:3:src/changes.ts",
|
|
244
|
+
"```",
|
|
245
|
+
"",
|
|
246
|
+
].join("\n");
|
|
247
|
+
await withTempSpec(spec, async (filePath) => {
|
|
248
|
+
const dir = path.dirname(filePath);
|
|
249
|
+
const uploaded = path.join(dir, "uploaded");
|
|
250
|
+
await fs.mkdir(uploaded);
|
|
251
|
+
const gh = path.join(dir, "gh");
|
|
252
|
+
await fs.writeFile(
|
|
253
|
+
gh,
|
|
254
|
+
[
|
|
255
|
+
"#!/bin/sh",
|
|
256
|
+
'if [ "$1" = auth ]; then exit 0; fi',
|
|
257
|
+
`for arg; do case "$arg" in *.json) cp "$arg" ${JSON.stringify(uploaded)};; esac; done`,
|
|
258
|
+
"echo https://gist.github.com/0123456789abcdef0123456789abcdef",
|
|
259
|
+
].join("\n"),
|
|
260
|
+
{ mode: 0o755 },
|
|
261
|
+
);
|
|
262
|
+
const originalPath = process.env.PATH;
|
|
263
|
+
process.env.PATH = `${dir}${path.delimiter}${originalPath ?? ""}`;
|
|
264
|
+
try {
|
|
265
|
+
const shared = await shareMarkdownFile({
|
|
266
|
+
workspaceRoot: packageRoot,
|
|
267
|
+
filePath,
|
|
268
|
+
isPublic: false,
|
|
269
|
+
});
|
|
270
|
+
assert.equal(shared instanceof Error, false, String(shared));
|
|
271
|
+
} finally {
|
|
272
|
+
process.env.PATH = originalPath;
|
|
273
|
+
}
|
|
274
|
+
const files: unknown = JSON.parse(
|
|
275
|
+
await fs.readFile(path.join(uploaded, "diffmap-files.json"), "utf8"),
|
|
276
|
+
);
|
|
277
|
+
assert.deepEqual(files, {
|
|
278
|
+
"src/share.ts": await fs.readFile(
|
|
279
|
+
path.join(packageRoot, "src/share.ts"),
|
|
280
|
+
"utf8",
|
|
281
|
+
),
|
|
282
|
+
"src/changes.ts": await fs.readFile(
|
|
283
|
+
path.join(packageRoot, "src/changes.ts"),
|
|
284
|
+
"utf8",
|
|
285
|
+
),
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
237
290
|
function mermaidCliMessage(filePath: string) {
|
|
238
291
|
return `diffmap could not parse mermaid in ${filePath} (diagram 1, line 3): Invalid mermaid header: "flwochart TD". Expected "graph TD", "flowchart LR", "stateDiagram-v2", etc.`;
|
|
239
292
|
}
|
package/src/parseViewer.ts
CHANGED
|
@@ -168,7 +168,7 @@ export function parseViewerDocument(source: string, path = "spec") {
|
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
function collectFences(nodes: ViewerNode[]): Fence[] {
|
|
171
|
+
export function collectFences(nodes: ViewerNode[]): Fence[] {
|
|
172
172
|
return nodes.flatMap((node) => {
|
|
173
173
|
if (node.type === "view") return [node.fence];
|
|
174
174
|
if (node.type === "element") return collectFences(node.children);
|
package/src/share.ts
CHANGED
|
@@ -6,9 +6,14 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import { DiffmapFileError, DiffmapShareError } from "./errors.js";
|
|
7
7
|
import { extractTitle } from "./extractDocument.js";
|
|
8
8
|
import { extractGistId, gistViewerUrl } from "./gist/route.ts";
|
|
9
|
-
import { GIST_CHANGES_FILE } from "./changes.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { GIST_CHANGES_FILE, GIST_FILES_FILE } from "./changes.js";
|
|
10
|
+
import { readSourceReference } from "./definitions.js";
|
|
11
|
+
import { readChanges, readDocumentChanges } from "./git.js";
|
|
12
|
+
import {
|
|
13
|
+
collectFences,
|
|
14
|
+
parseViewerDocument,
|
|
15
|
+
type ViewerDocument,
|
|
16
|
+
} from "./parseViewer.js";
|
|
12
17
|
|
|
13
18
|
const execFileAsync = promisify(execFile);
|
|
14
19
|
|
|
@@ -39,6 +44,16 @@ export async function shareMarkdownFile(input: {
|
|
|
39
44
|
// The hosted viewer has no git, so the gist carries a snapshot of each commit.
|
|
40
45
|
const changes = await readDocumentChanges(input.workspaceRoot, parsed);
|
|
41
46
|
if (changes instanceof Error) return changes;
|
|
47
|
+
// An uncommitted changes card shows the tree as it is when you share.
|
|
48
|
+
for (const rev of parsed.changeRevs) {
|
|
49
|
+
if (changes.has(rev)) continue;
|
|
50
|
+
const loaded = await readChanges(input.workspaceRoot, rev);
|
|
51
|
+
if (loaded instanceof Error) return loaded;
|
|
52
|
+
changes.set(rev, loaded);
|
|
53
|
+
}
|
|
54
|
+
// It has no files either, so excerpts and references without a commit read these.
|
|
55
|
+
const files = readDocumentFiles(input.workspaceRoot, parsed);
|
|
56
|
+
if (files instanceof Error) return files;
|
|
42
57
|
|
|
43
58
|
const auth = await runGh(["auth", "status"]);
|
|
44
59
|
if (auth instanceof DiffmapShareError) return auth;
|
|
@@ -46,14 +61,20 @@ export async function shareMarkdownFile(input: {
|
|
|
46
61
|
const args = ["gist", "create"];
|
|
47
62
|
if (input.isPublic) args.push("--public");
|
|
48
63
|
args.push("--desc", extractTitle(source), filePath);
|
|
64
|
+
const snapshots = [
|
|
65
|
+
{ name: GIST_CHANGES_FILE, value: Object.fromEntries(changes) },
|
|
66
|
+
{ name: GIST_FILES_FILE, value: files },
|
|
67
|
+
].filter((snapshot) => Object.keys(snapshot.value).length > 0);
|
|
49
68
|
const snapshotDir =
|
|
50
|
-
|
|
69
|
+
snapshots.length === 0
|
|
51
70
|
? undefined
|
|
52
71
|
: await fs.mkdtemp(path.join(os.tmpdir(), "diffmap-share-"));
|
|
53
72
|
if (snapshotDir !== undefined) {
|
|
54
|
-
const snapshot
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
for (const snapshot of snapshots) {
|
|
74
|
+
const snapshotPath = path.join(snapshotDir, snapshot.name);
|
|
75
|
+
await fs.writeFile(snapshotPath, JSON.stringify(snapshot.value));
|
|
76
|
+
args.push(snapshotPath);
|
|
77
|
+
}
|
|
57
78
|
}
|
|
58
79
|
|
|
59
80
|
const created = await runGh(args);
|
|
@@ -81,6 +102,28 @@ export async function shareMarkdownFile(input: {
|
|
|
81
102
|
};
|
|
82
103
|
}
|
|
83
104
|
|
|
105
|
+
/** Current contents of every file an excerpt or a reference without a commit reads, by path. */
|
|
106
|
+
function readDocumentFiles(root: string, document: ViewerDocument) {
|
|
107
|
+
const paths = new Set<string>();
|
|
108
|
+
for (const reference of document.references) {
|
|
109
|
+
if (reference.kind === "file" && reference.rev === undefined)
|
|
110
|
+
paths.add(reference.path);
|
|
111
|
+
}
|
|
112
|
+
for (const fence of collectFences(document.nodes)) {
|
|
113
|
+
if (fence.kind === "file") paths.add(fence.path);
|
|
114
|
+
}
|
|
115
|
+
const files: Record<string, string> = {};
|
|
116
|
+
for (const filePath of paths) {
|
|
117
|
+
const read = readSourceReference(
|
|
118
|
+
root,
|
|
119
|
+
new URLSearchParams({ path: filePath }),
|
|
120
|
+
);
|
|
121
|
+
if (read instanceof Error) return read;
|
|
122
|
+
files[filePath] = read.contents;
|
|
123
|
+
}
|
|
124
|
+
return files;
|
|
125
|
+
}
|
|
126
|
+
|
|
84
127
|
async function runGh(args: string[]) {
|
|
85
128
|
try {
|
|
86
129
|
return await execFileAsync("gh", args, {
|
|
@@ -112,7 +112,8 @@ export function SourceDiffPanel(props: {
|
|
|
112
112
|
? navigation.status
|
|
113
113
|
: source instanceof Error
|
|
114
114
|
? source.message
|
|
115
|
-
: changes
|
|
115
|
+
: // A shown file replaces the changes, so their error is moot, as in a gist without uncommitted changes.
|
|
116
|
+
changes instanceof Error && file === undefined
|
|
116
117
|
? changes.message
|
|
117
118
|
: changes === undefined
|
|
118
119
|
? "Loading changes…"
|