@youtyan/code-viewer 0.1.18 → 0.1.20
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 +190 -25
- package/package.json +3 -3
- package/web/app.js +352 -166
- package/web/style.css +19 -6
package/dist/code-viewer.js
CHANGED
|
@@ -82,7 +82,13 @@ function startDevAssetReload(options) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// web-src/server/git.ts
|
|
85
|
-
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";
|
|
86
92
|
|
|
87
93
|
// web-src/server/runtime.ts
|
|
88
94
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -235,9 +241,11 @@ async function writeWebResponse(res, response) {
|
|
|
235
241
|
}
|
|
236
242
|
|
|
237
243
|
// web-src/server/git.ts
|
|
238
|
-
import { join as join2 } from "node:path";
|
|
239
244
|
var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32;
|
|
240
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";
|
|
241
249
|
var DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
|
|
242
250
|
"node_modules",
|
|
243
251
|
".venv",
|
|
@@ -291,11 +299,19 @@ function catFileBlobStream(oid, cwd) {
|
|
|
291
299
|
}
|
|
292
300
|
function objectSize(ref, path, cwd) {
|
|
293
301
|
const res = run(["git", "cat-file", "-s", `${ref}:${path}`], cwd);
|
|
294
|
-
return {
|
|
302
|
+
return {
|
|
303
|
+
code: res.code,
|
|
304
|
+
size: Number(res.stdout.trim()) || 0,
|
|
305
|
+
stderr: res.stderr
|
|
306
|
+
};
|
|
295
307
|
}
|
|
296
308
|
function objectByteSize(oid, cwd) {
|
|
297
309
|
const res = run(["git", "cat-file", "-s", oid], cwd);
|
|
298
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
code: res.code,
|
|
312
|
+
size: Number(res.stdout.trim()) || 0,
|
|
313
|
+
stderr: res.stderr
|
|
314
|
+
};
|
|
299
315
|
}
|
|
300
316
|
function objectId(ref, path, cwd) {
|
|
301
317
|
const res = run(["git", "rev-parse", "--verify", `${ref}:${path}`], cwd);
|
|
@@ -316,30 +332,133 @@ function verifyTreeRef(ref, cwd) {
|
|
|
316
332
|
return res.code === 0;
|
|
317
333
|
}
|
|
318
334
|
function refs(cwd) {
|
|
319
|
-
const out = {
|
|
335
|
+
const out = {
|
|
336
|
+
branches: [],
|
|
337
|
+
tags: [],
|
|
338
|
+
commits: [],
|
|
339
|
+
current: ""
|
|
340
|
+
};
|
|
320
341
|
const branches = run([
|
|
321
342
|
"git",
|
|
322
343
|
"for-each-ref",
|
|
323
344
|
"--sort=-committerdate",
|
|
324
|
-
"--format=%(refname:short)",
|
|
345
|
+
"--format=%(refname)%09%(refname:short)%09%(committerdate:iso-strict)",
|
|
325
346
|
"refs/heads",
|
|
326
347
|
"refs/remotes"
|
|
327
348
|
], cwd);
|
|
328
349
|
if (branches.code === 0) {
|
|
329
|
-
|
|
330
|
-
`)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
350
|
+
for (const line of branches.stdout.split(`
|
|
351
|
+
`)) {
|
|
352
|
+
const [fullName, name, when] = line.split("\t");
|
|
353
|
+
if (!fullName || !name || fullName.startsWith("refs/remotes/") && fullName.endsWith("/HEAD"))
|
|
354
|
+
continue;
|
|
355
|
+
out.branches.push({ name, when });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const tags = run([
|
|
359
|
+
"git",
|
|
360
|
+
"for-each-ref",
|
|
361
|
+
"--sort=-creatordate",
|
|
362
|
+
"--format=%(refname:short)%09%(creatordate:iso-strict)",
|
|
363
|
+
"refs/tags"
|
|
364
|
+
], cwd);
|
|
365
|
+
if (tags.code === 0) {
|
|
366
|
+
for (const line of tags.stdout.split(`
|
|
367
|
+
`)) {
|
|
368
|
+
const [name, when] = line.split("\t");
|
|
369
|
+
if (!name)
|
|
370
|
+
continue;
|
|
371
|
+
out.tags.push({ name, when });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
out.commits = refCommits(cwd, "", DEFAULT_REF_COMMIT_LIMIT);
|
|
340
375
|
out.current = currentBranch(cwd) || "";
|
|
341
376
|
return out;
|
|
342
377
|
}
|
|
378
|
+
function clampCommitLimit(max) {
|
|
379
|
+
return Math.max(1, Math.min(max, MAX_REF_COMMIT_LIMIT));
|
|
380
|
+
}
|
|
381
|
+
function parseCommitLog(stdout) {
|
|
382
|
+
const parts = stdout.split("\x00");
|
|
383
|
+
const commits = [];
|
|
384
|
+
for (let index = 0;index < parts.length; ) {
|
|
385
|
+
if (!parts[index]) {
|
|
386
|
+
index++;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const sha = parts[index++] || "";
|
|
390
|
+
const subject = parts[index++] || "";
|
|
391
|
+
const author = parts[index++] || "";
|
|
392
|
+
const when = parts[index++] || "";
|
|
393
|
+
if (sha)
|
|
394
|
+
commits.push({ sha, subject, author, when });
|
|
395
|
+
}
|
|
396
|
+
return commits;
|
|
397
|
+
}
|
|
398
|
+
function commitLogArgs(limit) {
|
|
399
|
+
return [
|
|
400
|
+
"git",
|
|
401
|
+
"log",
|
|
402
|
+
"--all",
|
|
403
|
+
"-z",
|
|
404
|
+
`--max-count=${limit}`,
|
|
405
|
+
`--format=${COMMIT_FORMAT}`
|
|
406
|
+
];
|
|
407
|
+
}
|
|
408
|
+
function mergeCommitResults(limit, ...groups) {
|
|
409
|
+
const seen = new Set;
|
|
410
|
+
const merged = [];
|
|
411
|
+
for (const commits of groups) {
|
|
412
|
+
for (const commit of commits) {
|
|
413
|
+
if (!commit.sha || seen.has(commit.sha))
|
|
414
|
+
continue;
|
|
415
|
+
seen.add(commit.sha);
|
|
416
|
+
merged.push(commit);
|
|
417
|
+
if (merged.length >= limit)
|
|
418
|
+
return merged;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return merged;
|
|
422
|
+
}
|
|
423
|
+
function runCommitLog(cwd, args) {
|
|
424
|
+
const commits = run(args, cwd);
|
|
425
|
+
return commits.code === 0 ? parseCommitLog(commits.stdout) : [];
|
|
426
|
+
}
|
|
427
|
+
function refCommits(cwd, query = "", max = DEFAULT_REF_COMMIT_LIMIT) {
|
|
428
|
+
const limit = clampCommitLimit(max);
|
|
429
|
+
const trimmed = query.trim().slice(0, 200).replace(/\0/g, "");
|
|
430
|
+
const hashMatches = [];
|
|
431
|
+
if (/^[0-9a-f]{4,40}$/i.test(trimmed)) {
|
|
432
|
+
const verified = run(["git", "rev-parse", "--verify", `${trimmed}^{commit}`], cwd);
|
|
433
|
+
const single = run([
|
|
434
|
+
"git",
|
|
435
|
+
"log",
|
|
436
|
+
"-z",
|
|
437
|
+
"-1",
|
|
438
|
+
`--format=${COMMIT_FORMAT}`,
|
|
439
|
+
verified.code === 0 && verified.stdout.trim() ? verified.stdout.trim() : trimmed
|
|
440
|
+
], cwd);
|
|
441
|
+
if (single.code === 0 && single.stdout.trim()) {
|
|
442
|
+
hashMatches.push(...parseCommitLog(single.stdout));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (!trimmed) {
|
|
446
|
+
return runCommitLog(cwd, commitLogArgs(limit));
|
|
447
|
+
}
|
|
448
|
+
const subjectMatches = runCommitLog(cwd, [
|
|
449
|
+
...commitLogArgs(limit),
|
|
450
|
+
"--regexp-ignore-case",
|
|
451
|
+
"--fixed-strings",
|
|
452
|
+
`--grep=${trimmed}`
|
|
453
|
+
]);
|
|
454
|
+
const authorMatches = runCommitLog(cwd, [
|
|
455
|
+
...commitLogArgs(limit),
|
|
456
|
+
"--regexp-ignore-case",
|
|
457
|
+
"--fixed-strings",
|
|
458
|
+
`--author=${trimmed}`
|
|
459
|
+
]);
|
|
460
|
+
return mergeCommitResults(limit, hashMatches, subjectMatches, authorMatches);
|
|
461
|
+
}
|
|
343
462
|
function nameStatus(args, cwd) {
|
|
344
463
|
const res = run([
|
|
345
464
|
"git",
|
|
@@ -366,7 +485,12 @@ function nameStatus(args, cwd) {
|
|
|
366
485
|
const oldPath = parts[i++] || "";
|
|
367
486
|
const path = parts[i++] || "";
|
|
368
487
|
if (path)
|
|
369
|
-
files.push({
|
|
488
|
+
files.push({
|
|
489
|
+
status: kind,
|
|
490
|
+
old_path: oldPath,
|
|
491
|
+
path,
|
|
492
|
+
similarity: Number(status.slice(1)) || undefined
|
|
493
|
+
});
|
|
370
494
|
} else {
|
|
371
495
|
const path = parts[i++] || "";
|
|
372
496
|
if (path)
|
|
@@ -512,7 +636,11 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
|
|
|
512
636
|
continue;
|
|
513
637
|
walk(full, entryPath, depth + 1);
|
|
514
638
|
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
515
|
-
if (!pushRecursiveEntry({
|
|
639
|
+
if (!pushRecursiveEntry({
|
|
640
|
+
name: entry.name,
|
|
641
|
+
path: entryPath,
|
|
642
|
+
type: "blob"
|
|
643
|
+
}))
|
|
516
644
|
return;
|
|
517
645
|
}
|
|
518
646
|
}
|
|
@@ -545,7 +673,11 @@ function gitTreeEntries(ref, path, cwd, recursive) {
|
|
|
545
673
|
if (!match)
|
|
546
674
|
return null;
|
|
547
675
|
const entryPath = match[2];
|
|
548
|
-
return {
|
|
676
|
+
return {
|
|
677
|
+
name: entryPath.split("/").pop() || entryPath,
|
|
678
|
+
path: entryPath,
|
|
679
|
+
type: match[1]
|
|
680
|
+
};
|
|
549
681
|
}).filter((entry) => !!entry);
|
|
550
682
|
if (recursive)
|
|
551
683
|
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -566,7 +698,11 @@ function worktreeFiles(cwd) {
|
|
|
566
698
|
function listTree(ref, path, cwd, options = {}) {
|
|
567
699
|
const base = normalizeTreePath(path);
|
|
568
700
|
if (ref === "worktree") {
|
|
569
|
-
return {
|
|
701
|
+
return {
|
|
702
|
+
code: 0,
|
|
703
|
+
entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames),
|
|
704
|
+
stderr: ""
|
|
705
|
+
};
|
|
570
706
|
}
|
|
571
707
|
const direct = gitTreeEntries(ref, base, cwd, false);
|
|
572
708
|
if (direct.code !== 0 || !options.recursive)
|
|
@@ -574,7 +710,11 @@ function listTree(ref, path, cwd, options = {}) {
|
|
|
574
710
|
const recursive = gitTreeEntries(ref, base, cwd, true);
|
|
575
711
|
if (recursive.code !== 0)
|
|
576
712
|
return recursive;
|
|
577
|
-
return {
|
|
713
|
+
return {
|
|
714
|
+
code: 0,
|
|
715
|
+
entries: combineDirectAndRecursiveFiles(direct.entries, recursive.entries),
|
|
716
|
+
stderr: ""
|
|
717
|
+
};
|
|
578
718
|
}
|
|
579
719
|
function untrackedMeta(cwd) {
|
|
580
720
|
return untracked(cwd).map((path) => {
|
|
@@ -589,7 +729,14 @@ function untrackedMeta(cwd) {
|
|
|
589
729
|
lines = data.toString("utf8").split(`
|
|
590
730
|
`).length - 1;
|
|
591
731
|
}
|
|
592
|
-
return {
|
|
732
|
+
return {
|
|
733
|
+
path,
|
|
734
|
+
status: "A",
|
|
735
|
+
additions: binary ? 0 : lines,
|
|
736
|
+
deletions: 0,
|
|
737
|
+
binary,
|
|
738
|
+
untracked: true
|
|
739
|
+
};
|
|
593
740
|
});
|
|
594
741
|
}
|
|
595
742
|
function fileMeta(args, cwd, includeUntracked = false) {
|
|
@@ -990,7 +1137,12 @@ function buildFileSearchList(ref, generation, entries) {
|
|
|
990
1137
|
}
|
|
991
1138
|
function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
|
|
992
1139
|
const safePaths = paths.length ? paths : ["."];
|
|
993
|
-
const omitGlobs = omitDirNames.flatMap((name) => [
|
|
1140
|
+
const omitGlobs = omitDirNames.flatMap((name) => [
|
|
1141
|
+
"--glob",
|
|
1142
|
+
`!${name}/**`,
|
|
1143
|
+
"--glob",
|
|
1144
|
+
`!**/${name}/**`
|
|
1145
|
+
]);
|
|
994
1146
|
const args = [
|
|
995
1147
|
"rg",
|
|
996
1148
|
"--no-config",
|
|
@@ -1029,7 +1181,12 @@ function parseRgOutput(stdout, max, omitDirNames = []) {
|
|
|
1029
1181
|
const preview = parsed[4];
|
|
1030
1182
|
if (!path || !lineNo || !column || isSkippableSearchPath(path, omitDirNames))
|
|
1031
1183
|
continue;
|
|
1032
|
-
matches.push({
|
|
1184
|
+
matches.push({
|
|
1185
|
+
path,
|
|
1186
|
+
line: lineNo,
|
|
1187
|
+
column,
|
|
1188
|
+
preview: preview.slice(0, 500)
|
|
1189
|
+
});
|
|
1033
1190
|
}
|
|
1034
1191
|
return matches;
|
|
1035
1192
|
}
|
|
@@ -1728,6 +1885,12 @@ function handleGrep(url) {
|
|
|
1728
1885
|
return text("invalid target", 400);
|
|
1729
1886
|
return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames));
|
|
1730
1887
|
}
|
|
1888
|
+
function handleRefCommits(url) {
|
|
1889
|
+
const query = url.searchParams.get("q") || "";
|
|
1890
|
+
const parsedMax = Number(url.searchParams.get("max") || "");
|
|
1891
|
+
const max = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : undefined;
|
|
1892
|
+
return json({ commits: refCommits(cwd, query, max) });
|
|
1893
|
+
}
|
|
1731
1894
|
function handleFileDiff(url) {
|
|
1732
1895
|
const path = url.searchParams.get("path") || "";
|
|
1733
1896
|
if (!safePath(path))
|
|
@@ -2300,6 +2463,8 @@ var server = await startServer({
|
|
|
2300
2463
|
return handleFiles(url);
|
|
2301
2464
|
if (url.pathname === "/_grep")
|
|
2302
2465
|
return handleGrep(url);
|
|
2466
|
+
if (url.pathname === "/_commits")
|
|
2467
|
+
return handleRefCommits(url);
|
|
2303
2468
|
if (url.pathname === "/file_diff")
|
|
2304
2469
|
return handleFileDiff(url);
|
|
2305
2470
|
if (url.pathname === "/file_range")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youtyan/code-viewer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Local browser-based code and git diff viewer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,12 +32,12 @@
|
|
|
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
|
+
"check:format": "biome format .",
|
|
36
36
|
"dev": "bun run web-src/server/dev.ts",
|
|
37
37
|
"preview": "bun run web-src/server/dev.ts",
|
|
38
38
|
"preview:raw": "bun run web-src/server/preview.ts",
|
|
39
39
|
"test": "bun test",
|
|
40
|
-
"lint": "biome lint web-src
|
|
40
|
+
"lint": "biome lint web-src package.json biome.jsonc",
|
|
41
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",
|
|
42
42
|
"pack:dry": "npm pack --dry-run",
|
|
43
43
|
"prepack": "bun run build",
|