@youtyan/code-viewer 0.1.7 → 0.1.9

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.
@@ -0,0 +1,37 @@
1
+ import { basename } from 'node:path';
2
+
3
+ export type WatchFn = (
4
+ path: string,
5
+ options: { persistent?: boolean },
6
+ listener: (eventType: string, filename: string | Buffer | null) => void,
7
+ ) => unknown;
8
+
9
+ type DevAssetReloadOptions = {
10
+ enabled: boolean;
11
+ webRoot: string;
12
+ watchedFiles: readonly string[];
13
+ watch: WatchFn;
14
+ sendReload: () => void;
15
+ setTimeoutFn?: typeof setTimeout;
16
+ clearTimeoutFn?: typeof clearTimeout;
17
+ debounceMs?: number;
18
+ };
19
+
20
+ export function startDevAssetReload(options: DevAssetReloadOptions): boolean {
21
+ if (!options.enabled) return false;
22
+ const watched = new Set(options.watchedFiles);
23
+ const setTimer = options.setTimeoutFn || setTimeout;
24
+ const clearTimer = options.clearTimeoutFn || clearTimeout;
25
+ const debounceMs = options.debounceMs ?? 150;
26
+ let timer: ReturnType<typeof setTimeout> | null = null;
27
+
28
+ options.watch(options.webRoot, { persistent: false }, (_event, filename) => {
29
+ if (!filename || !watched.has(basename(filename.toString()))) return;
30
+ if (timer) clearTimer(timer);
31
+ timer = setTimer(() => {
32
+ timer = null;
33
+ options.sendReload();
34
+ }, debounceMs);
35
+ });
36
+ return true;
37
+ }
@@ -56,7 +56,12 @@ function startServer() {
56
56
  firstStart = false;
57
57
  server = Bun.spawn([
58
58
  'bun', 'run', 'web-src/server/preview.ts', ...args,
59
- ], { cwd: ROOT, stdout: 'inherit', stderr: 'inherit' }) as ChildProcess;
59
+ ], {
60
+ cwd: ROOT,
61
+ stdout: 'inherit',
62
+ stderr: 'inherit',
63
+ env: { ...process.env, CODE_VIEWER_DEV: '1' },
64
+ }) as ChildProcess;
60
65
  }
61
66
 
62
67
  async function restartServer() {
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
1
+ import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
 
4
4
  export type GitFileMeta = {
@@ -17,6 +17,7 @@ export type GitTreeEntry = {
17
17
  name: string;
18
18
  path: string;
19
19
  type: 'tree' | 'blob' | 'commit';
20
+ children_omitted?: true;
20
21
  };
21
22
 
22
23
  function run(args: string[], cwd: string): { code: number; stdout: string; stderr: string } {
@@ -28,6 +29,15 @@ function run(args: string[], cwd: string): { code: number; stdout: string; stder
28
29
  };
29
30
  }
30
31
 
32
+ function runBytes(args: string[], cwd: string): { code: number; stdout: Uint8Array; stderr: string } {
33
+ const proc = Bun.spawnSync(args, { cwd, stdout: 'pipe', stderr: 'pipe' });
34
+ return {
35
+ code: proc.exitCode,
36
+ stdout: new Uint8Array(proc.stdout),
37
+ stderr: new TextDecoder().decode(proc.stderr),
38
+ };
39
+ }
40
+
31
41
  export function repoRoot(cwd: string): string | null {
32
42
  const res = run(['git', 'rev-parse', '--show-toplevel'], cwd);
33
43
  return res.code === 0 ? res.stdout.trimEnd() : null;
@@ -42,6 +52,15 @@ export function show(ref: string, path: string, cwd: string): { code: number; st
42
52
  return run(['git', 'show', `${ref}:${path}`], cwd);
43
53
  }
44
54
 
55
+ export function showBytes(ref: string, path: string, cwd: string): { code: number; stdout: Uint8Array; stderr: string } {
56
+ return runBytes(['git', 'show', `${ref}:${path}`], cwd);
57
+ }
58
+
59
+ export function objectSize(ref: string, path: string, cwd: string): { code: number; size: number; stderr: string } {
60
+ const res = run(['git', 'cat-file', '-s', `${ref}:${path}`], cwd);
61
+ return { code: res.code, size: Number(res.stdout.trim()) || 0, stderr: res.stderr };
62
+ }
63
+
45
64
  export function verifyTreeRef(ref: string, cwd: string): boolean {
46
65
  if (!ref || ref === 'worktree') return false;
47
66
  if (ref.startsWith('-')) return false;
@@ -143,10 +162,13 @@ function worktreeDirectChildren(cwd: string, path: string): GitTreeEntry[] {
143
162
  try {
144
163
  return sortTreeEntries(readdirSync(dir, { withFileTypes: true }).map((entry) => {
145
164
  const entryPath = base ? `${base}/${entry.name}` : entry.name;
165
+ const type = entry.isDirectory()
166
+ ? hasDotGitEntry(join(dir, entry.name)) ? 'commit' as const : 'tree' as const
167
+ : 'blob' as const;
146
168
  return {
147
169
  name: entry.name,
148
170
  path: entryPath,
149
- type: entry.isDirectory() ? 'tree' as const : 'blob' as const,
171
+ type,
150
172
  };
151
173
  }));
152
174
  } catch {
@@ -154,6 +176,15 @@ function worktreeDirectChildren(cwd: string, path: string): GitTreeEntry[] {
154
176
  }
155
177
  }
156
178
 
179
+ function hasDotGitEntry(dir: string): boolean {
180
+ try {
181
+ lstatSync(join(dir, '.git'));
182
+ return true;
183
+ } catch (err) {
184
+ return !!err && typeof err === 'object' && 'code' in err && err.code !== 'ENOENT';
185
+ }
186
+ }
187
+
157
188
  function worktreeRecursiveFiles(cwd: string, path: string): GitTreeEntry[] {
158
189
  const base = normalizeTreePath(path);
159
190
  const trackedArgs = ['git', '-c', 'core.quotepath=false', 'ls-files', '-z'];
@@ -196,6 +227,31 @@ function combineDirectAndRecursiveFiles(directEntries: GitTreeEntry[], fileEntri
196
227
  ];
197
228
  }
198
229
 
230
+ function ignoredWorktreePaths(cwd: string, path: string): Set<string> {
231
+ const base = normalizeTreePath(path);
232
+ const args = ['git', '-c', 'core.quotepath=false', 'status', '--ignored', '--porcelain=v1', '-z', '--'];
233
+ if (base) args.push(`${base}/`);
234
+ const res = run(args, cwd);
235
+ const ignored = new Set<string>();
236
+ if (res.code !== 0) return ignored;
237
+ const records = res.stdout.split('\0').filter(Boolean);
238
+ for (let i = 0; i < records.length; i++) {
239
+ const rec = records[i];
240
+ if (!rec.startsWith('!! ')) continue;
241
+ const path = rec.slice(3).replace(/\/+$/g, '');
242
+ if (path) ignored.add(path);
243
+ }
244
+ return ignored;
245
+ }
246
+
247
+ function annotateOmittedWorktreeChildren(entries: GitTreeEntry[], ignoredPaths: Set<string>): GitTreeEntry[] {
248
+ return entries.map((entry) => {
249
+ return entry.type === 'tree' && ignoredPaths.has(entry.path)
250
+ ? { ...entry, children_omitted: true }
251
+ : entry;
252
+ });
253
+ }
254
+
199
255
  export function worktreeEntries(cwd: string, path: string): GitTreeEntry[] {
200
256
  return listTree('worktree', path, cwd).entries;
201
257
  }
@@ -221,8 +277,11 @@ export function listTree(
221
277
  const base = normalizeTreePath(path);
222
278
  if (ref === 'worktree') {
223
279
  const directEntries = worktreeDirectChildren(cwd, base);
224
- if (!options.recursive) return { code: 0, entries: directEntries, stderr: '' };
225
- return { code: 0, entries: combineDirectAndRecursiveFiles(directEntries, worktreeRecursiveFiles(cwd, base)), stderr: '' };
280
+ const ignoredPaths = ignoredWorktreePaths(cwd, base);
281
+ const annotatedDirectEntries = annotateOmittedWorktreeChildren(directEntries, ignoredPaths);
282
+ if (!options.recursive) return { code: 0, entries: annotatedDirectEntries, stderr: '' };
283
+ const recursiveEntries = worktreeRecursiveFiles(cwd, base);
284
+ return { code: 0, entries: combineDirectAndRecursiveFiles(annotatedDirectEntries, recursiveEntries), stderr: '' };
226
285
  }
227
286
 
228
287
  const direct = gitTreeEntries(ref, base, cwd, false);
@@ -302,17 +361,65 @@ export function truncateToNHunks(diffText: string, n: number): {
302
361
  totalHunks: number;
303
362
  renderedHunks: number;
304
363
  lineCount: number;
364
+ lineTruncated: boolean;
365
+ };
366
+ export function truncateToNHunks(diffText: string, n: number, maxLines: number): {
367
+ text: string;
368
+ totalHunks: number;
369
+ renderedHunks: number;
370
+ lineCount: number;
371
+ lineTruncated: boolean;
372
+ };
373
+ export function truncateToNHunks(diffText: string, n: number, maxLines = Number.POSITIVE_INFINITY): {
374
+ text: string;
375
+ totalHunks: number;
376
+ renderedHunks: number;
377
+ lineCount: number;
378
+ lineTruncated: boolean;
305
379
  } {
306
380
  const { header, hunks } = splitHunks(diffText);
307
381
  if (hunks.length === 0) {
308
- return { text: diffText, totalHunks: 0, renderedHunks: 0, lineCount: (diffText.match(/\n/g) || []).length };
382
+ const lines = diffText.split('\n');
383
+ const lineTruncated = Number.isFinite(maxLines) && lines.length > maxLines;
384
+ const text = lineTruncated ? lines.slice(0, maxLines).join('\n') : diffText;
385
+ return {
386
+ text,
387
+ totalHunks: 0,
388
+ renderedHunks: 0,
389
+ lineCount: (text.match(/\n/g) || []).length,
390
+ lineTruncated,
391
+ };
392
+ }
393
+ const maxHunks = Math.min(n, hunks.length);
394
+ const rendered: string[] = [];
395
+ let renderedHunks = 0;
396
+ let usedLines = (header.match(/\n/g) || []).length;
397
+ let lineTruncated = false;
398
+ for (let index = 0; index < maxHunks; index++) {
399
+ const hunk = hunks[index];
400
+ const lines = hunk.split('\n');
401
+ const separatorLines = rendered.length > 0 ? 1 : 0;
402
+ const remaining = maxLines - usedLines - separatorLines;
403
+ if (remaining <= 0) {
404
+ lineTruncated = true;
405
+ break;
406
+ }
407
+ if (Number.isFinite(maxLines) && lines.length > remaining) {
408
+ rendered.push(lines.slice(0, remaining).join('\n'));
409
+ renderedHunks++;
410
+ lineTruncated = true;
411
+ break;
412
+ }
413
+ rendered.push(hunk);
414
+ renderedHunks++;
415
+ usedLines += separatorLines + lines.length;
309
416
  }
310
- const renderedHunks = Math.min(n, hunks.length);
311
- const text = header + hunks.slice(0, renderedHunks).join('\n');
417
+ const text = header + rendered.join('\n');
312
418
  return {
313
419
  text,
314
420
  totalHunks: hunks.length,
315
421
  renderedHunks,
316
422
  lineCount: (text.match(/\n/g) || []).length,
423
+ lineTruncated,
317
424
  };
318
425
  }