@youtyan/code-viewer 0.1.6 → 0.1.8
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/package.json +1 -1
- package/web/app.js +39 -16
- package/web-src/server/cache.ts +13 -0
- package/web-src/server/dev-assets.ts +37 -0
- package/web-src/server/dev.ts +6 -1
- package/web-src/server/preview.ts +22 -13
- package/web-src/server/runtime.d.ts +6 -0
- package/web-src/types.ts +0 -4
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -109,6 +109,21 @@
|
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// web-src/catch-up.ts
|
|
113
|
+
function shouldCatchUpDiff(route) {
|
|
114
|
+
return route.screen !== "repo" && !(route.screen === "file" && route.view === "blob");
|
|
115
|
+
}
|
|
116
|
+
function createCatchUpGate(now, minIntervalMs) {
|
|
117
|
+
let lastForceAt = 0;
|
|
118
|
+
return function shouldRun() {
|
|
119
|
+
const current = now();
|
|
120
|
+
if (current - lastForceAt < minIntervalMs)
|
|
121
|
+
return false;
|
|
122
|
+
lastForceAt = current;
|
|
123
|
+
return true;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
112
127
|
// web-src/routes.ts
|
|
113
128
|
function assertNever(value) {
|
|
114
129
|
throw new Error("unhandled route: " + JSON.stringify(value));
|
|
@@ -2686,7 +2701,7 @@
|
|
|
2686
2701
|
syncHeaderMenu();
|
|
2687
2702
|
}).catch(() => setStatus("error"));
|
|
2688
2703
|
}
|
|
2689
|
-
function load() {
|
|
2704
|
+
function load(options = {}) {
|
|
2690
2705
|
if (STATE.route.screen === "repo")
|
|
2691
2706
|
return loadRepo();
|
|
2692
2707
|
setStatus("refreshing");
|
|
@@ -2697,6 +2712,8 @@
|
|
|
2697
2712
|
params.set("from", STATE.from);
|
|
2698
2713
|
if (STATE.to)
|
|
2699
2714
|
params.set("to", STATE.to);
|
|
2715
|
+
if (options.force)
|
|
2716
|
+
params.set("nocache", "1");
|
|
2700
2717
|
const url = "/diff.json" + (params.toString() ? "?" + params.toString() : "");
|
|
2701
2718
|
return trackLoad(fetch(url).then((r) => r.json())).then((data) => {
|
|
2702
2719
|
renderShell(data);
|
|
@@ -3033,24 +3050,30 @@
|
|
|
3033
3050
|
}, 350);
|
|
3034
3051
|
}
|
|
3035
3052
|
const es = new EventSource("/events");
|
|
3053
|
+
const catchUpGate = createCatchUpGate(() => Date.now(), 1000);
|
|
3054
|
+
let openedOnce = false;
|
|
3036
3055
|
es.addEventListener("update", () => scheduleSseLoad());
|
|
3037
3056
|
es.addEventListener("reload", () => location.reload());
|
|
3038
3057
|
es.addEventListener("error", () => setStatus("error"));
|
|
3039
|
-
es.addEventListener("open", () =>
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3058
|
+
es.addEventListener("open", () => {
|
|
3059
|
+
setStatus("live");
|
|
3060
|
+
if (!openedOnce) {
|
|
3061
|
+
openedOnce = true;
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
catchUpDiff();
|
|
3065
|
+
});
|
|
3066
|
+
function catchUpDiff() {
|
|
3067
|
+
if (!shouldCatchUpDiff(STATE.route))
|
|
3068
|
+
return;
|
|
3069
|
+
if (!catchUpGate())
|
|
3070
|
+
return;
|
|
3071
|
+
load({ force: true });
|
|
3052
3072
|
}
|
|
3053
|
-
|
|
3054
|
-
|
|
3073
|
+
document.addEventListener("visibilitychange", () => {
|
|
3074
|
+
if (!document.hidden)
|
|
3075
|
+
catchUpDiff();
|
|
3076
|
+
});
|
|
3077
|
+
window.addEventListener("focus", catchUpDiff);
|
|
3055
3078
|
})();
|
|
3056
3079
|
})();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Short enough that a browser reload self-heals stale git data, while still
|
|
2
|
+
// coalescing bursts from one render pass.
|
|
3
|
+
export const CACHE_TTL_MS = 1500;
|
|
4
|
+
|
|
5
|
+
export type TimedCacheEntry<T> = T & { storedAt: number };
|
|
6
|
+
|
|
7
|
+
export function cacheFresh<T>(
|
|
8
|
+
cached: TimedCacheEntry<T> | undefined,
|
|
9
|
+
now = Date.now(),
|
|
10
|
+
ttlMs = CACHE_TTL_MS,
|
|
11
|
+
): cached is TimedCacheEntry<T> {
|
|
12
|
+
return !!cached && now - cached.storedAt <= ttlMs;
|
|
13
|
+
}
|
|
@@ -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() {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readFileSync, realpathSync, statSync, watch } from 'node:fs';
|
|
4
4
|
import { basename, extname, join, normalize, relative } from 'node:path';
|
|
5
5
|
import { APP_ENTRY_PATHS, SPA_PATHS } from '../routes';
|
|
6
6
|
import type { DiffMeta, FileDiffResponse, FileMeta, FileRangeResponse, RepoTreeResponse } from '../types';
|
|
7
|
+
import { cacheFresh, type TimedCacheEntry } from './cache';
|
|
8
|
+
import { startDevAssetReload } from './dev-assets';
|
|
7
9
|
import * as git from './git';
|
|
8
10
|
import { isSameWorktreeRange } from './range';
|
|
9
11
|
|
|
@@ -24,8 +26,8 @@ let listenPort = 0;
|
|
|
24
26
|
|
|
25
27
|
const enc = new TextEncoder();
|
|
26
28
|
const sseClients = new Set<ReadableStreamDefaultController<Uint8Array>>();
|
|
27
|
-
const fileCache = new Map<string, string
|
|
28
|
-
const metaCache = new Map<string, { body: string; sig: string }
|
|
29
|
+
const fileCache = new Map<string, TimedCacheEntry<{ diffText: string }>>();
|
|
30
|
+
const metaCache = new Map<string, TimedCacheEntry<{ body: string; sig: string }>>();
|
|
29
31
|
|
|
30
32
|
function parseCli() {
|
|
31
33
|
const rest: string[] = [];
|
|
@@ -242,14 +244,14 @@ function handleDiffJson(url: URL) {
|
|
|
242
244
|
fileCache.clear();
|
|
243
245
|
}
|
|
244
246
|
const body = JSON.stringify(payload);
|
|
245
|
-
metaCache.set(key, { body, sig });
|
|
247
|
+
metaCache.set(key, { body, sig, storedAt: Date.now() });
|
|
246
248
|
return new Response(body, { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' } });
|
|
247
249
|
}
|
|
248
250
|
const cached = metaCache.get(key);
|
|
249
|
-
if (cached) return new Response(cached.body, { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' } });
|
|
251
|
+
if (cacheFresh(cached)) return new Response(cached.body, { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' } });
|
|
250
252
|
const payload = computePayload(extras, range);
|
|
251
253
|
const body = JSON.stringify(payload);
|
|
252
|
-
metaCache.set(key, { body, sig: JSON.stringify({ ...payload, generation: undefined }) });
|
|
254
|
+
metaCache.set(key, { body, sig: JSON.stringify({ ...payload, generation: undefined }), storedAt: Date.now() });
|
|
253
255
|
return new Response(body, { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' } });
|
|
254
256
|
}
|
|
255
257
|
|
|
@@ -343,9 +345,12 @@ function handleFileDiff(url: URL) {
|
|
|
343
345
|
const cacheKey = isUntracked
|
|
344
346
|
? `u\0${path}\0${extras.join('\0')}`
|
|
345
347
|
: `t\0${path}\0${oldPath || ''}\0${[...extras, ...args].join('\0')}`;
|
|
346
|
-
|
|
348
|
+
const cached = fileCache.get(cacheKey);
|
|
349
|
+
let diffText: string;
|
|
347
350
|
let errText = '';
|
|
348
|
-
if (
|
|
351
|
+
if (cacheFresh(cached)) {
|
|
352
|
+
diffText = cached.diffText;
|
|
353
|
+
} else {
|
|
349
354
|
if (isUntracked) {
|
|
350
355
|
diffText = git.untrackedFileDiff(extras, path, cwd).stdout || '';
|
|
351
356
|
} else {
|
|
@@ -353,7 +358,7 @@ function handleFileDiff(url: URL) {
|
|
|
353
358
|
diffText = res.stdout || '';
|
|
354
359
|
if (res.code !== 0) errText = res.stderr;
|
|
355
360
|
}
|
|
356
|
-
fileCache.set(cacheKey, diffText);
|
|
361
|
+
fileCache.set(cacheKey, { diffText, storedAt: Date.now() });
|
|
357
362
|
}
|
|
358
363
|
const mode = url.searchParams.get('mode') || 'full';
|
|
359
364
|
const truncated = mode === 'preview'
|
|
@@ -454,10 +459,6 @@ const server = Bun.serve({
|
|
|
454
459
|
if (url.pathname === '/file_range') return handleFileRange(url);
|
|
455
460
|
if (url.pathname === '/_file') return handleRawFile(url);
|
|
456
461
|
if (url.pathname === '/_refs') return json(git.refs(cwd));
|
|
457
|
-
if (url.pathname === '/_asset_version') {
|
|
458
|
-
const version = Math.max(...WATCHED_ASSET_FILES.map((name) => statSync(join(WEB_ROOT, name)).mtimeMs));
|
|
459
|
-
return json({ version });
|
|
460
|
-
}
|
|
461
462
|
if (url.pathname === '/refresh' && req.method === 'POST') {
|
|
462
463
|
generation++;
|
|
463
464
|
fileCache.clear();
|
|
@@ -498,5 +499,13 @@ const server = Bun.serve({
|
|
|
498
499
|
},
|
|
499
500
|
});
|
|
500
501
|
|
|
502
|
+
startDevAssetReload({
|
|
503
|
+
enabled: process.env.CODE_VIEWER_DEV === '1',
|
|
504
|
+
webRoot: WEB_ROOT,
|
|
505
|
+
watchedFiles: WATCHED_ASSET_FILES,
|
|
506
|
+
watch,
|
|
507
|
+
sendReload: () => sendSse('reload'),
|
|
508
|
+
});
|
|
509
|
+
|
|
501
510
|
console.log(`GDP_LISTEN_URL=http://127.0.0.1:${server.port}/`);
|
|
502
511
|
console.log(`git-diff-preview serving ${cwd}`);
|
|
@@ -17,6 +17,7 @@ declare const Bun: {
|
|
|
17
17
|
|
|
18
18
|
declare const process: {
|
|
19
19
|
argv: string[];
|
|
20
|
+
env: Record<string, string | undefined>;
|
|
20
21
|
cwd(): string;
|
|
21
22
|
platform: 'darwin' | 'win32' | string;
|
|
22
23
|
on(event: 'SIGINT' | 'SIGTERM', listener: () => void): void;
|
|
@@ -34,6 +35,11 @@ declare module 'node:fs' {
|
|
|
34
35
|
export function readFileSync(path: string, encoding: BufferEncoding): string;
|
|
35
36
|
export function realpathSync(path: string): string;
|
|
36
37
|
export function statSync(path: string): { mtimeMs: number };
|
|
38
|
+
export function watch(
|
|
39
|
+
path: string,
|
|
40
|
+
options: { persistent?: boolean },
|
|
41
|
+
listener: (eventType: string, filename: string | Buffer | null) => void,
|
|
42
|
+
): unknown;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
declare module 'node:path' {
|