@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.
- package/README.md +56 -2
- package/package.json +13 -5
- package/web/app.js +7340 -526
- package/web/index.html +7 -3
- package/web/mermaid.js +156840 -0
- package/web/shiki.js +13182 -0
- package/web/style.css +719 -30
- package/web-src/server/dev-assets.ts +37 -0
- package/web-src/server/dev.ts +6 -1
- package/web-src/server/git.ts +114 -7
- package/web-src/server/preview.ts +309 -20
- package/web-src/server/runtime.d.ts +6 -0
- package/web-src/types.ts +2 -4
|
@@ -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
|
+
}
|
package/web-src/server/dev.ts
CHANGED
|
@@ -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
|
-
], {
|
|
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() {
|
package/web-src/server/git.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|