@youtyan/code-viewer 0.1.27 → 0.1.28
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 +180 -34
- package/package.json +1 -1
package/dist/code-viewer.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
closeSync,
|
|
6
6
|
constants,
|
|
7
7
|
existsSync as existsSync3,
|
|
8
|
-
lstatSync as
|
|
8
|
+
lstatSync as lstatSync4,
|
|
9
9
|
mkdirSync,
|
|
10
10
|
openSync,
|
|
11
11
|
readFileSync as readFileSync2,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
writeFileSync
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
|
-
import { basename as basename2, dirname as dirname2, extname, join as
|
|
20
|
+
import { basename as basename2, dirname as dirname2, extname, join as join5, relative as relative2 } from "node:path";
|
|
21
21
|
|
|
22
22
|
// web-src/directory-name.ts
|
|
23
23
|
function normalizeNewDirectoryName(name) {
|
|
@@ -1240,9 +1240,150 @@ function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames =
|
|
|
1240
1240
|
return parseRgOutput(normalized, max, omitDirNames, excludeNames);
|
|
1241
1241
|
}
|
|
1242
1242
|
|
|
1243
|
+
// web-src/server/worktree-watcher.ts
|
|
1244
|
+
import {
|
|
1245
|
+
lstatSync as lstatSync3,
|
|
1246
|
+
readdirSync as nodeReaddirSync,
|
|
1247
|
+
watch as nodeWatch
|
|
1248
|
+
} from "node:fs";
|
|
1249
|
+
import { join as join4, relative } from "node:path";
|
|
1250
|
+
function normalizeRelativePath(path) {
|
|
1251
|
+
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1252
|
+
}
|
|
1253
|
+
function isInsideRoot(root, path) {
|
|
1254
|
+
const rel = relative(root, path).replace(/\\/g, "/");
|
|
1255
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
|
|
1256
|
+
}
|
|
1257
|
+
function startWorktreeUpdateWatch(options) {
|
|
1258
|
+
const watch = options.watch || nodeWatch;
|
|
1259
|
+
const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
|
|
1260
|
+
const isDirectory = options.isDirectory || ((path) => {
|
|
1261
|
+
try {
|
|
1262
|
+
return lstatSync3(path).isDirectory();
|
|
1263
|
+
} catch {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
const directorySignature = options.directorySignature || ((path) => {
|
|
1268
|
+
try {
|
|
1269
|
+
const stats = lstatSync3(path);
|
|
1270
|
+
if (!stats.isDirectory())
|
|
1271
|
+
return null;
|
|
1272
|
+
return `${stats.dev}:${stats.ino}`;
|
|
1273
|
+
} catch {
|
|
1274
|
+
return null;
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
const setTimer = options.setTimeoutFn || setTimeout;
|
|
1278
|
+
const clearTimer = options.clearTimeoutFn || clearTimeout;
|
|
1279
|
+
const debounceMs = options.debounceMs ?? 250;
|
|
1280
|
+
const watchers = new Map;
|
|
1281
|
+
const signatures = new Map;
|
|
1282
|
+
let timer = null;
|
|
1283
|
+
const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
|
|
1284
|
+
const scheduleUpdate = () => {
|
|
1285
|
+
if (timer)
|
|
1286
|
+
clearTimer(timer);
|
|
1287
|
+
timer = setTimer(() => {
|
|
1288
|
+
timer = null;
|
|
1289
|
+
options.onUpdate();
|
|
1290
|
+
}, debounceMs);
|
|
1291
|
+
};
|
|
1292
|
+
const closeSubtree = (dir) => {
|
|
1293
|
+
for (const [watchedDir, watcher] of [...watchers]) {
|
|
1294
|
+
if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
|
|
1295
|
+
continue;
|
|
1296
|
+
try {
|
|
1297
|
+
watcher.close?.();
|
|
1298
|
+
} catch {}
|
|
1299
|
+
watchers.delete(watchedDir);
|
|
1300
|
+
signatures.delete(watchedDir);
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
const closeAll = () => {
|
|
1304
|
+
for (const watcher of [...watchers.values()]) {
|
|
1305
|
+
try {
|
|
1306
|
+
watcher.close?.();
|
|
1307
|
+
} catch {}
|
|
1308
|
+
}
|
|
1309
|
+
watchers.clear();
|
|
1310
|
+
signatures.clear();
|
|
1311
|
+
};
|
|
1312
|
+
const watchDirectory = (dir) => {
|
|
1313
|
+
if (watchers.has(dir))
|
|
1314
|
+
return;
|
|
1315
|
+
const rel = normalizeRelativePath(relative(options.root, dir));
|
|
1316
|
+
if (rel && ignored(rel))
|
|
1317
|
+
return;
|
|
1318
|
+
try {
|
|
1319
|
+
const watcher = watch(dir, { persistent: false }, (_event, filename) => {
|
|
1320
|
+
if (!filename) {
|
|
1321
|
+
scheduleUpdate();
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const changed = normalizeRelativePath(join4(rel, filename.toString()));
|
|
1325
|
+
if (ignored(changed))
|
|
1326
|
+
return;
|
|
1327
|
+
const fullChangedPath = join4(options.root, changed);
|
|
1328
|
+
if (!isInsideRoot(options.root, fullChangedPath))
|
|
1329
|
+
return;
|
|
1330
|
+
const known = watchers.has(fullChangedPath);
|
|
1331
|
+
if (isDirectory(fullChangedPath)) {
|
|
1332
|
+
if (known) {
|
|
1333
|
+
const signature2 = directorySignature(fullChangedPath);
|
|
1334
|
+
if (signature2 && signature2 !== signatures.get(fullChangedPath)) {
|
|
1335
|
+
closeSubtree(fullChangedPath);
|
|
1336
|
+
watchDirectory(fullChangedPath);
|
|
1337
|
+
}
|
|
1338
|
+
scheduleUpdate();
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
watchDirectory(fullChangedPath);
|
|
1342
|
+
} else if (known) {
|
|
1343
|
+
closeSubtree(fullChangedPath);
|
|
1344
|
+
}
|
|
1345
|
+
scheduleUpdate();
|
|
1346
|
+
}) || {};
|
|
1347
|
+
watchers.set(dir, watcher);
|
|
1348
|
+
const signature = directorySignature(dir);
|
|
1349
|
+
if (signature)
|
|
1350
|
+
signatures.set(dir, signature);
|
|
1351
|
+
watcher.on?.("error", () => {
|
|
1352
|
+
if (watchers.get(dir) === watcher) {
|
|
1353
|
+
watchers.delete(dir);
|
|
1354
|
+
signatures.delete(dir);
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
watcher.on?.("close", () => {
|
|
1358
|
+
if (watchers.get(dir) === watcher) {
|
|
1359
|
+
watchers.delete(dir);
|
|
1360
|
+
signatures.delete(dir);
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
options.onError?.(error);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
let entries;
|
|
1368
|
+
try {
|
|
1369
|
+
entries = readDirs(dir);
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
options.onError?.(error);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
for (const entry of entries) {
|
|
1375
|
+
if (!entry.isDirectory())
|
|
1376
|
+
continue;
|
|
1377
|
+
watchDirectory(join4(dir, entry.name));
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
watchDirectory(options.root);
|
|
1381
|
+
return { started: watchers.size > 0, close: closeAll };
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1243
1384
|
// web-src/server/preview.ts
|
|
1244
|
-
var WEB_ROOT =
|
|
1245
|
-
var VERSION = JSON.parse(readFileSync2(
|
|
1385
|
+
var WEB_ROOT = join5(ROOT, "web");
|
|
1386
|
+
var VERSION = JSON.parse(readFileSync2(join5(ROOT, "package.json"), "utf8")).version;
|
|
1246
1387
|
var DEFAULT_ARGS = ["HEAD"];
|
|
1247
1388
|
var PREVIEW_HUNKS_DEFAULT = 3;
|
|
1248
1389
|
var PREVIEW_LINES_DEFAULT = 1200;
|
|
@@ -1448,7 +1589,7 @@ function staticFile(pathname) {
|
|
|
1448
1589
|
const spec = map[pathname];
|
|
1449
1590
|
if (!spec)
|
|
1450
1591
|
return null;
|
|
1451
|
-
const full =
|
|
1592
|
+
const full = join5(WEB_ROOT, spec[0]);
|
|
1452
1593
|
if (!existsSync3(full))
|
|
1453
1594
|
return text("not found", 404);
|
|
1454
1595
|
return new Response(readFileSync2(full), {
|
|
@@ -1682,7 +1823,7 @@ function parseScopeExcludeNamesQuery(value) {
|
|
|
1682
1823
|
return normalizeScopeExcludeNames(names);
|
|
1683
1824
|
}
|
|
1684
1825
|
function loadProjectConfig() {
|
|
1685
|
-
const full =
|
|
1826
|
+
const full = join5(cwd, ".code-viewer.json");
|
|
1686
1827
|
if (!existsSync3(full))
|
|
1687
1828
|
return null;
|
|
1688
1829
|
let realCwd;
|
|
@@ -1747,7 +1888,7 @@ function safeWorktreePath(path) {
|
|
|
1747
1888
|
return null;
|
|
1748
1889
|
if (isGitInternalPath(path))
|
|
1749
1890
|
return null;
|
|
1750
|
-
const full =
|
|
1891
|
+
const full = join5(cwd, path);
|
|
1751
1892
|
if (!existsSync3(full))
|
|
1752
1893
|
return null;
|
|
1753
1894
|
let realCwd;
|
|
@@ -1758,7 +1899,7 @@ function safeWorktreePath(path) {
|
|
|
1758
1899
|
} catch {
|
|
1759
1900
|
return null;
|
|
1760
1901
|
}
|
|
1761
|
-
const rel =
|
|
1902
|
+
const rel = relative2(realCwd, realFull);
|
|
1762
1903
|
if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\"))
|
|
1763
1904
|
return null;
|
|
1764
1905
|
if (isGitInternalPath(rel))
|
|
@@ -1766,7 +1907,7 @@ function safeWorktreePath(path) {
|
|
|
1766
1907
|
return realFull;
|
|
1767
1908
|
}
|
|
1768
1909
|
function worktreePath(path) {
|
|
1769
|
-
return
|
|
1910
|
+
return join5(cwd, path);
|
|
1770
1911
|
}
|
|
1771
1912
|
function safeOpenWorktreePath(path) {
|
|
1772
1913
|
if (path === "") {
|
|
@@ -1963,7 +2104,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
|
|
|
1963
2104
|
continue;
|
|
1964
2105
|
let stat;
|
|
1965
2106
|
try {
|
|
1966
|
-
stat =
|
|
2107
|
+
stat = lstatSync4(full);
|
|
1967
2108
|
} catch {
|
|
1968
2109
|
continue;
|
|
1969
2110
|
}
|
|
@@ -2529,8 +2670,8 @@ async function handleUploadFiles(req) {
|
|
|
2529
2670
|
total += file.size;
|
|
2530
2671
|
if (total > MAX_UPLOAD_TOTAL_BYTES)
|
|
2531
2672
|
return text("upload too large", 413);
|
|
2532
|
-
const target =
|
|
2533
|
-
if (
|
|
2673
|
+
const target = join5(realDir, safeName);
|
|
2674
|
+
if (relative2(realDir, dirname2(target)) !== "")
|
|
2534
2675
|
return text("invalid filename", 400);
|
|
2535
2676
|
if (existsSync3(target))
|
|
2536
2677
|
return text("file exists", 409);
|
|
@@ -2557,10 +2698,7 @@ async function handleUploadFiles(req) {
|
|
|
2557
2698
|
return text("file exists", 409);
|
|
2558
2699
|
return text("upload failed", 500);
|
|
2559
2700
|
}
|
|
2560
|
-
|
|
2561
|
-
fileCache.clear();
|
|
2562
|
-
metaCache.clear();
|
|
2563
|
-
sendSse("update");
|
|
2701
|
+
triggerUpdate();
|
|
2564
2702
|
return json({
|
|
2565
2703
|
ok: true,
|
|
2566
2704
|
files: uploads.map((upload) => upload.name),
|
|
@@ -2647,10 +2785,15 @@ function clearMutableCaches() {
|
|
|
2647
2785
|
metaCache.clear();
|
|
2648
2786
|
fileListCache.clear();
|
|
2649
2787
|
}
|
|
2788
|
+
function triggerUpdate() {
|
|
2789
|
+
generation++;
|
|
2790
|
+
clearMutableCaches();
|
|
2791
|
+
sendSse("update");
|
|
2792
|
+
}
|
|
2650
2793
|
function moveMacPathIntoTrash(path) {
|
|
2651
|
-
const trashDir =
|
|
2794
|
+
const trashDir = join5(homedir(), ".Trash");
|
|
2652
2795
|
const base = basename2(path) || "code-viewer-trash-item";
|
|
2653
|
-
const target =
|
|
2796
|
+
const target = join5(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
2654
2797
|
try {
|
|
2655
2798
|
mkdirSync(trashDir, { recursive: true });
|
|
2656
2799
|
renameSync(path, target);
|
|
@@ -2660,7 +2803,7 @@ function moveMacPathIntoTrash(path) {
|
|
|
2660
2803
|
}
|
|
2661
2804
|
}
|
|
2662
2805
|
function movePathToTrash(path) {
|
|
2663
|
-
|
|
2806
|
+
lstatSync4(path);
|
|
2664
2807
|
if (process.platform === "darwin") {
|
|
2665
2808
|
return moveMacPathIntoTrash(path);
|
|
2666
2809
|
}
|
|
@@ -2692,8 +2835,8 @@ function restoreTrashPath(originalPath, trashPath) {
|
|
|
2692
2835
|
if (!existsSync3(trashPath))
|
|
2693
2836
|
return { ok: false, error: "trash item not found" };
|
|
2694
2837
|
try {
|
|
2695
|
-
const trashRoot =
|
|
2696
|
-
const trashRelative =
|
|
2838
|
+
const trashRoot = join5(homedir(), ".Trash");
|
|
2839
|
+
const trashRelative = relative2(trashRoot, trashPath);
|
|
2697
2840
|
if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
|
|
2698
2841
|
return { ok: false, error: "invalid trash handle" };
|
|
2699
2842
|
mkdirSync(dirname2(original), { recursive: true });
|
|
@@ -2799,9 +2942,7 @@ async function handleTrashPath(req) {
|
|
|
2799
2942
|
trashPath: moved.trashPath
|
|
2800
2943
|
}
|
|
2801
2944
|
};
|
|
2802
|
-
|
|
2803
|
-
clearMutableCaches();
|
|
2804
|
-
sendSse("update");
|
|
2945
|
+
triggerUpdate();
|
|
2805
2946
|
return json({ ok: true, generation, undo });
|
|
2806
2947
|
}
|
|
2807
2948
|
async function handleCreateDirectory(req) {
|
|
@@ -2844,7 +2985,7 @@ async function handleCreateDirectory(req) {
|
|
|
2844
2985
|
const targetPath = dir ? `${dir}/${name}` : name;
|
|
2845
2986
|
if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
|
|
2846
2987
|
return text("invalid target", 400);
|
|
2847
|
-
const target =
|
|
2988
|
+
const target = join5(parent, name);
|
|
2848
2989
|
if (existsSync3(target))
|
|
2849
2990
|
return text("already exists", 409);
|
|
2850
2991
|
try {
|
|
@@ -2854,9 +2995,7 @@ async function handleCreateDirectory(req) {
|
|
|
2854
2995
|
return text("already exists", 409);
|
|
2855
2996
|
return text("create failed", 500);
|
|
2856
2997
|
}
|
|
2857
|
-
|
|
2858
|
-
clearMutableCaches();
|
|
2859
|
-
sendSse("update");
|
|
2998
|
+
triggerUpdate();
|
|
2860
2999
|
return json({ ok: true, path: targetPath, generation });
|
|
2861
3000
|
}
|
|
2862
3001
|
async function handleRestoreTrash(req) {
|
|
@@ -2888,9 +3027,7 @@ async function handleRestoreTrash(req) {
|
|
|
2888
3027
|
const restored = restoreTrashPath(originalPath, trashPath || undefined);
|
|
2889
3028
|
if (!restored.ok)
|
|
2890
3029
|
return text(restored.error || "undo failed", 409);
|
|
2891
|
-
|
|
2892
|
-
clearMutableCaches();
|
|
2893
|
-
sendSse("update");
|
|
3030
|
+
triggerUpdate();
|
|
2894
3031
|
return json({ ok: true, generation });
|
|
2895
3032
|
}
|
|
2896
3033
|
function sendSse(event, data = "tick") {
|
|
@@ -2954,9 +3091,7 @@ var server = await startServer({
|
|
|
2954
3091
|
if (url.pathname === "/refresh" && req.method === "POST") {
|
|
2955
3092
|
if (!sideEffectRequestAllowed(req))
|
|
2956
3093
|
return text("forbidden", 403);
|
|
2957
|
-
|
|
2958
|
-
clearMutableCaches();
|
|
2959
|
-
sendSse("update");
|
|
3094
|
+
triggerUpdate();
|
|
2960
3095
|
return json({ ok: true, generation });
|
|
2961
3096
|
}
|
|
2962
3097
|
if (url.pathname === "/events") {
|
|
@@ -3007,5 +3142,16 @@ startDevAssetReload({
|
|
|
3007
3142
|
watch,
|
|
3008
3143
|
sendReload: () => sendSse("reload")
|
|
3009
3144
|
});
|
|
3145
|
+
startWorktreeUpdateWatch({
|
|
3146
|
+
root: cwd,
|
|
3147
|
+
omitDirNames: scopeOmitDirNames,
|
|
3148
|
+
excludeNames: scopeExcludeNames,
|
|
3149
|
+
watch,
|
|
3150
|
+
onUpdate: triggerUpdate,
|
|
3151
|
+
onError: (error) => {
|
|
3152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3153
|
+
console.warn(`code-viewer worktree watch skipped: ${message}`);
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
3010
3156
|
console.log(`GDP_LISTEN_URL=http://127.0.0.1:${server.port}/`);
|
|
3011
3157
|
console.log(`git-diff-preview serving ${cwd}`);
|