@tanishqkancharla/diffmap 0.1.4 → 0.2.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.
Files changed (53) hide show
  1. package/README.md +69 -61
  2. package/index.html +18 -0
  3. package/package.json +7 -5
  4. package/src/annotations.ts +36 -29
  5. package/src/changes.ts +229 -0
  6. package/src/cli.test.ts +50 -0
  7. package/src/cli.ts +41 -5
  8. package/src/errors.ts +10 -0
  9. package/src/gist/GistApp.tsx +39 -20
  10. package/src/gist/GistStatus.tsx +2 -51
  11. package/src/gist/fetchGist.ts +23 -0
  12. package/src/git.test.ts +402 -0
  13. package/src/git.ts +498 -0
  14. package/src/github/GitHubApp.tsx +39 -22
  15. package/src/github/fetchRepo.test.ts +59 -1
  16. package/src/github/fetchRepo.ts +99 -43
  17. package/src/github/patch.test.ts +18 -9
  18. package/src/github/patch.ts +30 -19
  19. package/src/host/HostHome.tsx +86 -0
  20. package/src/host/files.ts +10 -0
  21. package/src/host/home.md +156 -0
  22. package/src/host/home.test.ts +83 -0
  23. package/src/index.ts +1 -0
  24. package/src/packageVersion.ts +7 -0
  25. package/src/parseFence.ts +6 -6
  26. package/src/parseViewer.test.ts +87 -0
  27. package/src/parseViewer.ts +103 -77
  28. package/src/serve.test.ts +306 -0
  29. package/src/serve.ts +48 -5
  30. package/src/share.ts +18 -0
  31. package/src/update.test.ts +19 -0
  32. package/src/update.ts +285 -0
  33. package/src/viewer/CallStackDiff.tsx +15 -3
  34. package/src/viewer/ChangesCard.tsx +267 -0
  35. package/src/viewer/CloseServerButton.tsx +16 -0
  36. package/src/viewer/ComarkView.tsx +83 -15
  37. package/src/viewer/Fence.tsx +9 -1
  38. package/src/viewer/FileExcerpt.tsx +25 -3
  39. package/src/viewer/HtmlPlaceholder.tsx +6 -1
  40. package/src/viewer/MermaidBlock.tsx +99 -11
  41. package/src/viewer/RangePicker.tsx +526 -0
  42. package/src/viewer/SourceDiffPanel.tsx +277 -121
  43. package/src/viewer/TableOfContents.tsx +313 -39
  44. package/src/viewer/ViewerApp.tsx +120 -68
  45. package/src/viewer/changesSource.ts +91 -0
  46. package/src/viewer/sourceHeader.test.ts +2 -87
  47. package/src/viewer/sourceHeader.ts +4 -63
  48. package/src/viewer/sourceNavigation.ts +33 -42
  49. package/src/viewer/viewerMode.ts +1 -1
  50. package/vite.config.ts +2 -1
  51. package/vite.gist.config.ts +57 -0
  52. package/src/viewer/Check.tsx +0 -24
  53. package/src/viewer/DoneButton.tsx +0 -32
package/src/cli.ts CHANGED
@@ -2,16 +2,25 @@
2
2
 
3
3
  import { Cli, z } from "incur";
4
4
  import { resolveFromInvokeCwd } from "./invokeCwd.js";
5
+ import { packageVersion } from "./packageVersion.js";
5
6
  import { listRunningDiffmaps } from "./registry.js";
6
7
  import { startServer } from "./serve.js";
7
8
  import { shareMarkdownFile } from "./share.js";
9
+ import {
10
+ detectInstallMethod,
11
+ fetchLatestVersion,
12
+ packageName,
13
+ resolveUpdateArgv,
14
+ runCommand,
15
+ runUpdate,
16
+ } from "./update.js";
8
17
 
9
18
  const serveArgs = z.object({
10
19
  file: z.string().describe("Path to the markdown file"),
11
20
  });
12
21
 
13
22
  const serveOptions = z.object({
14
- port: z.coerce.number().optional().describe("Port (default 4177)"),
23
+ port: z.coerce.number().optional().describe("Port (default: a free port)"),
15
24
  root: z.string().optional().describe("Workspace root for file excerpts"),
16
25
  });
17
26
 
@@ -33,7 +42,7 @@ async function* runServe(c: ServeContext) {
33
42
  c.options.root === undefined
34
43
  ? resolveFromInvokeCwd(".")
35
44
  : resolveFromInvokeCwd(c.options.root),
36
- port: c.options.port === undefined ? 4177 : c.options.port,
45
+ port: c.options.port,
37
46
  });
38
47
  if (started instanceof Error) {
39
48
  return c.error({
@@ -48,11 +57,11 @@ async function* runServe(c: ServeContext) {
48
57
 
49
58
  const cli = Cli.create("diffmap", {
50
59
  description: "Serve a spec locally, or share it as a gist",
51
- version: "0.1.3",
60
+ version: packageVersion,
52
61
  args: serveArgs,
53
62
  options: serveOptions,
54
63
  output: serveOutput,
55
- hint: "`serve <spec.md>` runs the local viewer. `share <spec.md>` publishes a secret gist.",
64
+ hint: "`serve <spec.md>` runs the local viewer. `share <spec.md>` publishes a secret gist. `update` (or `--update`) installs the latest diffmap.",
56
65
  examples: [
57
66
  {
58
67
  args: { file: "spec.md" },
@@ -96,6 +105,7 @@ const cli = Cli.create("diffmap", {
96
105
  async run(c) {
97
106
  const shared = await shareMarkdownFile({
98
107
  filePath: resolveFromInvokeCwd(c.args.file),
108
+ workspaceRoot: resolveFromInvokeCwd("."),
99
109
  isPublic: c.options.public === true,
100
110
  });
101
111
  if (shared instanceof Error) {
@@ -129,6 +139,32 @@ const cli = Cli.create("diffmap", {
129
139
  }
130
140
  return c.ok({ instances });
131
141
  },
142
+ })
143
+ .command("update", {
144
+ description: `Update diffmap to the latest ${packageName} on npm (same as --update)`,
145
+ output: z.object({
146
+ status: z.enum(["up-to-date", "updated", "manual"]),
147
+ message: z.string(),
148
+ current: z.string(),
149
+ previous: z.string().optional(),
150
+ latest: z.string().optional(),
151
+ command: z.string().optional(),
152
+ }),
153
+ async run(c) {
154
+ const result = await runUpdate({
155
+ currentVersion: packageVersion,
156
+ fetchLatestVersion,
157
+ detectInstallMethod,
158
+ runCommand,
159
+ });
160
+ if (result instanceof Error) {
161
+ return c.error({
162
+ code: "UPDATE_FAILED",
163
+ message: result.message,
164
+ });
165
+ }
166
+ return c.ok(result);
167
+ },
132
168
  });
133
169
 
134
- await cli.serve();
170
+ await cli.serve(resolveUpdateArgv(process.argv.slice(2)));
package/src/errors.ts CHANGED
@@ -30,6 +30,11 @@ export class DiffmapAnnotationError extends errore.createTaggedError({
30
30
  message: "diffmap source annotation: $reason",
31
31
  }) {}
32
32
 
33
+ export class DiffmapGitError extends errore.createTaggedError({
34
+ name: "DiffmapGitError",
35
+ message: "$reason",
36
+ }) {}
37
+
33
38
  export class DiffmapDefinitionError extends errore.createTaggedError({
34
39
  name: "DiffmapDefinitionError",
35
40
  message: "$reason",
@@ -39,3 +44,8 @@ export class DiffmapShareError extends errore.createTaggedError({
39
44
  name: "DiffmapShareError",
40
45
  message: "diffmap share failed: $reason",
41
46
  }) {}
47
+
48
+ export class DiffmapUpdateError extends errore.createTaggedError({
49
+ name: "DiffmapUpdateError",
50
+ message: "diffmap update failed: $reason",
51
+ }) {}
@@ -4,10 +4,16 @@ import { style, useStyles } from "purse-styles";
4
4
  import { init } from "md4x/standalone";
5
5
  import { parseViewerDocument } from "../parseViewer.ts";
6
6
  import type { ViewerDocument } from "../parseViewer.ts";
7
+ import { WORKING } from "../changes.ts";
8
+ import {
9
+ ChangesLoaderContext,
10
+ type ChangesLoader,
11
+ } from "../viewer/changesSource.ts";
7
12
  import { ViewerApp } from "../viewer/ViewerApp.tsx";
8
13
  import { loadGistMarkdown, type GistDocument } from "./fetchGist.ts";
9
14
  import { GistPinContext } from "./pin.ts";
10
- import { GistLanding, GistLoading, GistStatus } from "./GistStatus.tsx";
15
+ import { HostHome } from "../host/HostHome.tsx";
16
+ import { GistLoading, GistStatus } from "./GistStatus.tsx";
11
17
  import {
12
18
  gistPath,
13
19
  parseGistPath,
@@ -40,6 +46,7 @@ type GistView =
40
46
  gist: GistDocument;
41
47
  file: string;
42
48
  document: ViewerDocument;
49
+ loadChanges: ChangesLoader;
43
50
  };
44
51
 
45
52
  export function GistApp() {
@@ -83,7 +90,7 @@ export function GistApp() {
83
90
  return () => abort.abort();
84
91
  }, [route]);
85
92
 
86
- if (route.kind === "home") return <GistLanding />;
93
+ if (route.kind === "home") return <HostHome />;
87
94
  if (route.kind === "invalid") {
88
95
  return (
89
96
  <GistStatus title="Not a gist id">
@@ -159,24 +166,26 @@ export function GistApp() {
159
166
  htmlUrl: view.gist.htmlUrl,
160
167
  }}
161
168
  >
162
- <ViewerApp
163
- key={`${view.gist.id}:${view.file}`}
164
- document={view.document}
165
- mode="gist"
166
- headerActions={
167
- <GistHeader
168
- gistId={view.gist.id}
169
- htmlUrl={view.gist.htmlUrl}
170
- files={view.gist.files}
171
- current={view.file}
172
- onSelect={(file) => {
173
- const next = gistPath(view.gist.id, file);
174
- window.history.pushState({}, "", next);
175
- setRoute(parseGistPath(next));
176
- }}
177
- />
178
- }
179
- />
169
+ <ChangesLoaderContext.Provider value={view.loadChanges}>
170
+ <ViewerApp
171
+ key={`${view.gist.id}:${view.file}`}
172
+ document={view.document}
173
+ mode="gist"
174
+ headerActions={
175
+ <GistHeader
176
+ gistId={view.gist.id}
177
+ htmlUrl={view.gist.htmlUrl}
178
+ files={view.gist.files}
179
+ current={view.file}
180
+ onSelect={(file) => {
181
+ const next = gistPath(view.gist.id, file);
182
+ window.history.pushState({}, "", next);
183
+ setRoute(parseGistPath(next));
184
+ }}
185
+ />
186
+ }
187
+ />
188
+ </ChangesLoaderContext.Provider>
180
189
  </GistPinContext.Provider>
181
190
  );
182
191
  }
@@ -217,11 +226,21 @@ async function loadView(
217
226
  message: parsed.message,
218
227
  };
219
228
  }
229
+ const snapshot = loaded.gist.changes;
220
230
  return {
221
231
  kind: "ready",
222
232
  gist: loaded.gist,
223
233
  file: loaded.gist.chosen.name,
224
234
  document: parsed,
235
+ loadChanges: (rev) =>
236
+ Promise.resolve(
237
+ snapshot[rev] ??
238
+ new Error(
239
+ rev === WORKING
240
+ ? "Uncommitted changes are only available in a local viewer."
241
+ : `This gist has no snapshot of ${rev}. Share the spec again with diffmap share.`,
242
+ ),
243
+ ),
225
244
  };
226
245
  }
227
246
 
@@ -61,52 +61,6 @@ export function GistLoading(props: { title?: string }) {
61
61
  );
62
62
  }
63
63
 
64
- export function GistLanding() {
65
- const code = useStyles(styles.code);
66
- return (
67
- <GistStatus title="diffmap">
68
- <P>
69
- A hosted viewer for diffmap walkthroughs. This page fetches a GitHub
70
- gist or a public repo file in your browser. There is no server holding
71
- the markdown.
72
- </P>
73
- <P>
74
- Canonical gist URL:{" "}
75
- <code className={code}>https://diffmap.dev/g/&lt;gistId&gt;</code>.
76
- Optional file:{" "}
77
- <code className={code}>/g/&lt;gistId&gt;/&lt;file.md&gt;</code>. GitHub
78
- spec:{" "}
79
- <code className={code}>
80
- https://diffmap.dev/&lt;owner&gt;/&lt;repo&gt;/pull/&lt;n&gt;/&lt;path.md&gt;
81
- </code>{" "}
82
- or{" "}
83
- <code className={code}>
84
- /&lt;owner&gt;/&lt;repo&gt;/commit/&lt;sha&gt;/&lt;path.md&gt;
85
- </code>
86
- . Heading hashes stay in the fragment, same as the local viewer.
87
- </P>
88
- <P>
89
- Share a secret gist (requires <code className={code}>gh</code>
90
- ):
91
- </P>
92
- <P>
93
- <code className={code}>
94
- npx @tanishqkancharla/diffmap share path/to.md
95
- </code>
96
- </P>
97
- <P>
98
- Secret gists are unlisted, not private. Anyone with the id can read
99
- them. Unauthenticated GitHub API is 60 requests per hour per IP.
100
- </P>
101
- <P>
102
- <Link href="https://github.com/tanishqkancharla/diffmap">
103
- tanishqkancharla/diffmap
104
- </Link>
105
- </P>
106
- </GistStatus>
107
- );
108
- }
109
-
110
64
  const styles = {
111
65
  shell: style(flex({ direction: "column" }), {
112
66
  width: "100%",
@@ -117,7 +71,7 @@ const styles = {
117
71
  backgroundColor: colors.gray[4],
118
72
  }),
119
73
  header: style(
120
- flex({ direction: "row", align: "center", justify: "between" }),
74
+ flex({ direction: "row", alignItems: "center", justifyContent: "between" }),
121
75
  spacing.padding({ x: 6, y: 3 }),
122
76
  flexItem({ size: "hug" }),
123
77
  border(["bottom"], "border"),
@@ -142,10 +96,7 @@ const styles = {
142
96
  title: style(text({ size: "xl", fontWeight: 600, color: "highContrast" }), {
143
97
  marginBottom: spacing.value(4),
144
98
  }),
145
- loading: style(flex({ direction: "row", align: "center" }), {
99
+ loading: style(flex({ direction: "row", alignItems: "center" }), {
146
100
  minHeight: spacing.value(12),
147
101
  }),
148
- code: style(text({ size: "sm", color: "highContrast" }), {
149
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
150
- }),
151
102
  };
@@ -1,3 +1,4 @@
1
+ import { GIST_CHANGES_FILE, type Changes, type Rev } from "../changes.ts";
1
2
  import { githubGistUrl, markdownFileNames, pickMarkdownFile } from "./route.ts";
2
3
 
3
4
  export type GistMarkdownFile = {
@@ -10,6 +11,8 @@ export type GistDocument = {
10
11
  htmlUrl: string;
11
12
  files: GistMarkdownFile[];
12
13
  chosen: GistMarkdownFile;
14
+ /** Snapshot `diffmap share` saved for each commit the spec references. */
15
+ changes: Record<Rev, Changes>;
13
16
  };
14
17
 
15
18
  export type GistLoadResult =
@@ -95,6 +98,25 @@ export async function loadGistMarkdown(
95
98
  return { status: "missing-file", htmlUrl, files };
96
99
  }
97
100
 
101
+ const snapshotFile = apiFiles[GIST_CHANGES_FILE];
102
+ const snapshot =
103
+ snapshotFile === undefined
104
+ ? "{}"
105
+ : await readGistFile(snapshotFile, signal);
106
+ if (snapshot instanceof Error) {
107
+ return { status: "error", message: snapshot.message };
108
+ }
109
+ let changes: Record<Rev, Changes>;
110
+ try {
111
+ // SAFETY: diffmap share writes this file from readDocumentChanges.
112
+ changes = JSON.parse(snapshot) as Record<Rev, Changes>;
113
+ } catch {
114
+ return {
115
+ status: "error",
116
+ message: `${GIST_CHANGES_FILE} is not valid JSON.`,
117
+ };
118
+ }
119
+
98
120
  return {
99
121
  status: "ok",
100
122
  gist: {
@@ -102,6 +124,7 @@ export async function loadGistMarkdown(
102
124
  htmlUrl,
103
125
  files,
104
126
  chosen,
127
+ changes,
105
128
  },
106
129
  };
107
130
  }