@youtyan/code-viewer 0.1.17 → 0.1.18

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.
@@ -1,7 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // web-src/server/preview.ts
4
- import { closeSync, constants, existsSync as existsSync3, lstatSync as lstatSync3, openSync, readFileSync as readFileSync2, realpathSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
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
@@ -1076,6 +1088,7 @@ var cliArgs = DEFAULT_ARGS;
1076
1088
  var listenPort = 0;
1077
1089
  var allowUpload = false;
1078
1090
  var uploadAllowedByCli = false;
1091
+ var openAfterStart = false;
1079
1092
  var scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
1080
1093
  var scopeOmitDirCliOverride = null;
1081
1094
  var rgAvailableCache = null;
@@ -1129,7 +1142,7 @@ Examples:
1129
1142
  }
1130
1143
  listenPort = parsed;
1131
1144
  } else if (arg === "--open") {
1132
- setTimeout(() => openBrowser(`http://127.0.0.1:${server.port}/`), 0);
1145
+ openAfterStart = true;
1133
1146
  } else if (arg === "--allow-upload") {
1134
1147
  allowUpload = true;
1135
1148
  uploadAllowedByCli = true;
@@ -1139,7 +1152,10 @@ Examples:
1139
1152
  console.error("--scope-omit-dir requires a directory name");
1140
1153
  process.exit(1);
1141
1154
  }
1142
- scopeOmitDirCliOverride = normalizeScopeOmitDirNames([...scopeOmitDirCliOverride || [], next]);
1155
+ scopeOmitDirCliOverride = normalizeScopeOmitDirNames([
1156
+ ...scopeOmitDirCliOverride || [],
1157
+ next
1158
+ ]);
1143
1159
  } else {
1144
1160
  rest.push(arg);
1145
1161
  }
@@ -1168,7 +1184,10 @@ function json(data, init = {}) {
1168
1184
  function text(body, status = 200) {
1169
1185
  return new Response(body, {
1170
1186
  status,
1171
- headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }
1187
+ headers: {
1188
+ "Content-Type": "text/plain; charset=utf-8",
1189
+ "Cache-Control": "no-store"
1190
+ }
1172
1191
  });
1173
1192
  }
1174
1193
  function requestAllowed(req) {
@@ -1192,11 +1211,26 @@ function staticFile(pathname) {
1192
1211
  "/app.js": ["app.js", "application/javascript; charset=utf-8"],
1193
1212
  "/mermaid.js": ["mermaid.js", "application/javascript; charset=utf-8"],
1194
1213
  "/shiki.js": ["shiki.js", "application/javascript; charset=utf-8"],
1195
- "/vendor/diff2html/diff2html.min.css": ["vendor/diff2html/diff2html.min.css", "text/css; charset=utf-8"],
1196
- "/vendor/diff2html/diff2html-ui.min.js": ["vendor/diff2html/diff2html-ui.min.js", "application/javascript; charset=utf-8"],
1197
- "/vendor/highlight.js/highlight.min.js": ["vendor/highlight.js/highlight.min.js", "application/javascript; charset=utf-8"],
1198
- "/vendor/highlight.js/styles/github.min.css": ["vendor/highlight.js/styles/github.min.css", "text/css; charset=utf-8"],
1199
- "/vendor/highlight.js/styles/github-dark.min.css": ["vendor/highlight.js/styles/github-dark.min.css", "text/css; charset=utf-8"]
1214
+ "/vendor/diff2html/diff2html.min.css": [
1215
+ "vendor/diff2html/diff2html.min.css",
1216
+ "text/css; charset=utf-8"
1217
+ ],
1218
+ "/vendor/diff2html/diff2html-ui.min.js": [
1219
+ "vendor/diff2html/diff2html-ui.min.js",
1220
+ "application/javascript; charset=utf-8"
1221
+ ],
1222
+ "/vendor/highlight.js/highlight.min.js": [
1223
+ "vendor/highlight.js/highlight.min.js",
1224
+ "application/javascript; charset=utf-8"
1225
+ ],
1226
+ "/vendor/highlight.js/styles/github.min.css": [
1227
+ "vendor/highlight.js/styles/github.min.css",
1228
+ "text/css; charset=utf-8"
1229
+ ],
1230
+ "/vendor/highlight.js/styles/github-dark.min.css": [
1231
+ "vendor/highlight.js/styles/github-dark.min.css",
1232
+ "text/css; charset=utf-8"
1233
+ ]
1200
1234
  };
1201
1235
  for (const spaPath of [...APP_ENTRY_PATHS, ...SPA_PATHS]) {
1202
1236
  map[spaPath] = ["index.html", "text/html; charset=utf-8"];
@@ -1266,7 +1300,14 @@ function buildQuery(params) {
1266
1300
  }
1267
1301
  function fileToMeta(file, range, extraQs) {
1268
1302
  const sizeClass = classify(file);
1269
- const q = { path: file.path, old_path: file.old_path, status: file.status, from: range.from, to: range.to, ...extraQs };
1303
+ const q = {
1304
+ path: file.path,
1305
+ old_path: file.old_path,
1306
+ status: file.status,
1307
+ from: range.from,
1308
+ to: range.to,
1309
+ ...extraQs
1310
+ };
1270
1311
  if (file.untracked)
1271
1312
  Object.assign(q, { untracked: "1" });
1272
1313
  const previewQ = { ...q, mode: "preview", max_hunks: PREVIEW_HUNKS_DEFAULT };
@@ -1326,7 +1367,14 @@ function computePayload(extras, range) {
1326
1367
  }, { files: meta.length, additions: 0, deletions: 0 });
1327
1368
  const toWorktree = !range.to || range.to === "worktree";
1328
1369
  const label = refs2.length ? `${refs2.join(" .. ")}${toWorktree && refs2.length === 1 ? " .. worktree" : ""}` : cliArgs.join(" ");
1329
- return { files: meta, totals, range: label || "HEAD", project: basename2(cwd), branch: currentBranch(cwd) || undefined, generation };
1370
+ return {
1371
+ files: meta,
1372
+ totals,
1373
+ range: label || "HEAD",
1374
+ project: basename2(cwd),
1375
+ branch: currentBranch(cwd) || undefined,
1376
+ generation
1377
+ };
1330
1378
  }
1331
1379
  function handleDiffJson(url) {
1332
1380
  const extras = [];
@@ -1334,7 +1382,10 @@ function handleDiffJson(url) {
1334
1382
  extras.push("-w");
1335
1383
  if (url.searchParams.get("ignore_blank") === "1")
1336
1384
  extras.push("--ignore-blank-lines");
1337
- const range = { from: url.searchParams.get("from") || "", to: url.searchParams.get("to") || "" };
1385
+ const range = {
1386
+ from: url.searchParams.get("from") || "",
1387
+ to: url.searchParams.get("to") || ""
1388
+ };
1338
1389
  const key = `${range.from}|${range.to}|${url.searchParams.get("ignore_ws") || ""}|${url.searchParams.get("ignore_blank") || ""}`;
1339
1390
  if (url.searchParams.get("nocache") === "1") {
1340
1391
  const payload2 = computePayload(extras, range);
@@ -1348,15 +1399,33 @@ function handleDiffJson(url) {
1348
1399
  }
1349
1400
  const body2 = JSON.stringify(payload2);
1350
1401
  setTimedCacheEntry(metaCache, key, { body: body2, sig });
1351
- return new Response(body2, { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" } });
1402
+ return new Response(body2, {
1403
+ headers: {
1404
+ "Content-Type": "application/json; charset=utf-8",
1405
+ "Cache-Control": "no-store"
1406
+ }
1407
+ });
1352
1408
  }
1353
1409
  const cached = metaCache.get(key);
1354
1410
  if (cacheFresh(cached))
1355
- return new Response(cached.body, { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" } });
1411
+ return new Response(cached.body, {
1412
+ headers: {
1413
+ "Content-Type": "application/json; charset=utf-8",
1414
+ "Cache-Control": "no-store"
1415
+ }
1416
+ });
1356
1417
  const payload = computePayload(extras, range);
1357
1418
  const body = JSON.stringify(payload);
1358
- setTimedCacheEntry(metaCache, key, { body, sig: JSON.stringify({ ...payload, generation: undefined }) });
1359
- return new Response(body, { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" } });
1419
+ setTimedCacheEntry(metaCache, key, {
1420
+ body,
1421
+ sig: JSON.stringify({ ...payload, generation: undefined })
1422
+ });
1423
+ return new Response(body, {
1424
+ headers: {
1425
+ "Content-Type": "application/json; charset=utf-8",
1426
+ "Cache-Control": "no-store"
1427
+ }
1428
+ });
1360
1429
  }
1361
1430
  function safePath(path) {
1362
1431
  if (!path || path.startsWith("/") || path.startsWith("\\") || path.includes("\x00"))
@@ -1369,7 +1438,9 @@ function safeRepoPath(path) {
1369
1438
  function normalizeScopeOmitDirNames(names) {
1370
1439
  if (!Array.isArray(names))
1371
1440
  return [];
1372
- return [...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"))].sort((a, b) => a.localeCompare(b));
1441
+ return [
1442
+ ...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"))
1443
+ ].sort((a, b) => a.localeCompare(b));
1373
1444
  }
1374
1445
  function parseScopeOmitDirNamesQuery(value) {
1375
1446
  const names = value ? value.split(",") : [];
@@ -1498,7 +1569,10 @@ function handleTree(url) {
1498
1569
  const recursive = url.searchParams.get("recursive") === "1";
1499
1570
  if (invalidScopeOmitDirNamesQuery(url))
1500
1571
  return text("invalid omit dirs", 400);
1501
- const entries = listTree(target, path, cwd, { recursive, omitDirNames: scopeOmitDirNamesFromQuery(url) }).entries;
1572
+ const entries = listTree(target, path, cwd, {
1573
+ recursive,
1574
+ omitDirNames: scopeOmitDirNamesFromQuery(url)
1575
+ }).entries;
1502
1576
  return json({
1503
1577
  ref: target,
1504
1578
  path,
@@ -1531,7 +1605,10 @@ function handleFiles(url) {
1531
1605
  if (cached && cached.generation === generation)
1532
1606
  return json(cached.body);
1533
1607
  const ref = target || "worktree";
1534
- const entries = listTree(ref, "", cwd, { recursive: true, omitDirNames }).entries;
1608
+ const entries = listTree(ref, "", cwd, {
1609
+ recursive: true,
1610
+ omitDirNames
1611
+ }).entries;
1535
1612
  const body = buildFileSearchList(ref, generation, entries);
1536
1613
  fileListCache.set(key, { generation, body });
1537
1614
  return json(body);
@@ -1584,12 +1661,27 @@ function grepWorktree(query, max, paths, regex, omitDirNames) {
1584
1661
  const proc = runSync(args, cwd, { timeout: 5000 });
1585
1662
  const stdout = proc.stdout;
1586
1663
  const matches2 = parseRgOutput(stdout, max, omitDirNames).filter((match) => safePath(match.path) && !isGitInternalPath(match.path) && !isSkippableSearchPath(match.path, omitDirNames) && !!safeWorktreePath(match.path));
1587
- return { ref: "worktree", engine: "rg", truncated: matches2.length >= max, matches: matches2 };
1664
+ return {
1665
+ ref: "worktree",
1666
+ engine: "rg",
1667
+ truncated: matches2.length >= max,
1668
+ matches: matches2
1669
+ };
1588
1670
  }
1589
1671
  if (regex)
1590
- return { ref: "worktree", engine: "fallback", truncated: false, matches: [] };
1672
+ return {
1673
+ ref: "worktree",
1674
+ engine: "fallback",
1675
+ truncated: false,
1676
+ matches: []
1677
+ };
1591
1678
  const matches = grepWorktreeFallback(query, max, paths, omitDirNames);
1592
- return { ref: "worktree", engine: "fallback", truncated: matches.length >= max, matches };
1679
+ return {
1680
+ ref: "worktree",
1681
+ engine: "fallback",
1682
+ truncated: matches.length >= max,
1683
+ matches
1684
+ };
1593
1685
  }
1594
1686
  function grepTreeRef(ref, query, max, paths, regex, omitDirNames) {
1595
1687
  const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames));
@@ -1624,7 +1716,12 @@ function handleGrep(url) {
1624
1716
  const paths = parseGrepPaths(url, omitDirNames);
1625
1717
  const regex = url.searchParams.get("regex") === "1";
1626
1718
  if (!query.trim())
1627
- return json({ ref, engine: ref === "worktree" ? "fallback" : "git", truncated: false, matches: [] });
1719
+ return json({
1720
+ ref,
1721
+ engine: ref === "worktree" ? "fallback" : "git",
1722
+ truncated: false,
1723
+ matches: []
1724
+ });
1628
1725
  if (ref === "worktree" || ref === "")
1629
1726
  return json(grepWorktree(query, max, paths, regex, omitDirNames));
1630
1727
  if (!verifyTreeRef(ref, cwd))
@@ -1641,7 +1738,10 @@ function handleFileDiff(url) {
1641
1738
  if (url.searchParams.get("ignore_blank") === "1")
1642
1739
  extras.push("--ignore-blank-lines");
1643
1740
  const isUntracked = url.searchParams.get("untracked") === "1";
1644
- const range = { from: url.searchParams.get("from") || "", to: url.searchParams.get("to") || "" };
1741
+ const range = {
1742
+ from: url.searchParams.get("from") || "",
1743
+ to: url.searchParams.get("to") || ""
1744
+ };
1645
1745
  if (isSameWorktreeRange(range)) {
1646
1746
  return json({
1647
1747
  path,
@@ -1661,7 +1761,15 @@ function handleFileDiff(url) {
1661
1761
  const oldPath = url.searchParams.get("old_path");
1662
1762
  let cacheKey;
1663
1763
  try {
1664
- cacheKey = fileDiffCacheKey({ path, oldPath, isUntracked, range, extras, args, cwd });
1764
+ cacheKey = fileDiffCacheKey({
1765
+ path,
1766
+ oldPath,
1767
+ isUntracked,
1768
+ range,
1769
+ extras,
1770
+ args,
1771
+ cwd
1772
+ });
1665
1773
  } catch {
1666
1774
  return text("invalid diff range", 400);
1667
1775
  }
@@ -1850,7 +1958,16 @@ async function handleFileRange(url) {
1850
1958
  if (!full)
1851
1959
  return text("no file", 404);
1852
1960
  const result = await collectIndexedWorktreeLineRange(full, start, end);
1853
- const body = { path, ref, start, end, lines: result.lines, total: result.total, complete: result.complete, generation };
1961
+ const body = {
1962
+ path,
1963
+ ref,
1964
+ start,
1965
+ end,
1966
+ lines: result.lines,
1967
+ total: result.total,
1968
+ complete: result.complete,
1969
+ generation
1970
+ };
1854
1971
  return json(body);
1855
1972
  } else {
1856
1973
  if (!verifyTreeRef(ref, cwd))
@@ -1864,7 +1981,16 @@ async function handleFileRange(url) {
1864
1981
  const result = await collectIndexedGitBlobLineRange(path, oid.oid, size.size, start, end);
1865
1982
  if (!result)
1866
1983
  return text("cannot read ref", 500);
1867
- const body = { path, ref, start, end, lines: result.lines, total: result.total, complete: result.complete, generation };
1984
+ const body = {
1985
+ path,
1986
+ ref,
1987
+ start,
1988
+ end,
1989
+ lines: result.lines,
1990
+ total: result.total,
1991
+ complete: result.complete,
1992
+ generation
1993
+ };
1868
1994
  return json(body);
1869
1995
  }
1870
1996
  }
@@ -1898,7 +2024,11 @@ function handleRawFile(req, url) {
1898
2024
  if (rangeResult?.kind === "unsatisfiable") {
1899
2025
  return new Response(null, {
1900
2026
  status: 416,
1901
- headers: { ...rawFileHeaders(path, size), "Content-Range": `bytes */${size}`, "Content-Length": "0" }
2027
+ headers: {
2028
+ ...rawFileHeaders(path, size),
2029
+ "Content-Range": `bytes */${size}`,
2030
+ "Content-Length": "0"
2031
+ }
1902
2032
  });
1903
2033
  }
1904
2034
  if (rangeResult?.kind === "range") {
@@ -1916,7 +2046,9 @@ function handleRawFile(req, url) {
1916
2046
  }
1917
2047
  if (req.method === "HEAD")
1918
2048
  return new Response(null, { headers: rawFileHeaders(path, size) });
1919
- return new Response(fileReadableStream(full), { headers: rawFileHeaders(path, size) });
2049
+ return new Response(fileReadableStream(full), {
2050
+ headers: rawFileHeaders(path, size)
2051
+ });
1920
2052
  }
1921
2053
  }
1922
2054
  function rawFileSize(path, ref) {
@@ -2080,7 +2212,11 @@ async function handleUploadFiles(req) {
2080
2212
  fileCache.clear();
2081
2213
  metaCache.clear();
2082
2214
  sendSse("update");
2083
- return json({ ok: true, files: uploads.map((upload) => upload.name), generation });
2215
+ return json({
2216
+ ok: true,
2217
+ files: uploads.map((upload) => upload.name),
2218
+ generation
2219
+ });
2084
2220
  }
2085
2221
  function openOsPath(path) {
2086
2222
  const cmd = process.platform === "darwin" ? ["open", "--", path] : process.platform === "win32" ? ["explorer.exe", path] : ["xdg-open", path];
@@ -2224,6 +2360,9 @@ data: ok
2224
2360
  return text("not found", 404);
2225
2361
  }
2226
2362
  });
2363
+ if (openAfterStart) {
2364
+ openBrowser(`http://127.0.0.1:${server.port}/`);
2365
+ }
2227
2366
  startDevAssetReload({
2228
2367
  enabled: process.env.CODE_VIEWER_DEV === "1",
2229
2368
  webRoot: WEB_ROOT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
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 biome.jsonc package.json web-src/app.ts web-src/server/preview.ts web-src/test/source-fixture.ts web-src/test/source-fixture.test.ts web-src/test/view-file-button.test.ts web-src/test/open-path.test.ts web-src/test/markdown-preview.test.ts web-src/test/search-server.test.ts web-src/test/upload-files.test.ts web-src/test/git-truncate.test.ts web-src/test/asset-version-removal.test.ts web-src/test/sidebar-folder-icon.test.ts web-src/test/mark-viewed-button.test.ts",
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"