@youtyan/code-viewer 0.1.17 → 0.1.19
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/dist/code-viewer.js +341 -48
- package/package.json +3 -2
- package/web/app.js +651 -234
- package/web/style.css +12 -0
package/dist/code-viewer.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// web-src/server/preview.ts
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
closeSync,
|
|
6
|
+
constants,
|
|
7
|
+
existsSync as existsSync3,
|
|
8
|
+
lstatSync as lstatSync3,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync as readFileSync2,
|
|
11
|
+
realpathSync,
|
|
12
|
+
statSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
watch,
|
|
15
|
+
writeFileSync
|
|
16
|
+
} from "node:fs";
|
|
5
17
|
import { basename as basename2, dirname as dirname2, extname, join as join4, relative } from "node:path";
|
|
6
18
|
|
|
7
19
|
// web-src/routes.ts
|
|
@@ -70,7 +82,13 @@ function startDevAssetReload(options) {
|
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
// web-src/server/git.ts
|
|
73
|
-
import {
|
|
85
|
+
import {
|
|
86
|
+
existsSync,
|
|
87
|
+
lstatSync as lstatSync2,
|
|
88
|
+
readdirSync,
|
|
89
|
+
readFileSync
|
|
90
|
+
} from "node:fs";
|
|
91
|
+
import { join as join2 } from "node:path";
|
|
74
92
|
|
|
75
93
|
// web-src/server/runtime.ts
|
|
76
94
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -223,9 +241,11 @@ async function writeWebResponse(res, response) {
|
|
|
223
241
|
}
|
|
224
242
|
|
|
225
243
|
// web-src/server/git.ts
|
|
226
|
-
import { join as join2 } from "node:path";
|
|
227
244
|
var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32;
|
|
228
245
|
var WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000;
|
|
246
|
+
var DEFAULT_REF_COMMIT_LIMIT = 100;
|
|
247
|
+
var MAX_REF_COMMIT_LIMIT = 500;
|
|
248
|
+
var COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI";
|
|
229
249
|
var DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
|
|
230
250
|
"node_modules",
|
|
231
251
|
".venv",
|
|
@@ -279,11 +299,19 @@ function catFileBlobStream(oid, cwd) {
|
|
|
279
299
|
}
|
|
280
300
|
function objectSize(ref, path, cwd) {
|
|
281
301
|
const res = run(["git", "cat-file", "-s", `${ref}:${path}`], cwd);
|
|
282
|
-
return {
|
|
302
|
+
return {
|
|
303
|
+
code: res.code,
|
|
304
|
+
size: Number(res.stdout.trim()) || 0,
|
|
305
|
+
stderr: res.stderr
|
|
306
|
+
};
|
|
283
307
|
}
|
|
284
308
|
function objectByteSize(oid, cwd) {
|
|
285
309
|
const res = run(["git", "cat-file", "-s", oid], cwd);
|
|
286
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
code: res.code,
|
|
312
|
+
size: Number(res.stdout.trim()) || 0,
|
|
313
|
+
stderr: res.stderr
|
|
314
|
+
};
|
|
287
315
|
}
|
|
288
316
|
function objectId(ref, path, cwd) {
|
|
289
317
|
const res = run(["git", "rev-parse", "--verify", `${ref}:${path}`], cwd);
|
|
@@ -304,7 +332,12 @@ function verifyTreeRef(ref, cwd) {
|
|
|
304
332
|
return res.code === 0;
|
|
305
333
|
}
|
|
306
334
|
function refs(cwd) {
|
|
307
|
-
const out = {
|
|
335
|
+
const out = {
|
|
336
|
+
branches: [],
|
|
337
|
+
tags: [],
|
|
338
|
+
commits: [],
|
|
339
|
+
current: ""
|
|
340
|
+
};
|
|
308
341
|
const branches = run([
|
|
309
342
|
"git",
|
|
310
343
|
"for-each-ref",
|
|
@@ -317,17 +350,104 @@ function refs(cwd) {
|
|
|
317
350
|
out.branches = branches.stdout.split(`
|
|
318
351
|
`).filter((line) => line && line !== "origin/HEAD");
|
|
319
352
|
}
|
|
320
|
-
const tags = run([
|
|
353
|
+
const tags = run([
|
|
354
|
+
"git",
|
|
355
|
+
"for-each-ref",
|
|
356
|
+
"--sort=-creatordate",
|
|
357
|
+
"--format=%(refname:short)",
|
|
358
|
+
"refs/tags"
|
|
359
|
+
], cwd);
|
|
321
360
|
if (tags.code === 0)
|
|
322
361
|
out.tags = tags.stdout.split(`
|
|
323
362
|
`).filter(Boolean);
|
|
324
|
-
|
|
325
|
-
if (commits.code === 0)
|
|
326
|
-
out.commits = commits.stdout.split(`
|
|
327
|
-
`).filter(Boolean);
|
|
363
|
+
out.commits = refCommits(cwd, "", DEFAULT_REF_COMMIT_LIMIT);
|
|
328
364
|
out.current = currentBranch(cwd) || "";
|
|
329
365
|
return out;
|
|
330
366
|
}
|
|
367
|
+
function clampCommitLimit(max) {
|
|
368
|
+
return Math.max(1, Math.min(max, MAX_REF_COMMIT_LIMIT));
|
|
369
|
+
}
|
|
370
|
+
function parseCommitLog(stdout) {
|
|
371
|
+
const parts = stdout.split("\x00");
|
|
372
|
+
const commits = [];
|
|
373
|
+
for (let index = 0;index < parts.length; ) {
|
|
374
|
+
if (!parts[index]) {
|
|
375
|
+
index++;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const sha = parts[index++] || "";
|
|
379
|
+
const subject = parts[index++] || "";
|
|
380
|
+
const author = parts[index++] || "";
|
|
381
|
+
const when = parts[index++] || "";
|
|
382
|
+
if (sha)
|
|
383
|
+
commits.push({ sha, subject, author, when });
|
|
384
|
+
}
|
|
385
|
+
return commits;
|
|
386
|
+
}
|
|
387
|
+
function commitLogArgs(limit) {
|
|
388
|
+
return [
|
|
389
|
+
"git",
|
|
390
|
+
"log",
|
|
391
|
+
"--all",
|
|
392
|
+
"-z",
|
|
393
|
+
`--max-count=${limit}`,
|
|
394
|
+
`--format=${COMMIT_FORMAT}`
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
function mergeCommitResults(limit, ...groups) {
|
|
398
|
+
const seen = new Set;
|
|
399
|
+
const merged = [];
|
|
400
|
+
for (const commits of groups) {
|
|
401
|
+
for (const commit of commits) {
|
|
402
|
+
if (!commit.sha || seen.has(commit.sha))
|
|
403
|
+
continue;
|
|
404
|
+
seen.add(commit.sha);
|
|
405
|
+
merged.push(commit);
|
|
406
|
+
if (merged.length >= limit)
|
|
407
|
+
return merged;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return merged;
|
|
411
|
+
}
|
|
412
|
+
function runCommitLog(cwd, args) {
|
|
413
|
+
const commits = run(args, cwd);
|
|
414
|
+
return commits.code === 0 ? parseCommitLog(commits.stdout) : [];
|
|
415
|
+
}
|
|
416
|
+
function refCommits(cwd, query = "", max = DEFAULT_REF_COMMIT_LIMIT) {
|
|
417
|
+
const limit = clampCommitLimit(max);
|
|
418
|
+
const trimmed = query.trim().slice(0, 200).replace(/\0/g, "");
|
|
419
|
+
const hashMatches = [];
|
|
420
|
+
if (/^[0-9a-f]{4,40}$/i.test(trimmed)) {
|
|
421
|
+
const verified = run(["git", "rev-parse", "--verify", `${trimmed}^{commit}`], cwd);
|
|
422
|
+
const single = run([
|
|
423
|
+
"git",
|
|
424
|
+
"log",
|
|
425
|
+
"-z",
|
|
426
|
+
"-1",
|
|
427
|
+
`--format=${COMMIT_FORMAT}`,
|
|
428
|
+
verified.code === 0 && verified.stdout.trim() ? verified.stdout.trim() : trimmed
|
|
429
|
+
], cwd);
|
|
430
|
+
if (single.code === 0 && single.stdout.trim()) {
|
|
431
|
+
hashMatches.push(...parseCommitLog(single.stdout));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (!trimmed) {
|
|
435
|
+
return runCommitLog(cwd, commitLogArgs(limit));
|
|
436
|
+
}
|
|
437
|
+
const subjectMatches = runCommitLog(cwd, [
|
|
438
|
+
...commitLogArgs(limit),
|
|
439
|
+
"--regexp-ignore-case",
|
|
440
|
+
"--fixed-strings",
|
|
441
|
+
`--grep=${trimmed}`
|
|
442
|
+
]);
|
|
443
|
+
const authorMatches = runCommitLog(cwd, [
|
|
444
|
+
...commitLogArgs(limit),
|
|
445
|
+
"--regexp-ignore-case",
|
|
446
|
+
"--fixed-strings",
|
|
447
|
+
`--author=${trimmed}`
|
|
448
|
+
]);
|
|
449
|
+
return mergeCommitResults(limit, hashMatches, subjectMatches, authorMatches);
|
|
450
|
+
}
|
|
331
451
|
function nameStatus(args, cwd) {
|
|
332
452
|
const res = run([
|
|
333
453
|
"git",
|
|
@@ -354,7 +474,12 @@ function nameStatus(args, cwd) {
|
|
|
354
474
|
const oldPath = parts[i++] || "";
|
|
355
475
|
const path = parts[i++] || "";
|
|
356
476
|
if (path)
|
|
357
|
-
files.push({
|
|
477
|
+
files.push({
|
|
478
|
+
status: kind,
|
|
479
|
+
old_path: oldPath,
|
|
480
|
+
path,
|
|
481
|
+
similarity: Number(status.slice(1)) || undefined
|
|
482
|
+
});
|
|
358
483
|
} else {
|
|
359
484
|
const path = parts[i++] || "";
|
|
360
485
|
if (path)
|
|
@@ -500,7 +625,11 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
|
|
|
500
625
|
continue;
|
|
501
626
|
walk(full, entryPath, depth + 1);
|
|
502
627
|
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
503
|
-
if (!pushRecursiveEntry({
|
|
628
|
+
if (!pushRecursiveEntry({
|
|
629
|
+
name: entry.name,
|
|
630
|
+
path: entryPath,
|
|
631
|
+
type: "blob"
|
|
632
|
+
}))
|
|
504
633
|
return;
|
|
505
634
|
}
|
|
506
635
|
}
|
|
@@ -533,7 +662,11 @@ function gitTreeEntries(ref, path, cwd, recursive) {
|
|
|
533
662
|
if (!match)
|
|
534
663
|
return null;
|
|
535
664
|
const entryPath = match[2];
|
|
536
|
-
return {
|
|
665
|
+
return {
|
|
666
|
+
name: entryPath.split("/").pop() || entryPath,
|
|
667
|
+
path: entryPath,
|
|
668
|
+
type: match[1]
|
|
669
|
+
};
|
|
537
670
|
}).filter((entry) => !!entry);
|
|
538
671
|
if (recursive)
|
|
539
672
|
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -554,7 +687,11 @@ function worktreeFiles(cwd) {
|
|
|
554
687
|
function listTree(ref, path, cwd, options = {}) {
|
|
555
688
|
const base = normalizeTreePath(path);
|
|
556
689
|
if (ref === "worktree") {
|
|
557
|
-
return {
|
|
690
|
+
return {
|
|
691
|
+
code: 0,
|
|
692
|
+
entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames),
|
|
693
|
+
stderr: ""
|
|
694
|
+
};
|
|
558
695
|
}
|
|
559
696
|
const direct = gitTreeEntries(ref, base, cwd, false);
|
|
560
697
|
if (direct.code !== 0 || !options.recursive)
|
|
@@ -562,7 +699,11 @@ function listTree(ref, path, cwd, options = {}) {
|
|
|
562
699
|
const recursive = gitTreeEntries(ref, base, cwd, true);
|
|
563
700
|
if (recursive.code !== 0)
|
|
564
701
|
return recursive;
|
|
565
|
-
return {
|
|
702
|
+
return {
|
|
703
|
+
code: 0,
|
|
704
|
+
entries: combineDirectAndRecursiveFiles(direct.entries, recursive.entries),
|
|
705
|
+
stderr: ""
|
|
706
|
+
};
|
|
566
707
|
}
|
|
567
708
|
function untrackedMeta(cwd) {
|
|
568
709
|
return untracked(cwd).map((path) => {
|
|
@@ -577,7 +718,14 @@ function untrackedMeta(cwd) {
|
|
|
577
718
|
lines = data.toString("utf8").split(`
|
|
578
719
|
`).length - 1;
|
|
579
720
|
}
|
|
580
|
-
return {
|
|
721
|
+
return {
|
|
722
|
+
path,
|
|
723
|
+
status: "A",
|
|
724
|
+
additions: binary ? 0 : lines,
|
|
725
|
+
deletions: 0,
|
|
726
|
+
binary,
|
|
727
|
+
untracked: true
|
|
728
|
+
};
|
|
581
729
|
});
|
|
582
730
|
}
|
|
583
731
|
function fileMeta(args, cwd, includeUntracked = false) {
|
|
@@ -978,7 +1126,12 @@ function buildFileSearchList(ref, generation, entries) {
|
|
|
978
1126
|
}
|
|
979
1127
|
function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
|
|
980
1128
|
const safePaths = paths.length ? paths : ["."];
|
|
981
|
-
const omitGlobs = omitDirNames.flatMap((name) => [
|
|
1129
|
+
const omitGlobs = omitDirNames.flatMap((name) => [
|
|
1130
|
+
"--glob",
|
|
1131
|
+
`!${name}/**`,
|
|
1132
|
+
"--glob",
|
|
1133
|
+
`!**/${name}/**`
|
|
1134
|
+
]);
|
|
982
1135
|
const args = [
|
|
983
1136
|
"rg",
|
|
984
1137
|
"--no-config",
|
|
@@ -1017,7 +1170,12 @@ function parseRgOutput(stdout, max, omitDirNames = []) {
|
|
|
1017
1170
|
const preview = parsed[4];
|
|
1018
1171
|
if (!path || !lineNo || !column || isSkippableSearchPath(path, omitDirNames))
|
|
1019
1172
|
continue;
|
|
1020
|
-
matches.push({
|
|
1173
|
+
matches.push({
|
|
1174
|
+
path,
|
|
1175
|
+
line: lineNo,
|
|
1176
|
+
column,
|
|
1177
|
+
preview: preview.slice(0, 500)
|
|
1178
|
+
});
|
|
1021
1179
|
}
|
|
1022
1180
|
return matches;
|
|
1023
1181
|
}
|
|
@@ -1076,6 +1234,7 @@ var cliArgs = DEFAULT_ARGS;
|
|
|
1076
1234
|
var listenPort = 0;
|
|
1077
1235
|
var allowUpload = false;
|
|
1078
1236
|
var uploadAllowedByCli = false;
|
|
1237
|
+
var openAfterStart = false;
|
|
1079
1238
|
var scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
|
|
1080
1239
|
var scopeOmitDirCliOverride = null;
|
|
1081
1240
|
var rgAvailableCache = null;
|
|
@@ -1129,7 +1288,7 @@ Examples:
|
|
|
1129
1288
|
}
|
|
1130
1289
|
listenPort = parsed;
|
|
1131
1290
|
} else if (arg === "--open") {
|
|
1132
|
-
|
|
1291
|
+
openAfterStart = true;
|
|
1133
1292
|
} else if (arg === "--allow-upload") {
|
|
1134
1293
|
allowUpload = true;
|
|
1135
1294
|
uploadAllowedByCli = true;
|
|
@@ -1139,7 +1298,10 @@ Examples:
|
|
|
1139
1298
|
console.error("--scope-omit-dir requires a directory name");
|
|
1140
1299
|
process.exit(1);
|
|
1141
1300
|
}
|
|
1142
|
-
scopeOmitDirCliOverride = normalizeScopeOmitDirNames([
|
|
1301
|
+
scopeOmitDirCliOverride = normalizeScopeOmitDirNames([
|
|
1302
|
+
...scopeOmitDirCliOverride || [],
|
|
1303
|
+
next
|
|
1304
|
+
]);
|
|
1143
1305
|
} else {
|
|
1144
1306
|
rest.push(arg);
|
|
1145
1307
|
}
|
|
@@ -1168,7 +1330,10 @@ function json(data, init = {}) {
|
|
|
1168
1330
|
function text(body, status = 200) {
|
|
1169
1331
|
return new Response(body, {
|
|
1170
1332
|
status,
|
|
1171
|
-
headers: {
|
|
1333
|
+
headers: {
|
|
1334
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1335
|
+
"Cache-Control": "no-store"
|
|
1336
|
+
}
|
|
1172
1337
|
});
|
|
1173
1338
|
}
|
|
1174
1339
|
function requestAllowed(req) {
|
|
@@ -1192,11 +1357,26 @@ function staticFile(pathname) {
|
|
|
1192
1357
|
"/app.js": ["app.js", "application/javascript; charset=utf-8"],
|
|
1193
1358
|
"/mermaid.js": ["mermaid.js", "application/javascript; charset=utf-8"],
|
|
1194
1359
|
"/shiki.js": ["shiki.js", "application/javascript; charset=utf-8"],
|
|
1195
|
-
"/vendor/diff2html/diff2html.min.css": [
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
"/vendor/
|
|
1360
|
+
"/vendor/diff2html/diff2html.min.css": [
|
|
1361
|
+
"vendor/diff2html/diff2html.min.css",
|
|
1362
|
+
"text/css; charset=utf-8"
|
|
1363
|
+
],
|
|
1364
|
+
"/vendor/diff2html/diff2html-ui.min.js": [
|
|
1365
|
+
"vendor/diff2html/diff2html-ui.min.js",
|
|
1366
|
+
"application/javascript; charset=utf-8"
|
|
1367
|
+
],
|
|
1368
|
+
"/vendor/highlight.js/highlight.min.js": [
|
|
1369
|
+
"vendor/highlight.js/highlight.min.js",
|
|
1370
|
+
"application/javascript; charset=utf-8"
|
|
1371
|
+
],
|
|
1372
|
+
"/vendor/highlight.js/styles/github.min.css": [
|
|
1373
|
+
"vendor/highlight.js/styles/github.min.css",
|
|
1374
|
+
"text/css; charset=utf-8"
|
|
1375
|
+
],
|
|
1376
|
+
"/vendor/highlight.js/styles/github-dark.min.css": [
|
|
1377
|
+
"vendor/highlight.js/styles/github-dark.min.css",
|
|
1378
|
+
"text/css; charset=utf-8"
|
|
1379
|
+
]
|
|
1200
1380
|
};
|
|
1201
1381
|
for (const spaPath of [...APP_ENTRY_PATHS, ...SPA_PATHS]) {
|
|
1202
1382
|
map[spaPath] = ["index.html", "text/html; charset=utf-8"];
|
|
@@ -1266,7 +1446,14 @@ function buildQuery(params) {
|
|
|
1266
1446
|
}
|
|
1267
1447
|
function fileToMeta(file, range, extraQs) {
|
|
1268
1448
|
const sizeClass = classify(file);
|
|
1269
|
-
const q = {
|
|
1449
|
+
const q = {
|
|
1450
|
+
path: file.path,
|
|
1451
|
+
old_path: file.old_path,
|
|
1452
|
+
status: file.status,
|
|
1453
|
+
from: range.from,
|
|
1454
|
+
to: range.to,
|
|
1455
|
+
...extraQs
|
|
1456
|
+
};
|
|
1270
1457
|
if (file.untracked)
|
|
1271
1458
|
Object.assign(q, { untracked: "1" });
|
|
1272
1459
|
const previewQ = { ...q, mode: "preview", max_hunks: PREVIEW_HUNKS_DEFAULT };
|
|
@@ -1326,7 +1513,14 @@ function computePayload(extras, range) {
|
|
|
1326
1513
|
}, { files: meta.length, additions: 0, deletions: 0 });
|
|
1327
1514
|
const toWorktree = !range.to || range.to === "worktree";
|
|
1328
1515
|
const label = refs2.length ? `${refs2.join(" .. ")}${toWorktree && refs2.length === 1 ? " .. worktree" : ""}` : cliArgs.join(" ");
|
|
1329
|
-
return {
|
|
1516
|
+
return {
|
|
1517
|
+
files: meta,
|
|
1518
|
+
totals,
|
|
1519
|
+
range: label || "HEAD",
|
|
1520
|
+
project: basename2(cwd),
|
|
1521
|
+
branch: currentBranch(cwd) || undefined,
|
|
1522
|
+
generation
|
|
1523
|
+
};
|
|
1330
1524
|
}
|
|
1331
1525
|
function handleDiffJson(url) {
|
|
1332
1526
|
const extras = [];
|
|
@@ -1334,7 +1528,10 @@ function handleDiffJson(url) {
|
|
|
1334
1528
|
extras.push("-w");
|
|
1335
1529
|
if (url.searchParams.get("ignore_blank") === "1")
|
|
1336
1530
|
extras.push("--ignore-blank-lines");
|
|
1337
|
-
const range = {
|
|
1531
|
+
const range = {
|
|
1532
|
+
from: url.searchParams.get("from") || "",
|
|
1533
|
+
to: url.searchParams.get("to") || ""
|
|
1534
|
+
};
|
|
1338
1535
|
const key = `${range.from}|${range.to}|${url.searchParams.get("ignore_ws") || ""}|${url.searchParams.get("ignore_blank") || ""}`;
|
|
1339
1536
|
if (url.searchParams.get("nocache") === "1") {
|
|
1340
1537
|
const payload2 = computePayload(extras, range);
|
|
@@ -1348,15 +1545,33 @@ function handleDiffJson(url) {
|
|
|
1348
1545
|
}
|
|
1349
1546
|
const body2 = JSON.stringify(payload2);
|
|
1350
1547
|
setTimedCacheEntry(metaCache, key, { body: body2, sig });
|
|
1351
|
-
return new Response(body2, {
|
|
1548
|
+
return new Response(body2, {
|
|
1549
|
+
headers: {
|
|
1550
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1551
|
+
"Cache-Control": "no-store"
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1352
1554
|
}
|
|
1353
1555
|
const cached = metaCache.get(key);
|
|
1354
1556
|
if (cacheFresh(cached))
|
|
1355
|
-
return new Response(cached.body, {
|
|
1557
|
+
return new Response(cached.body, {
|
|
1558
|
+
headers: {
|
|
1559
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1560
|
+
"Cache-Control": "no-store"
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1356
1563
|
const payload = computePayload(extras, range);
|
|
1357
1564
|
const body = JSON.stringify(payload);
|
|
1358
|
-
setTimedCacheEntry(metaCache, key, {
|
|
1359
|
-
|
|
1565
|
+
setTimedCacheEntry(metaCache, key, {
|
|
1566
|
+
body,
|
|
1567
|
+
sig: JSON.stringify({ ...payload, generation: undefined })
|
|
1568
|
+
});
|
|
1569
|
+
return new Response(body, {
|
|
1570
|
+
headers: {
|
|
1571
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1572
|
+
"Cache-Control": "no-store"
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1360
1575
|
}
|
|
1361
1576
|
function safePath(path) {
|
|
1362
1577
|
if (!path || path.startsWith("/") || path.startsWith("\\") || path.includes("\x00"))
|
|
@@ -1369,7 +1584,9 @@ function safeRepoPath(path) {
|
|
|
1369
1584
|
function normalizeScopeOmitDirNames(names) {
|
|
1370
1585
|
if (!Array.isArray(names))
|
|
1371
1586
|
return [];
|
|
1372
|
-
return [
|
|
1587
|
+
return [
|
|
1588
|
+
...new Set(names.filter((name) => typeof name === "string").map((name) => name.trim()).filter((name) => name && name.length <= 64 && !name.includes("/") && !name.includes("\\") && !name.includes("\x00") && name !== "." && name !== ".." && name !== ".git"))
|
|
1589
|
+
].sort((a, b) => a.localeCompare(b));
|
|
1373
1590
|
}
|
|
1374
1591
|
function parseScopeOmitDirNamesQuery(value) {
|
|
1375
1592
|
const names = value ? value.split(",") : [];
|
|
@@ -1498,7 +1715,10 @@ function handleTree(url) {
|
|
|
1498
1715
|
const recursive = url.searchParams.get("recursive") === "1";
|
|
1499
1716
|
if (invalidScopeOmitDirNamesQuery(url))
|
|
1500
1717
|
return text("invalid omit dirs", 400);
|
|
1501
|
-
const entries = listTree(target, path, cwd, {
|
|
1718
|
+
const entries = listTree(target, path, cwd, {
|
|
1719
|
+
recursive,
|
|
1720
|
+
omitDirNames: scopeOmitDirNamesFromQuery(url)
|
|
1721
|
+
}).entries;
|
|
1502
1722
|
return json({
|
|
1503
1723
|
ref: target,
|
|
1504
1724
|
path,
|
|
@@ -1531,7 +1751,10 @@ function handleFiles(url) {
|
|
|
1531
1751
|
if (cached && cached.generation === generation)
|
|
1532
1752
|
return json(cached.body);
|
|
1533
1753
|
const ref = target || "worktree";
|
|
1534
|
-
const entries = listTree(ref, "", cwd, {
|
|
1754
|
+
const entries = listTree(ref, "", cwd, {
|
|
1755
|
+
recursive: true,
|
|
1756
|
+
omitDirNames
|
|
1757
|
+
}).entries;
|
|
1535
1758
|
const body = buildFileSearchList(ref, generation, entries);
|
|
1536
1759
|
fileListCache.set(key, { generation, body });
|
|
1537
1760
|
return json(body);
|
|
@@ -1584,12 +1807,27 @@ function grepWorktree(query, max, paths, regex, omitDirNames) {
|
|
|
1584
1807
|
const proc = runSync(args, cwd, { timeout: 5000 });
|
|
1585
1808
|
const stdout = proc.stdout;
|
|
1586
1809
|
const matches2 = parseRgOutput(stdout, max, omitDirNames).filter((match) => safePath(match.path) && !isGitInternalPath(match.path) && !isSkippableSearchPath(match.path, omitDirNames) && !!safeWorktreePath(match.path));
|
|
1587
|
-
return {
|
|
1810
|
+
return {
|
|
1811
|
+
ref: "worktree",
|
|
1812
|
+
engine: "rg",
|
|
1813
|
+
truncated: matches2.length >= max,
|
|
1814
|
+
matches: matches2
|
|
1815
|
+
};
|
|
1588
1816
|
}
|
|
1589
1817
|
if (regex)
|
|
1590
|
-
return {
|
|
1818
|
+
return {
|
|
1819
|
+
ref: "worktree",
|
|
1820
|
+
engine: "fallback",
|
|
1821
|
+
truncated: false,
|
|
1822
|
+
matches: []
|
|
1823
|
+
};
|
|
1591
1824
|
const matches = grepWorktreeFallback(query, max, paths, omitDirNames);
|
|
1592
|
-
return {
|
|
1825
|
+
return {
|
|
1826
|
+
ref: "worktree",
|
|
1827
|
+
engine: "fallback",
|
|
1828
|
+
truncated: matches.length >= max,
|
|
1829
|
+
matches
|
|
1830
|
+
};
|
|
1593
1831
|
}
|
|
1594
1832
|
function grepTreeRef(ref, query, max, paths, regex, omitDirNames) {
|
|
1595
1833
|
const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames));
|
|
@@ -1624,13 +1862,24 @@ function handleGrep(url) {
|
|
|
1624
1862
|
const paths = parseGrepPaths(url, omitDirNames);
|
|
1625
1863
|
const regex = url.searchParams.get("regex") === "1";
|
|
1626
1864
|
if (!query.trim())
|
|
1627
|
-
return json({
|
|
1865
|
+
return json({
|
|
1866
|
+
ref,
|
|
1867
|
+
engine: ref === "worktree" ? "fallback" : "git",
|
|
1868
|
+
truncated: false,
|
|
1869
|
+
matches: []
|
|
1870
|
+
});
|
|
1628
1871
|
if (ref === "worktree" || ref === "")
|
|
1629
1872
|
return json(grepWorktree(query, max, paths, regex, omitDirNames));
|
|
1630
1873
|
if (!verifyTreeRef(ref, cwd))
|
|
1631
1874
|
return text("invalid target", 400);
|
|
1632
1875
|
return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames));
|
|
1633
1876
|
}
|
|
1877
|
+
function handleRefCommits(url) {
|
|
1878
|
+
const query = url.searchParams.get("q") || "";
|
|
1879
|
+
const parsedMax = Number(url.searchParams.get("max") || "");
|
|
1880
|
+
const max = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : undefined;
|
|
1881
|
+
return json({ commits: refCommits(cwd, query, max) });
|
|
1882
|
+
}
|
|
1634
1883
|
function handleFileDiff(url) {
|
|
1635
1884
|
const path = url.searchParams.get("path") || "";
|
|
1636
1885
|
if (!safePath(path))
|
|
@@ -1641,7 +1890,10 @@ function handleFileDiff(url) {
|
|
|
1641
1890
|
if (url.searchParams.get("ignore_blank") === "1")
|
|
1642
1891
|
extras.push("--ignore-blank-lines");
|
|
1643
1892
|
const isUntracked = url.searchParams.get("untracked") === "1";
|
|
1644
|
-
const range = {
|
|
1893
|
+
const range = {
|
|
1894
|
+
from: url.searchParams.get("from") || "",
|
|
1895
|
+
to: url.searchParams.get("to") || ""
|
|
1896
|
+
};
|
|
1645
1897
|
if (isSameWorktreeRange(range)) {
|
|
1646
1898
|
return json({
|
|
1647
1899
|
path,
|
|
@@ -1661,7 +1913,15 @@ function handleFileDiff(url) {
|
|
|
1661
1913
|
const oldPath = url.searchParams.get("old_path");
|
|
1662
1914
|
let cacheKey;
|
|
1663
1915
|
try {
|
|
1664
|
-
cacheKey = fileDiffCacheKey({
|
|
1916
|
+
cacheKey = fileDiffCacheKey({
|
|
1917
|
+
path,
|
|
1918
|
+
oldPath,
|
|
1919
|
+
isUntracked,
|
|
1920
|
+
range,
|
|
1921
|
+
extras,
|
|
1922
|
+
args,
|
|
1923
|
+
cwd
|
|
1924
|
+
});
|
|
1665
1925
|
} catch {
|
|
1666
1926
|
return text("invalid diff range", 400);
|
|
1667
1927
|
}
|
|
@@ -1850,7 +2110,16 @@ async function handleFileRange(url) {
|
|
|
1850
2110
|
if (!full)
|
|
1851
2111
|
return text("no file", 404);
|
|
1852
2112
|
const result = await collectIndexedWorktreeLineRange(full, start, end);
|
|
1853
|
-
const body = {
|
|
2113
|
+
const body = {
|
|
2114
|
+
path,
|
|
2115
|
+
ref,
|
|
2116
|
+
start,
|
|
2117
|
+
end,
|
|
2118
|
+
lines: result.lines,
|
|
2119
|
+
total: result.total,
|
|
2120
|
+
complete: result.complete,
|
|
2121
|
+
generation
|
|
2122
|
+
};
|
|
1854
2123
|
return json(body);
|
|
1855
2124
|
} else {
|
|
1856
2125
|
if (!verifyTreeRef(ref, cwd))
|
|
@@ -1864,7 +2133,16 @@ async function handleFileRange(url) {
|
|
|
1864
2133
|
const result = await collectIndexedGitBlobLineRange(path, oid.oid, size.size, start, end);
|
|
1865
2134
|
if (!result)
|
|
1866
2135
|
return text("cannot read ref", 500);
|
|
1867
|
-
const body = {
|
|
2136
|
+
const body = {
|
|
2137
|
+
path,
|
|
2138
|
+
ref,
|
|
2139
|
+
start,
|
|
2140
|
+
end,
|
|
2141
|
+
lines: result.lines,
|
|
2142
|
+
total: result.total,
|
|
2143
|
+
complete: result.complete,
|
|
2144
|
+
generation
|
|
2145
|
+
};
|
|
1868
2146
|
return json(body);
|
|
1869
2147
|
}
|
|
1870
2148
|
}
|
|
@@ -1898,7 +2176,11 @@ function handleRawFile(req, url) {
|
|
|
1898
2176
|
if (rangeResult?.kind === "unsatisfiable") {
|
|
1899
2177
|
return new Response(null, {
|
|
1900
2178
|
status: 416,
|
|
1901
|
-
headers: {
|
|
2179
|
+
headers: {
|
|
2180
|
+
...rawFileHeaders(path, size),
|
|
2181
|
+
"Content-Range": `bytes */${size}`,
|
|
2182
|
+
"Content-Length": "0"
|
|
2183
|
+
}
|
|
1902
2184
|
});
|
|
1903
2185
|
}
|
|
1904
2186
|
if (rangeResult?.kind === "range") {
|
|
@@ -1916,7 +2198,9 @@ function handleRawFile(req, url) {
|
|
|
1916
2198
|
}
|
|
1917
2199
|
if (req.method === "HEAD")
|
|
1918
2200
|
return new Response(null, { headers: rawFileHeaders(path, size) });
|
|
1919
|
-
return new Response(fileReadableStream(full), {
|
|
2201
|
+
return new Response(fileReadableStream(full), {
|
|
2202
|
+
headers: rawFileHeaders(path, size)
|
|
2203
|
+
});
|
|
1920
2204
|
}
|
|
1921
2205
|
}
|
|
1922
2206
|
function rawFileSize(path, ref) {
|
|
@@ -2080,7 +2364,11 @@ async function handleUploadFiles(req) {
|
|
|
2080
2364
|
fileCache.clear();
|
|
2081
2365
|
metaCache.clear();
|
|
2082
2366
|
sendSse("update");
|
|
2083
|
-
return json({
|
|
2367
|
+
return json({
|
|
2368
|
+
ok: true,
|
|
2369
|
+
files: uploads.map((upload) => upload.name),
|
|
2370
|
+
generation
|
|
2371
|
+
});
|
|
2084
2372
|
}
|
|
2085
2373
|
function openOsPath(path) {
|
|
2086
2374
|
const cmd = process.platform === "darwin" ? ["open", "--", path] : process.platform === "win32" ? ["explorer.exe", path] : ["xdg-open", path];
|
|
@@ -2164,6 +2452,8 @@ var server = await startServer({
|
|
|
2164
2452
|
return handleFiles(url);
|
|
2165
2453
|
if (url.pathname === "/_grep")
|
|
2166
2454
|
return handleGrep(url);
|
|
2455
|
+
if (url.pathname === "/_commits")
|
|
2456
|
+
return handleRefCommits(url);
|
|
2167
2457
|
if (url.pathname === "/file_diff")
|
|
2168
2458
|
return handleFileDiff(url);
|
|
2169
2459
|
if (url.pathname === "/file_range")
|
|
@@ -2224,6 +2514,9 @@ data: ok
|
|
|
2224
2514
|
return text("not found", 404);
|
|
2225
2515
|
}
|
|
2226
2516
|
});
|
|
2517
|
+
if (openAfterStart) {
|
|
2518
|
+
openBrowser(`http://127.0.0.1:${server.port}/`);
|
|
2519
|
+
}
|
|
2227
2520
|
startDevAssetReload({
|
|
2228
2521
|
enabled: process.env.CODE_VIEWER_DEV === "1",
|
|
2229
2522
|
webRoot: WEB_ROOT,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youtyan/code-viewer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Local browser-based code and git diff viewer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,12 +32,13 @@
|
|
|
32
32
|
"check": "bun run typecheck",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
34
|
"check:bundle": "bun build --target=browser --format=iife --outfile=/tmp/code-viewer-app.js web-src/app.ts && cmp /tmp/code-viewer-app.js web/app.js && bun build --target=browser --format=esm --outfile=/tmp/code-viewer-mermaid.js web-src/mermaid-entry.ts && cmp /tmp/code-viewer-mermaid.js web/mermaid.js && bun build --target=browser --format=esm --outfile=/tmp/code-viewer-shiki.js web-src/shiki-entry.ts && cmp /tmp/code-viewer-shiki.js web/shiki.js",
|
|
35
|
+
"check:format": "biome format .",
|
|
35
36
|
"dev": "bun run web-src/server/dev.ts",
|
|
36
37
|
"preview": "bun run web-src/server/dev.ts",
|
|
37
38
|
"preview:raw": "bun run web-src/server/preview.ts",
|
|
38
39
|
"test": "bun test",
|
|
39
40
|
"lint": "biome lint web-src/server package.json biome.jsonc",
|
|
40
|
-
"verify": "bun run check && bun run lint && bun run build && bun run check:bundle && bun run test && node --check web/app.js && node --check web/mermaid.js && node --check web/shiki.js && node --check dist/code-viewer.js && node dist/code-viewer.js --help && node scripts/node-smoke.mjs",
|
|
41
|
+
"verify": "bun run check && bun run lint && bun run check:format && bun run build && bun run check:bundle && bun run test && node --check web/app.js && node --check web/mermaid.js && node --check web/shiki.js && node --check dist/code-viewer.js && node dist/code-viewer.js --help && node scripts/node-smoke.mjs",
|
|
41
42
|
"pack:dry": "npm pack --dry-run",
|
|
42
43
|
"prepack": "bun run build",
|
|
43
44
|
"prepublishOnly": "bun run verify"
|