@tanishqkancharla/diffmap 0.1.3
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/LICENSE +21 -0
- package/README.md +212 -0
- package/bin.js +5 -0
- package/index.html +38 -0
- package/package.json +71 -0
- package/src/annotations.ts +82 -0
- package/src/cli.ts +94 -0
- package/src/codeFont.ts +2 -0
- package/src/contentPlugin.ts +47 -0
- package/src/definitions.ts +320 -0
- package/src/diagramAnnotations.ts +34 -0
- package/src/errors.ts +41 -0
- package/src/extractDocument.ts +20 -0
- package/src/gist/GistApp.tsx +292 -0
- package/src/gist/GistStatus.tsx +137 -0
- package/src/gist/fetchGist.ts +128 -0
- package/src/gist/main.tsx +25 -0
- package/src/gist/route.test.ts +90 -0
- package/src/gist/route.ts +134 -0
- package/src/index.ts +22 -0
- package/src/invokeCwd.ts +10 -0
- package/src/mermaid.ts +29 -0
- package/src/normalizeDiff.ts +39 -0
- package/src/parseFence.ts +91 -0
- package/src/parseViewer.ts +399 -0
- package/src/registry.ts +121 -0
- package/src/serve.ts +350 -0
- package/src/share.ts +94 -0
- package/src/viewer/CallStackDiff.tsx +156 -0
- package/src/viewer/Check.tsx +24 -0
- package/src/viewer/CodeDiff.tsx +26 -0
- package/src/viewer/ComarkView.tsx +209 -0
- package/src/viewer/DiffButton.tsx +24 -0
- package/src/viewer/DoneButton.tsx +32 -0
- package/src/viewer/Fence.tsx +51 -0
- package/src/viewer/FileExcerpt.tsx +65 -0
- package/src/viewer/HtmlBlock.tsx +19 -0
- package/src/viewer/HtmlPlaceholder.tsx +29 -0
- package/src/viewer/MermaidBlock.tsx +126 -0
- package/src/viewer/SourceDiffPanel.tsx +305 -0
- package/src/viewer/TableOfContents.tsx +211 -0
- package/src/viewer/ViewerApp.tsx +257 -0
- package/src/viewer/diagramLinks.ts +74 -0
- package/src/viewer/diffsTheme.ts +4 -0
- package/src/viewer/main.tsx +17 -0
- package/src/viewer/pierre.ts +47 -0
- package/src/viewer/sourceNavigation.ts +104 -0
- package/src/viewer/styles.css +9 -0
- package/src/viewer/viewerMode.ts +9 -0
- package/src/viewer/vite-env.d.ts +4 -0
- package/vite.config.ts +41 -0
package/src/serve.ts
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createServer, type ViteDevServer } from "vite";
|
|
5
|
+
import { diffmapContentPlugin } from "./contentPlugin.js";
|
|
6
|
+
import { DiffmapFileError, DiffmapServeError } from "./errors.js";
|
|
7
|
+
import { extractTitle } from "./extractDocument.js";
|
|
8
|
+
import { findDefinition, readSourceReference } from "./definitions.js";
|
|
9
|
+
import {
|
|
10
|
+
registerRunningDiffmap,
|
|
11
|
+
unregisterRunningDiffmap,
|
|
12
|
+
} from "./registry.js";
|
|
13
|
+
|
|
14
|
+
export type DiffmapServer = {
|
|
15
|
+
url: string;
|
|
16
|
+
filePath: string;
|
|
17
|
+
shutdown: () => Promise<void>;
|
|
18
|
+
closed: Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type StartServerInput = {
|
|
22
|
+
filePath: string;
|
|
23
|
+
workspaceRoot: string;
|
|
24
|
+
port: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const packageRoot = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
28
|
+
|
|
29
|
+
function noop() {}
|
|
30
|
+
|
|
31
|
+
function createClosedBarrier() {
|
|
32
|
+
let resolveFn = noop;
|
|
33
|
+
const closed = new Promise<void>((resolve) => {
|
|
34
|
+
resolveFn = resolve;
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
closed,
|
|
38
|
+
resolve() {
|
|
39
|
+
resolveFn();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function startServer(input: StartServerInput) {
|
|
45
|
+
const filePath = path.resolve(input.filePath);
|
|
46
|
+
const workspaceRoot = path.resolve(input.workspaceRoot);
|
|
47
|
+
const source = await fs.readFile(filePath, "utf8").catch(
|
|
48
|
+
(cause) =>
|
|
49
|
+
new DiffmapFileError({
|
|
50
|
+
path: filePath,
|
|
51
|
+
reason: "read",
|
|
52
|
+
cause,
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
if (source instanceof Error) return source;
|
|
56
|
+
|
|
57
|
+
const title = extractTitle(source);
|
|
58
|
+
|
|
59
|
+
let vite: ViteDevServer | undefined;
|
|
60
|
+
let registryPath: string | undefined;
|
|
61
|
+
const closedBarrier = createClosedBarrier();
|
|
62
|
+
let shuttingDown = false;
|
|
63
|
+
let inactivityTimer: ReturnType<typeof setTimeout> | undefined;
|
|
64
|
+
|
|
65
|
+
async function shutdown() {
|
|
66
|
+
if (shuttingDown) return;
|
|
67
|
+
shuttingDown = true;
|
|
68
|
+
clearTimeout(inactivityTimer);
|
|
69
|
+
if (vite !== undefined) await vite.close();
|
|
70
|
+
if (registryPath !== undefined) {
|
|
71
|
+
const removed = await unregisterRunningDiffmap(registryPath);
|
|
72
|
+
if (removed instanceof Error) console.error(removed.message);
|
|
73
|
+
}
|
|
74
|
+
closedBarrier.resolve();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
vite = await createServer({
|
|
78
|
+
configFile: path.join(packageRoot, "vite.config.ts"),
|
|
79
|
+
root: packageRoot,
|
|
80
|
+
server: {
|
|
81
|
+
port: input.port,
|
|
82
|
+
host: "127.0.0.1",
|
|
83
|
+
strictPort: input.port !== 0,
|
|
84
|
+
fs: {
|
|
85
|
+
allow: [packageRoot, workspaceRoot, path.dirname(filePath)],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
plugins: [
|
|
89
|
+
diffmapContentPlugin({ filePath, title }),
|
|
90
|
+
{
|
|
91
|
+
name: "diffmap-api",
|
|
92
|
+
configureServer(server) {
|
|
93
|
+
server.middlewares.use((req, res, next) => {
|
|
94
|
+
const url = req.url;
|
|
95
|
+
const pathname =
|
|
96
|
+
url === undefined
|
|
97
|
+
? undefined
|
|
98
|
+
: new URL(url, "http://127.0.0.1").pathname;
|
|
99
|
+
if (
|
|
100
|
+
req.method === "GET" &&
|
|
101
|
+
(pathname === "/" ||
|
|
102
|
+
pathname === "/__diffmap/file" ||
|
|
103
|
+
pathname === "/__diffmap/definition" ||
|
|
104
|
+
pathname === "/__diffmap/source")
|
|
105
|
+
) {
|
|
106
|
+
inactivityTimer?.refresh();
|
|
107
|
+
}
|
|
108
|
+
if (
|
|
109
|
+
req.method === "GET" &&
|
|
110
|
+
pathname === "/" &&
|
|
111
|
+
acceptsMarkdown(req.headers.accept)
|
|
112
|
+
) {
|
|
113
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- Connect middleware callbacks cannot await response handling.
|
|
114
|
+
void serveMarkdown({ filePath, res });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (url === undefined || !url.startsWith("/__diffmap")) {
|
|
118
|
+
next();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- Connect middleware callbacks cannot await response handling.
|
|
122
|
+
void handleDiffmapRequest({
|
|
123
|
+
url,
|
|
124
|
+
method: req.method === undefined ? "GET" : req.method,
|
|
125
|
+
workspaceRoot,
|
|
126
|
+
title,
|
|
127
|
+
filePath,
|
|
128
|
+
shutdown,
|
|
129
|
+
res,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await vite.listen(input.port);
|
|
138
|
+
const localUrl = vite.resolvedUrls?.local[0];
|
|
139
|
+
if (localUrl === undefined) {
|
|
140
|
+
await shutdown();
|
|
141
|
+
return new DiffmapServeError({ reason: "no listen url" });
|
|
142
|
+
}
|
|
143
|
+
const url = localUrl.replace(/\/$/, "");
|
|
144
|
+
const registered = await registerRunningDiffmap({
|
|
145
|
+
pid: process.pid,
|
|
146
|
+
title,
|
|
147
|
+
url,
|
|
148
|
+
file: filePath,
|
|
149
|
+
});
|
|
150
|
+
if (registered instanceof Error) {
|
|
151
|
+
await shutdown();
|
|
152
|
+
return registered;
|
|
153
|
+
}
|
|
154
|
+
registryPath = registered;
|
|
155
|
+
inactivityTimer = setTimeout(
|
|
156
|
+
() => {
|
|
157
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- Timer callbacks cannot await shutdown.
|
|
158
|
+
void shutdown();
|
|
159
|
+
},
|
|
160
|
+
24 * 60 * 60 * 1_000,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
process.once("SIGINT", () => {
|
|
164
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- Process signal callbacks cannot await shutdown.
|
|
165
|
+
void shutdown();
|
|
166
|
+
});
|
|
167
|
+
process.once("SIGTERM", () => {
|
|
168
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- Process signal callbacks cannot await shutdown.
|
|
169
|
+
void shutdown();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
url,
|
|
174
|
+
filePath,
|
|
175
|
+
shutdown,
|
|
176
|
+
closed: closedBarrier.closed,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
type DiffmapResponse = {
|
|
181
|
+
statusCode: number;
|
|
182
|
+
setHeader: (name: string, value: string) => void;
|
|
183
|
+
end: (chunk: string) => void;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
function acceptsMarkdown(accept: string | undefined) {
|
|
187
|
+
if (accept === undefined) return false;
|
|
188
|
+
return accept
|
|
189
|
+
.split(",")
|
|
190
|
+
.some(
|
|
191
|
+
(mediaRange) =>
|
|
192
|
+
mediaRange.trim().split(";", 1)[0]?.trim() === "text/markdown",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function serveMarkdown(input: {
|
|
197
|
+
filePath: string;
|
|
198
|
+
res: DiffmapResponse;
|
|
199
|
+
}) {
|
|
200
|
+
const source = await fs.readFile(input.filePath, "utf8").catch(
|
|
201
|
+
(cause) =>
|
|
202
|
+
new DiffmapFileError({
|
|
203
|
+
path: input.filePath,
|
|
204
|
+
reason: "read",
|
|
205
|
+
cause,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
if (source instanceof Error) {
|
|
209
|
+
input.res.statusCode = 500;
|
|
210
|
+
input.res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
211
|
+
input.res.end(source.message);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
input.res.statusCode = 200;
|
|
215
|
+
input.res.setHeader("content-type", "text/markdown; charset=utf-8");
|
|
216
|
+
input.res.setHeader("vary", "Accept");
|
|
217
|
+
input.res.end(source);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function handleDiffmapRequest(input: {
|
|
221
|
+
url: string;
|
|
222
|
+
method: string;
|
|
223
|
+
workspaceRoot: string;
|
|
224
|
+
title: string;
|
|
225
|
+
filePath: string;
|
|
226
|
+
shutdown: () => Promise<void>;
|
|
227
|
+
res: DiffmapResponse;
|
|
228
|
+
}) {
|
|
229
|
+
const parsed = new URL(input.url, "http://127.0.0.1");
|
|
230
|
+
if (
|
|
231
|
+
(parsed.pathname === "/__diffmap/definition" ||
|
|
232
|
+
parsed.pathname === "/__diffmap/source") &&
|
|
233
|
+
input.method === "GET"
|
|
234
|
+
) {
|
|
235
|
+
const definition =
|
|
236
|
+
parsed.pathname === "/__diffmap/source"
|
|
237
|
+
? readSourceReference(input.workspaceRoot, parsed.searchParams)
|
|
238
|
+
: findDefinition(input.workspaceRoot, parsed.searchParams);
|
|
239
|
+
input.res.statusCode = definition instanceof Error ? 400 : 200;
|
|
240
|
+
input.res.setHeader("content-type", "application/json; charset=utf-8");
|
|
241
|
+
input.res.end(
|
|
242
|
+
JSON.stringify(
|
|
243
|
+
definition instanceof Error
|
|
244
|
+
? { error: definition.message }
|
|
245
|
+
: { definition },
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (parsed.pathname === "/__diffmap/shutdown" && input.method === "POST") {
|
|
251
|
+
input.res.statusCode = 200;
|
|
252
|
+
input.res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
253
|
+
input.res.end("ok");
|
|
254
|
+
setTimeout(() => {
|
|
255
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- The response must finish before the delayed shutdown begins.
|
|
256
|
+
void input.shutdown();
|
|
257
|
+
}, 250);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (parsed.pathname === "/__diffmap/meta" && input.method === "GET") {
|
|
261
|
+
input.res.statusCode = 200;
|
|
262
|
+
input.res.setHeader("content-type", "application/json; charset=utf-8");
|
|
263
|
+
input.res.end(
|
|
264
|
+
JSON.stringify({
|
|
265
|
+
title: input.title,
|
|
266
|
+
file: input.filePath,
|
|
267
|
+
pid: process.pid,
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (parsed.pathname === "/__diffmap/file" && input.method === "GET") {
|
|
273
|
+
const excerpt = await readWorkspaceExcerpt({
|
|
274
|
+
workspaceRoot: input.workspaceRoot,
|
|
275
|
+
requestedPath: parsed.searchParams.get("path"),
|
|
276
|
+
start: parsed.searchParams.get("start"),
|
|
277
|
+
end: parsed.searchParams.get("end"),
|
|
278
|
+
});
|
|
279
|
+
if (excerpt instanceof Error) {
|
|
280
|
+
input.res.statusCode = 400;
|
|
281
|
+
input.res.setHeader("content-type", "application/json; charset=utf-8");
|
|
282
|
+
input.res.end(JSON.stringify({ error: excerpt.message }));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
input.res.statusCode = 200;
|
|
286
|
+
input.res.setHeader("content-type", "application/json; charset=utf-8");
|
|
287
|
+
input.res.end(JSON.stringify(excerpt));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
input.res.statusCode = 404;
|
|
291
|
+
input.res.end("not found");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export type FileExcerpt = {
|
|
295
|
+
path: string;
|
|
296
|
+
start: number;
|
|
297
|
+
end: number;
|
|
298
|
+
contents: string;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
async function readWorkspaceExcerpt(input: {
|
|
302
|
+
workspaceRoot: string;
|
|
303
|
+
requestedPath: string | null;
|
|
304
|
+
start: string | null;
|
|
305
|
+
end: string | null;
|
|
306
|
+
}) {
|
|
307
|
+
if (input.requestedPath === null || input.requestedPath.length === 0) {
|
|
308
|
+
return new DiffmapFileError({
|
|
309
|
+
path: "",
|
|
310
|
+
reason: "missing path",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const requestedPath = input.requestedPath;
|
|
314
|
+
const resolvedRoot = path.resolve(input.workspaceRoot);
|
|
315
|
+
const resolved = path.resolve(resolvedRoot, requestedPath);
|
|
316
|
+
const prefix = resolvedRoot.endsWith(path.sep)
|
|
317
|
+
? resolvedRoot
|
|
318
|
+
: resolvedRoot + path.sep;
|
|
319
|
+
if (resolved !== resolvedRoot && !resolved.startsWith(prefix)) {
|
|
320
|
+
return new DiffmapFileError({
|
|
321
|
+
path: requestedPath,
|
|
322
|
+
reason: "path escapes workspace",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const contents = await fs.readFile(resolved, "utf8").catch(
|
|
326
|
+
(cause) =>
|
|
327
|
+
new DiffmapFileError({
|
|
328
|
+
path: requestedPath,
|
|
329
|
+
reason: "read",
|
|
330
|
+
cause,
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
if (contents instanceof Error) return contents;
|
|
334
|
+
const lines = contents.split("\n");
|
|
335
|
+
const start = parseLine(input.start, 1);
|
|
336
|
+
const end = parseLine(input.end, lines.length);
|
|
337
|
+
return {
|
|
338
|
+
path: requestedPath,
|
|
339
|
+
start,
|
|
340
|
+
end,
|
|
341
|
+
contents: lines.slice(start - 1, end).join("\n"),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseLine(value: string | null, fallback: number) {
|
|
346
|
+
if (value === null || value.length === 0) return fallback;
|
|
347
|
+
const parsed = Number(value);
|
|
348
|
+
if (!Number.isInteger(parsed) || parsed < 1) return fallback;
|
|
349
|
+
return parsed;
|
|
350
|
+
}
|
package/src/share.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { DiffmapFileError, DiffmapShareError } from "./errors.js";
|
|
6
|
+
import { extractTitle } from "./extractDocument.js";
|
|
7
|
+
import { extractGistId, gistViewerUrl } from "./gist/route.ts";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
export type ShareResult = {
|
|
12
|
+
gistId: string;
|
|
13
|
+
gistUrl: string;
|
|
14
|
+
viewerUrl: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function shareMarkdownFile(input: {
|
|
18
|
+
filePath: string;
|
|
19
|
+
isPublic: boolean;
|
|
20
|
+
}): Promise<ShareResult | DiffmapFileError | DiffmapShareError> {
|
|
21
|
+
const filePath = path.resolve(input.filePath);
|
|
22
|
+
const source = await fs.readFile(filePath, "utf8").catch(
|
|
23
|
+
(cause) =>
|
|
24
|
+
new DiffmapFileError({
|
|
25
|
+
path: filePath,
|
|
26
|
+
reason: "read",
|
|
27
|
+
cause,
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
if (source instanceof Error) return source;
|
|
31
|
+
|
|
32
|
+
const auth = await runGh(["auth", "status"]);
|
|
33
|
+
if (auth instanceof DiffmapShareError) return auth;
|
|
34
|
+
|
|
35
|
+
const args = ["gist", "create"];
|
|
36
|
+
if (input.isPublic) args.push("--public");
|
|
37
|
+
args.push("--desc", extractTitle(source), filePath);
|
|
38
|
+
|
|
39
|
+
const created = await runGh(args);
|
|
40
|
+
if (created instanceof DiffmapShareError) return created;
|
|
41
|
+
|
|
42
|
+
const combined = `${created.stderr}\n${created.stdout}`;
|
|
43
|
+
const gistUrl = combined.match(/https:\/\/gist\.github\.com\/[^\s]+/)?.[0];
|
|
44
|
+
if (gistUrl === undefined) {
|
|
45
|
+
return new DiffmapShareError({
|
|
46
|
+
reason: "gh did not print a gist URL",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const gistId = extractGistId(gistUrl);
|
|
50
|
+
if (gistId === undefined) {
|
|
51
|
+
return new DiffmapShareError({
|
|
52
|
+
reason: `could not parse gist id from ${gistUrl}`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
gistId,
|
|
57
|
+
gistUrl,
|
|
58
|
+
viewerUrl: gistViewerUrl(gistId, undefined, process.env.DIFFMAP_ORIGIN),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function runGh(args: string[]) {
|
|
63
|
+
try {
|
|
64
|
+
return await execFileAsync("gh", args, {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
maxBuffer: 10_000_000,
|
|
67
|
+
});
|
|
68
|
+
} catch (cause) {
|
|
69
|
+
if (isErrnoException(cause) && cause.code === "ENOENT") {
|
|
70
|
+
return new DiffmapShareError({
|
|
71
|
+
reason:
|
|
72
|
+
"gh is not installed. Install GitHub CLI and run gh auth login.",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const stderr = execStderr(cause);
|
|
76
|
+
if (stderr.length > 0) {
|
|
77
|
+
return new DiffmapShareError({ reason: stderr.trim() });
|
|
78
|
+
}
|
|
79
|
+
return new DiffmapShareError({
|
|
80
|
+
reason: cause instanceof Error ? cause.message : "gh failed",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isErrnoException(cause: unknown): cause is NodeJS.ErrnoException {
|
|
86
|
+
if (typeof cause !== "object" || !cause) return false;
|
|
87
|
+
return "code" in cause && typeof cause.code === "string";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function execStderr(cause: unknown) {
|
|
91
|
+
if (typeof cause !== "object" || !cause) return "";
|
|
92
|
+
if (!("stderr" in cause) || typeof cause.stderr !== "string") return "";
|
|
93
|
+
return cause.stderr;
|
|
94
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { backgroundColor, colors, focusRing, spacing, text } from "maui";
|
|
2
|
+
import { style, useStyles } from "purse-styles";
|
|
3
|
+
import { codeFontFamily } from "../codeFont.js";
|
|
4
|
+
import type { SourceAnnotation, SourceNavigation } from "../annotations.js";
|
|
5
|
+
import { pierreShell } from "./pierre.js";
|
|
6
|
+
|
|
7
|
+
export function CallStackDiff(
|
|
8
|
+
props: { lines: SourceAnnotation[] } & SourceNavigation,
|
|
9
|
+
) {
|
|
10
|
+
const shell = useStyles(pierreShell, styles.shell);
|
|
11
|
+
const row = useStyles(styles.row);
|
|
12
|
+
return (
|
|
13
|
+
<div className={shell} data-diffmap-kind="callstack">
|
|
14
|
+
{props.lines.map((line, index) => {
|
|
15
|
+
const sign = line.text.startsWith("+")
|
|
16
|
+
? "+"
|
|
17
|
+
: line.text.startsWith("-")
|
|
18
|
+
? "-"
|
|
19
|
+
: " ";
|
|
20
|
+
const label = line.text.replace(/^[+ -]/, "");
|
|
21
|
+
const tree = /^[ \t│┃├└─]*/u.exec(label)![0];
|
|
22
|
+
const description = label.slice(tree.length);
|
|
23
|
+
const comment = description.indexOf("#");
|
|
24
|
+
const content = (
|
|
25
|
+
<>
|
|
26
|
+
<span className="diff-sign" aria-hidden="true">
|
|
27
|
+
{sign}
|
|
28
|
+
</span>
|
|
29
|
+
<span className="stack-content">
|
|
30
|
+
<span className="tree-prefix" aria-hidden="true">
|
|
31
|
+
{[...tree].map((branch, i) => (
|
|
32
|
+
<span key={i} data-branch={branch} />
|
|
33
|
+
))}
|
|
34
|
+
</span>
|
|
35
|
+
<span className="stack-label">
|
|
36
|
+
{comment === -1 ? description : description.slice(0, comment)}
|
|
37
|
+
{comment !== -1 && (
|
|
38
|
+
<span className="stack-comment">
|
|
39
|
+
{description.slice(comment)}
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
</span>
|
|
43
|
+
</span>
|
|
44
|
+
{line.references.length > 0 && (
|
|
45
|
+
<span className="source-indicator" aria-hidden="true">
|
|
46
|
+
↗
|
|
47
|
+
</span>
|
|
48
|
+
)}
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
if (line.references.length === 0) {
|
|
52
|
+
return (
|
|
53
|
+
<div key={index} className={row} data-change={sign}>
|
|
54
|
+
{content}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return (
|
|
59
|
+
<button
|
|
60
|
+
key={index}
|
|
61
|
+
type="button"
|
|
62
|
+
className={row}
|
|
63
|
+
data-change={sign}
|
|
64
|
+
aria-pressed={props.selectedAnnotation === line}
|
|
65
|
+
aria-controls="source-diff-panel"
|
|
66
|
+
title="Show source changes"
|
|
67
|
+
aria-label={description}
|
|
68
|
+
onClick={() => props.onSelectAnnotation(line)}
|
|
69
|
+
>
|
|
70
|
+
{content}
|
|
71
|
+
</button>
|
|
72
|
+
);
|
|
73
|
+
})}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const styles = {
|
|
79
|
+
shell: style(
|
|
80
|
+
text({ size: "sm", fontWeight: 500, color: "highContrast" }),
|
|
81
|
+
spacing.padding({ y: 2 }),
|
|
82
|
+
{
|
|
83
|
+
backgroundColor: backgroundColor.app,
|
|
84
|
+
overflowX: "auto",
|
|
85
|
+
},
|
|
86
|
+
),
|
|
87
|
+
row: style(
|
|
88
|
+
spacing.padding({ x: 3 }),
|
|
89
|
+
{
|
|
90
|
+
display: "flex",
|
|
91
|
+
gap: "12px",
|
|
92
|
+
width: "100%",
|
|
93
|
+
minWidth: "max-content",
|
|
94
|
+
textAlign: "left",
|
|
95
|
+
font: "inherit",
|
|
96
|
+
lineHeight: "1.6",
|
|
97
|
+
whiteSpace: "pre",
|
|
98
|
+
color: "inherit",
|
|
99
|
+
background: "transparent",
|
|
100
|
+
border: 0,
|
|
101
|
+
"&[data-change='+']": { backgroundColor: colors.green[3] },
|
|
102
|
+
"&[data-change='-']": { backgroundColor: colors.red[3] },
|
|
103
|
+
"&[aria-pressed]": { cursor: "pointer" },
|
|
104
|
+
"&[aria-pressed]:hover": { backgroundColor: colors.blue[2] },
|
|
105
|
+
"&[aria-pressed='true'], &[aria-pressed='true']:hover": {
|
|
106
|
+
backgroundColor: colors.blue[3],
|
|
107
|
+
boxShadow: `inset 4px 0 ${colors.blue[9]}`,
|
|
108
|
+
},
|
|
109
|
+
"& .stack-content": {
|
|
110
|
+
display: "flex",
|
|
111
|
+
alignItems: "stretch",
|
|
112
|
+
minHeight: "28px",
|
|
113
|
+
},
|
|
114
|
+
"& .stack-label": { paddingBlock: "3px" },
|
|
115
|
+
"& .stack-comment": { color: colors.gray[11], fontWeight: 400 },
|
|
116
|
+
"& .tree-prefix": { display: "inline-flex", alignSelf: "stretch" },
|
|
117
|
+
"& [data-branch]": {
|
|
118
|
+
position: "relative",
|
|
119
|
+
width: "0.6em",
|
|
120
|
+
flexShrink: 0,
|
|
121
|
+
},
|
|
122
|
+
"& [data-branch='│']::before, & [data-branch='┃']::before, & [data-branch='├']::before, & [data-branch='└']::before":
|
|
123
|
+
{
|
|
124
|
+
content: "''",
|
|
125
|
+
position: "absolute",
|
|
126
|
+
left: "50%",
|
|
127
|
+
top: 0,
|
|
128
|
+
bottom: 0,
|
|
129
|
+
borderLeft: `1px solid ${colors.gray[9]}`,
|
|
130
|
+
},
|
|
131
|
+
"& [data-branch='└']::before": { bottom: "50%" },
|
|
132
|
+
"& [data-branch='├']::after, & [data-branch='└']::after, & [data-branch='─']::after":
|
|
133
|
+
{
|
|
134
|
+
content: "''",
|
|
135
|
+
position: "absolute",
|
|
136
|
+
left: "50%",
|
|
137
|
+
right: 0,
|
|
138
|
+
top: "50%",
|
|
139
|
+
borderTop: `1px solid ${colors.gray[9]}`,
|
|
140
|
+
},
|
|
141
|
+
"& [data-branch='─']::after": { left: 0 },
|
|
142
|
+
"& .tree-prefix, & .diff-sign": {
|
|
143
|
+
fontFamily: codeFontFamily,
|
|
144
|
+
whiteSpace: "pre",
|
|
145
|
+
flexShrink: 0,
|
|
146
|
+
},
|
|
147
|
+
"& > .diff-sign": { alignSelf: "center" },
|
|
148
|
+
"& > .source-indicator": {
|
|
149
|
+
alignSelf: "center",
|
|
150
|
+
marginLeft: "auto",
|
|
151
|
+
color: colors.blue[11],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
focusRing(),
|
|
155
|
+
),
|
|
156
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SVGProps } from "react";
|
|
2
|
+
|
|
3
|
+
export function Check(props: SVGProps<SVGSVGElement>) {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
focusable="false"
|
|
7
|
+
aria-hidden="true"
|
|
8
|
+
role="img"
|
|
9
|
+
width={24}
|
|
10
|
+
height={24}
|
|
11
|
+
fill="none"
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<path
|
|
16
|
+
stroke="currentColor"
|
|
17
|
+
strokeLinecap="round"
|
|
18
|
+
strokeLinejoin="round"
|
|
19
|
+
strokeWidth={1.5}
|
|
20
|
+
d="M5.75 12.5L10 16.75L18.25 7.25"
|
|
21
|
+
/>
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PatchDiff } from "@pierre/diffs/react";
|
|
2
|
+
import { useTheme } from "maui";
|
|
3
|
+
import { useStyles } from "purse-styles";
|
|
4
|
+
import { toUnifiedDiff } from "../normalizeDiff.js";
|
|
5
|
+
import { pierreDiffOptions, pierreShell } from "./pierre.ts";
|
|
6
|
+
|
|
7
|
+
export function CodeDiff(props: { source: string; path?: string }) {
|
|
8
|
+
const { resolvedTheme } = useTheme();
|
|
9
|
+
const shell = useStyles(pierreShell);
|
|
10
|
+
const path = props.path === undefined ? "diff" : props.path;
|
|
11
|
+
const patch = toUnifiedDiff(props.source, path);
|
|
12
|
+
const disableFileHeader = props.path === undefined;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className={shell} data-file-path={path} data-diffmap-kind="diff">
|
|
16
|
+
<PatchDiff
|
|
17
|
+
patch={patch}
|
|
18
|
+
disableWorkerPool
|
|
19
|
+
options={pierreDiffOptions({
|
|
20
|
+
themeType: resolvedTheme,
|
|
21
|
+
disableFileHeader,
|
|
22
|
+
})}
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|