@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.
Files changed (2) hide show
  1. package/dist/code-viewer.js +180 -34
  2. package/package.json +1 -1
@@ -5,7 +5,7 @@ import {
5
5
  closeSync,
6
6
  constants,
7
7
  existsSync as existsSync3,
8
- lstatSync as lstatSync3,
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 join4, relative } from "node:path";
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 = join4(ROOT, "web");
1245
- var VERSION = JSON.parse(readFileSync2(join4(ROOT, "package.json"), "utf8")).version;
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 = join4(WEB_ROOT, spec[0]);
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 = join4(cwd, ".code-viewer.json");
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 = join4(cwd, path);
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 = relative(realCwd, realFull);
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 join4(cwd, path);
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 = lstatSync3(full);
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 = join4(realDir, safeName);
2533
- if (relative(realDir, dirname2(target)) !== "")
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
- generation++;
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 = join4(homedir(), ".Trash");
2794
+ const trashDir = join5(homedir(), ".Trash");
2652
2795
  const base = basename2(path) || "code-viewer-trash-item";
2653
- const target = join4(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
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
- lstatSync3(path);
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 = join4(homedir(), ".Trash");
2696
- const trashRelative = relative(trashRoot, trashPath);
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
- generation++;
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 = join4(parent, name);
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
- generation++;
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
- generation++;
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
- generation++;
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {