@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.
- package/dist/code-viewer.js +169 -30
- package/package.json +3 -2
- package/web/app.js +489 -211
- 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
|
|
@@ -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
|
-
|
|
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([
|
|
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: {
|
|
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": [
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
"/vendor/
|
|
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 = {
|
|
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 {
|
|
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 = {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
1359
|
-
|
|
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 [
|
|
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, {
|
|
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, {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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({
|
|
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 = {
|
|
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({
|
|
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 = {
|
|
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 = {
|
|
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: {
|
|
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), {
|
|
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({
|
|
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.
|
|
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"
|