@youtyan/code-viewer 0.1.14 → 0.1.16
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 +9 -0
- package/package.json +1 -1
- package/web/app.js +875 -109
- package/web/index.html +45 -8
- package/web/style.css +414 -72
- package/web-src/server/git.ts +90 -29
- package/web-src/server/preview.ts +301 -49
- package/web-src/server/range.ts +228 -0
- package/web-src/server/runtime.d.ts +10 -0
- package/web-src/server/search.ts +10 -7
- package/web-src/types.ts +16 -1
package/web-src/server/git.ts
CHANGED
|
@@ -18,10 +18,39 @@ export type GitTreeEntry = {
|
|
|
18
18
|
path: string;
|
|
19
19
|
type: 'tree' | 'blob' | 'commit';
|
|
20
20
|
children_omitted?: true;
|
|
21
|
-
children_omitted_reason?: '
|
|
21
|
+
children_omitted_reason?: 'heavy' | 'internal' | 'truncated';
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const WORKTREE_RECURSIVE_DEPTH_LIMIT = 32;
|
|
25
|
+
export const WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000;
|
|
26
|
+
export const DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
|
|
27
|
+
'node_modules',
|
|
28
|
+
'.venv',
|
|
29
|
+
'venv',
|
|
30
|
+
'.next',
|
|
31
|
+
'.nuxt',
|
|
32
|
+
'.svelte-kit',
|
|
33
|
+
'.astro',
|
|
34
|
+
'.vercel',
|
|
35
|
+
'dist',
|
|
36
|
+
'build',
|
|
37
|
+
'out',
|
|
38
|
+
'target',
|
|
39
|
+
'.gradle',
|
|
40
|
+
'__pycache__',
|
|
41
|
+
'.pytest_cache',
|
|
42
|
+
'.tox',
|
|
43
|
+
'.terraform',
|
|
44
|
+
'.idea',
|
|
45
|
+
'.vscode',
|
|
46
|
+
'vendor',
|
|
47
|
+
'.cache',
|
|
48
|
+
'coverage',
|
|
49
|
+
'DerivedData',
|
|
50
|
+
'Pods',
|
|
51
|
+
'bin',
|
|
52
|
+
'obj',
|
|
53
|
+
];
|
|
25
54
|
|
|
26
55
|
function run(args: string[], cwd: string): { code: number; stdout: string; stderr: string } {
|
|
27
56
|
const proc = Bun.spawnSync(args, { cwd, stdout: 'pipe', stderr: 'pipe' });
|
|
@@ -59,11 +88,34 @@ export function showBytes(ref: string, path: string, cwd: string): { code: numbe
|
|
|
59
88
|
return runBytes(['git', 'show', `${ref}:${path}`], cwd);
|
|
60
89
|
}
|
|
61
90
|
|
|
91
|
+
export function catFileBlobStream(oid: string, cwd: string): { stream: ReadableStream<Uint8Array>; exited: Promise<number>; kill(signal?: string): void } {
|
|
92
|
+
const proc = Bun.spawn(['git', 'cat-file', 'blob', oid], { cwd, stdout: 'pipe', stderr: 'ignore', stdin: 'ignore' });
|
|
93
|
+
return {
|
|
94
|
+
stream: proc.stdout as ReadableStream<Uint8Array>,
|
|
95
|
+
exited: proc.exited,
|
|
96
|
+
kill: (signal?: string) => proc.kill(signal),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
62
100
|
export function objectSize(ref: string, path: string, cwd: string): { code: number; size: number; stderr: string } {
|
|
63
101
|
const res = run(['git', 'cat-file', '-s', `${ref}:${path}`], cwd);
|
|
64
102
|
return { code: res.code, size: Number(res.stdout.trim()) || 0, stderr: res.stderr };
|
|
65
103
|
}
|
|
66
104
|
|
|
105
|
+
export function objectByteSize(oid: string, cwd: string): { code: number; size: number; stderr: string } {
|
|
106
|
+
const res = run(['git', 'cat-file', '-s', oid], cwd);
|
|
107
|
+
return { code: res.code, size: Number(res.stdout.trim()) || 0, stderr: res.stderr };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function objectId(ref: string, path: string, cwd: string): { code: number; oid: string; stderr: string } {
|
|
111
|
+
const res = run(['git', 'rev-parse', '--verify', `${ref}:${path}`], cwd);
|
|
112
|
+
const oid = res.stdout.trim();
|
|
113
|
+
if (res.code !== 0 || !oid) return { code: res.code || 1, oid: '', stderr: res.stderr };
|
|
114
|
+
const type = run(['git', 'cat-file', '-t', oid], cwd);
|
|
115
|
+
if (type.code !== 0 || type.stdout.trim() !== 'blob') return { code: 1, oid: '', stderr: type.stderr };
|
|
116
|
+
return { code: 0, oid, stderr: '' };
|
|
117
|
+
}
|
|
118
|
+
|
|
67
119
|
export function verifyTreeRef(ref: string, cwd: string): boolean {
|
|
68
120
|
if (!ref || ref === 'worktree') return false;
|
|
69
121
|
if (ref.startsWith('-')) return false;
|
|
@@ -159,55 +211,63 @@ function sortTreeEntries(entries: GitTreeEntry[]): GitTreeEntry[] {
|
|
|
159
211
|
});
|
|
160
212
|
}
|
|
161
213
|
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (base) args.push('--', `${base}/`);
|
|
166
|
-
const proc = Bun.spawnSync(args, {
|
|
167
|
-
cwd,
|
|
168
|
-
stdout: 'pipe',
|
|
169
|
-
stderr: 'ignore',
|
|
170
|
-
});
|
|
171
|
-
if (proc.exitCode !== 0) return new Set();
|
|
172
|
-
return new Set(new TextDecoder().decode(proc.stdout)
|
|
173
|
-
.split('\0')
|
|
174
|
-
.filter(entry => entry.endsWith('/'))
|
|
175
|
-
.map(entry => entry.replace(/\/+$/g, ''))
|
|
176
|
-
.filter(entry => entry && entry !== base));
|
|
214
|
+
function omittedWorktreeDirectoryReason(name: string, omitDirNames: Set<string>): GitTreeEntry['children_omitted_reason'] | undefined {
|
|
215
|
+
if (name === '.git') return 'internal';
|
|
216
|
+
return omitDirNames.has(name) ? 'heavy' : undefined;
|
|
177
217
|
}
|
|
178
218
|
|
|
179
|
-
function worktreeEntryFromDirent(base: string, dir: string, name: string, isDirectory: boolean,
|
|
219
|
+
function worktreeEntryFromDirent(base: string, dir: string, name: string, isDirectory: boolean, omitDirNames: Set<string>): GitTreeEntry {
|
|
180
220
|
const entryPath = base ? `${base}/${name}` : name;
|
|
181
221
|
const type = isDirectory
|
|
182
222
|
? hasDotGitEntry(join(dir, name)) ? 'commit' as const : 'tree' as const
|
|
183
223
|
: 'blob' as const;
|
|
184
|
-
|
|
224
|
+
const omittedReason = type === 'tree' ? omittedWorktreeDirectoryReason(name, omitDirNames) : undefined;
|
|
225
|
+
return omittedReason
|
|
185
226
|
? {
|
|
186
227
|
name,
|
|
187
228
|
path: entryPath,
|
|
188
229
|
type,
|
|
189
230
|
children_omitted: true,
|
|
190
|
-
children_omitted_reason:
|
|
231
|
+
children_omitted_reason: omittedReason,
|
|
191
232
|
}
|
|
192
233
|
: { name, path: entryPath, type };
|
|
193
234
|
}
|
|
194
235
|
|
|
195
|
-
function worktreeFilesystemEntries(cwd: string, path: string, recursive: boolean): GitTreeEntry[] {
|
|
236
|
+
function worktreeFilesystemEntries(cwd: string, path: string, recursive: boolean, omitDirNames: string[] = DEFAULT_WORKTREE_OMIT_DIR_NAMES): GitTreeEntry[] {
|
|
196
237
|
const base = normalizeTreePath(path);
|
|
197
238
|
const root = join(cwd, base);
|
|
198
|
-
const
|
|
239
|
+
const omitDirNameSet = new Set(omitDirNames);
|
|
199
240
|
let directEntries: GitTreeEntry[];
|
|
200
241
|
try {
|
|
201
242
|
const dirents = readdirSync(root, { withFileTypes: true });
|
|
202
243
|
directEntries = sortTreeEntries(dirents
|
|
203
|
-
.map(entry => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(),
|
|
244
|
+
.map(entry => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet)));
|
|
204
245
|
} catch {
|
|
205
246
|
return [];
|
|
206
247
|
}
|
|
207
248
|
if (!recursive) return directEntries;
|
|
208
249
|
|
|
209
250
|
const fileEntries: GitTreeEntry[] = [];
|
|
251
|
+
let truncated = false;
|
|
252
|
+
const pushRecursiveEntry = (entry: GitTreeEntry): boolean => {
|
|
253
|
+
if (fileEntries.length >= WORKTREE_RECURSIVE_ENTRY_LIMIT) {
|
|
254
|
+
if (!truncated) {
|
|
255
|
+
fileEntries.push({
|
|
256
|
+
name: 'more...',
|
|
257
|
+
path: '__code_viewer_truncated__',
|
|
258
|
+
type: 'tree',
|
|
259
|
+
children_omitted: true,
|
|
260
|
+
children_omitted_reason: 'truncated',
|
|
261
|
+
});
|
|
262
|
+
truncated = true;
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
fileEntries.push(entry);
|
|
267
|
+
return true;
|
|
268
|
+
};
|
|
210
269
|
const walk = (dir: string, prefix: string, depth: number) => {
|
|
270
|
+
if (truncated) return;
|
|
211
271
|
if (depth >= WORKTREE_RECURSIVE_DEPTH_LIMIT) return;
|
|
212
272
|
let entries;
|
|
213
273
|
try {
|
|
@@ -219,20 +279,21 @@ function worktreeFilesystemEntries(cwd: string, path: string, recursive: boolean
|
|
|
219
279
|
const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
220
280
|
const full = join(dir, entry.name);
|
|
221
281
|
if (entry.isDirectory()) {
|
|
222
|
-
|
|
223
|
-
|
|
282
|
+
const omittedReason = omittedWorktreeDirectoryReason(entry.name, omitDirNameSet);
|
|
283
|
+
if (omittedReason) {
|
|
284
|
+
if (!pushRecursiveEntry({
|
|
224
285
|
name: entry.name,
|
|
225
286
|
path: entryPath,
|
|
226
287
|
type: 'tree',
|
|
227
288
|
children_omitted: true,
|
|
228
|
-
children_omitted_reason:
|
|
229
|
-
});
|
|
289
|
+
children_omitted_reason: omittedReason,
|
|
290
|
+
})) return;
|
|
230
291
|
continue;
|
|
231
292
|
}
|
|
232
293
|
if (hasDotGitEntry(full)) continue;
|
|
233
294
|
walk(full, entryPath, depth + 1);
|
|
234
295
|
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
235
|
-
|
|
296
|
+
if (!pushRecursiveEntry({ name: entry.name, path: entryPath, type: 'blob' })) return;
|
|
236
297
|
}
|
|
237
298
|
}
|
|
238
299
|
};
|
|
@@ -297,11 +358,11 @@ export function listTree(
|
|
|
297
358
|
ref: string,
|
|
298
359
|
path: string,
|
|
299
360
|
cwd: string,
|
|
300
|
-
options: { recursive?: boolean } = {},
|
|
361
|
+
options: { recursive?: boolean; omitDirNames?: string[] } = {},
|
|
301
362
|
): { code: number; entries: GitTreeEntry[]; stderr: string } {
|
|
302
363
|
const base = normalizeTreePath(path);
|
|
303
364
|
if (ref === 'worktree') {
|
|
304
|
-
return { code: 0, entries: worktreeFilesystemEntries(cwd, base, !!options.recursive), stderr: '' };
|
|
365
|
+
return { code: 0, entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames), stderr: '' };
|
|
305
366
|
}
|
|
306
367
|
|
|
307
368
|
const direct = gitTreeEntries(ref, base, cwd, false);
|