ts-axi 0.1.2 → 0.1.4

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.
@@ -155,6 +155,14 @@ function bodyOf(node) {
155
155
  function openBrace(node, sf) {
156
156
  return node.getChildren(sf).find((c) => c.kind === ts.SyntaxKind.OpenBraceToken)?.getStart(sf);
157
157
  }
158
+ /** Text up to the body of a function initializer, or up to the end of the type annotation (the initializer is the body). */
159
+ function headText(node, sf, init, typeEnd) {
160
+ if (isFunctionInitializer(init)) {
161
+ const text = tokenText(node, sf, node.getStart(sf), init.body.getStart(sf));
162
+ return ts.isArrowFunction(init) ? text.replace(/\s*=>$/, " =>") : text;
163
+ }
164
+ return tokenText(node, sf, node.getStart(sf), typeEnd);
165
+ }
158
166
  /** Uncapped Signature text; `signatureOf` clips it for display. */
159
167
  export function fullSignatureOf(node, sf) {
160
168
  if (ts.isVariableDeclaration(node)) {
@@ -163,13 +171,13 @@ export function fullSignatureOf(node, sf) {
163
171
  const head = ts.isVariableDeclarationList(list) && ts.isVariableStatement(statement)
164
172
  ? tokenText(statement, sf, statement.getStart(sf), list.declarations[0].getStart(sf))
165
173
  : "";
166
- const fn = isFunctionInitializer(node.initializer) ? node.initializer : undefined;
167
- const end = fn ? fn.body.getStart(sf) : node.end;
168
- let text = tokenText(node, sf, node.getStart(sf), end);
169
- if (fn && ts.isArrowFunction(fn))
170
- text = text.replace(/\s*=>$/, " =>");
171
- return `${head} ${text}`.trim();
174
+ return `${head} ${headText(node, sf, node.initializer, (node.type ?? node.exclamationToken ?? node.name).end)}`.trim();
175
+ }
176
+ if (ts.isPropertyDeclaration(node)) {
177
+ return headText(node, sf, node.initializer, (node.type ?? node.exclamationToken ?? node.questionToken ?? node.name).end).replace(/;$/, "");
172
178
  }
179
+ if (ts.isEnumMember(node))
180
+ return tokenText(node, sf, node.getStart(sf), node.name.end);
173
181
  if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
174
182
  return tokenText(node, sf, node.getStart(sf), openBrace(node, sf) ?? node.end);
175
183
  }
@@ -216,12 +224,76 @@ function nameText(name, sf) {
216
224
  return name.text;
217
225
  return name.getText(sf);
218
226
  }
227
+ /** Registration verbs whose `('Name', fn)` call introduces a nested Declaration (AngularJS style). */
228
+ const REGISTRATION_VERBS = new Set(["controller", "service", "factory", "directive", "filter", "component", "run", "config", "provider"]);
229
+ /** Verbs whose callback is a constructor: its `this.x = function` statements are members. */
230
+ const CONSTRUCTOR_VERBS = new Set(["controller", "service", "provider"]);
231
+ /** `Template.x.<hook>(fn)` lifecycle callbacks. */
232
+ const TEMPLATE_HOOKS = new Set(["onCreated", "onRendered", "onDestroyed"]);
233
+ function unwrap(expr) {
234
+ let e = expr;
235
+ for (;;) {
236
+ if (ts.isParenthesizedExpression(e))
237
+ e = e.expression;
238
+ else if (ts.isPrefixUnaryExpression(e))
239
+ e = e.operand;
240
+ else if (ts.isVoidExpression(e))
241
+ e = e.expression;
242
+ else
243
+ return e;
244
+ }
245
+ }
246
+ function asFunction(expr) {
247
+ if (!expr)
248
+ return undefined;
249
+ const e = unwrap(expr);
250
+ return ts.isFunctionExpression(e) || ts.isArrowFunction(e) ? e : undefined;
251
+ }
252
+ /** Statements of an IIFE body: `(function () {...})()`, `!function () {...}()`, `(function () {...}).call(this)`. */
253
+ function iifeBody(expr) {
254
+ if (!expr)
255
+ return undefined;
256
+ const call = unwrap(expr);
257
+ if (!ts.isCallExpression(call))
258
+ return undefined;
259
+ let callee = unwrap(call.expression);
260
+ if (ts.isPropertyAccessExpression(callee) && (callee.name.text === "call" || callee.name.text === "apply"))
261
+ callee = unwrap(callee.expression);
262
+ const fn = asFunction(callee);
263
+ return fn && ts.isBlock(fn.body) ? fn.body.statements : undefined;
264
+ }
265
+ /** The callback of a registration: a function, or the last element of a DI array. */
266
+ function registeredFunction(arg) {
267
+ if (!arg)
268
+ return undefined;
269
+ if (ts.isArrayLiteralExpression(arg))
270
+ return asFunction(arg.elements[arg.elements.length - 1]);
271
+ return asFunction(arg);
272
+ }
273
+ function isStringName(node) {
274
+ return node !== undefined && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node));
275
+ }
276
+ function compact(node, sf) {
277
+ return node.getText(sf).replace(/\s+/g, "");
278
+ }
279
+ /** Parameter list (and return type) of a function as `(a, b): T`. */
280
+ function paramsText(fn, sf) {
281
+ const params = fn.parameters.map((p) => tokenText(p, sf, p.getStart(sf), p.end)).join(", ");
282
+ return `(${params})${fn.type ? `: ${tokenText(fn.type, sf, fn.type.getStart(sf), fn.type.end)}` : ""}`;
283
+ }
284
+ /** `node` text up to the body of its function value; `=>` kept for arrows. */
285
+ function headUntil(node, fn, sf) {
286
+ const text = tokenText(node, sf, node.getStart(sf), fn.body.getStart(sf));
287
+ return ts.isArrowFunction(fn) ? text.replace(/\s*=>$/, " =>") : text;
288
+ }
219
289
  /**
220
- * Top-level Declarations of a file in source order, with class, interface and
221
- * enum members and namespace members following their parent.
290
+ * Declarations of a file in source order: top-level ones with class, interface
291
+ * and enum members and namespace members following their parent, plus nested
292
+ * declarations of script-style code (registrations, IIFE helpers, prototype and
293
+ * object-literal methods; see GLOSSARY.md Declaration).
222
294
  */
223
295
  export function collectDeclarations(sf, file) {
224
- const result = [];
296
+ const raws = [];
225
297
  const local = localExportNames(sf);
226
298
  const add = (node, nameNode, name, exported, parent) => {
227
299
  const kind = kindOf(node);
@@ -231,26 +303,228 @@ export function collectDeclarations(sf, file) {
231
303
  ? node.parent.parent
232
304
  : node;
233
305
  const start = startNode.getStart(sf);
234
- const startLc = sf.getLineAndCharacterOfPosition(start);
235
- const namePos = nameNode ? nameNode.getStart(sf) : start;
236
306
  const body = bodyOf(ts.isVariableDeclaration(node) && isFunctionInitializer(node.initializer) ? node.initializer : node);
237
- result.push({
238
- file,
239
- line: startLc.line + 1,
240
- col: startLc.character + 1,
241
- endLine: sf.getLineAndCharacterOfPosition(node.end).line + 1,
307
+ raws.push({
308
+ start,
309
+ end: node.end,
242
310
  kind,
243
311
  name,
244
312
  qualifiedName: parent ? `${parent}.${name}` : name,
245
313
  exported,
246
- signature: signatureOf(node, sf),
314
+ signature: fullSignatureOf(node, sf),
247
315
  ...(parent ? { parentName: parent } : {}),
248
- namePos,
249
- nameLine: sf.getLineAndCharacterOfPosition(namePos).line + 1,
316
+ namePos: nameNode ? nameNode.getStart(sf) : start,
250
317
  hasBody: body !== undefined ||
251
318
  !(ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node)),
252
319
  });
253
320
  };
321
+ const nested = (raw) => {
322
+ raws.push({ ...raw, qualifiedName: raw.qualifiedName ?? (raw.parentName ? `${raw.parentName}.${raw.name}` : raw.name), hasBody: true });
323
+ };
324
+ /** Method and function-valued properties of an object literal as `<holder>.<name>`. */
325
+ const objectMembers = (obj, holder, exported) => {
326
+ for (const prop of obj.properties) {
327
+ const name = nameText(prop.name, sf);
328
+ if (!name || !prop.name)
329
+ continue;
330
+ if (ts.isMethodDeclaration(prop)) {
331
+ nested({ start: prop.getStart(sf), end: prop.end, kind: "method", name, parentName: holder, exported, signature: fullSignatureOf(prop, sf), namePos: prop.name.getStart(sf) });
332
+ }
333
+ else if (ts.isPropertyAssignment(prop)) {
334
+ const fn = asFunction(prop.initializer);
335
+ if (fn)
336
+ nested({ start: prop.getStart(sf), end: prop.end, kind: "method", name, parentName: holder, exported, signature: headUntil(prop, fn, sf), namePos: prop.name.getStart(sf) });
337
+ }
338
+ }
339
+ };
340
+ /** `this.x = function` statements at the top level of a constructor-like body. */
341
+ const thisMembers = (body, owner, exported) => {
342
+ if (!body || !ts.isBlock(body))
343
+ return;
344
+ for (const statement of body.statements) {
345
+ if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression))
346
+ continue;
347
+ const { left, operatorToken, right } = statement.expression;
348
+ const fn = asFunction(right);
349
+ if (operatorToken.kind !== ts.SyntaxKind.EqualsToken || !fn || !ts.isPropertyAccessExpression(left) || left.expression.kind !== ts.SyntaxKind.ThisKeyword)
350
+ continue;
351
+ nested({ start: statement.getStart(sf), end: statement.end, kind: "method", name: left.name.text, parentName: owner, exported, signature: headUntil(statement.expression, fn, sf), namePos: left.name.getStart(sf) });
352
+ }
353
+ };
354
+ /** Top-level object literals returned by a factory: its API. */
355
+ const returnedMembers = (body, owner) => {
356
+ if (ts.isObjectLiteralExpression(unwrap(body)))
357
+ return objectMembers(unwrap(body), owner, true);
358
+ if (!ts.isBlock(body))
359
+ return;
360
+ for (const statement of body.statements) {
361
+ if (ts.isReturnStatement(statement) && statement.expression && ts.isObjectLiteralExpression(unwrap(statement.expression))) {
362
+ objectMembers(unwrap(statement.expression), owner, true);
363
+ }
364
+ }
365
+ };
366
+ const topLevelExported = (name) => raws.find((r) => r.qualifiedName === name && r.parentName === undefined)?.exported ?? false;
367
+ const isConstructorName = (name) => /^[A-Z]/.test(name);
368
+ /** `.controller('Name', fn)` and friends; `.run(fn)` / `.config(fn)` take the verb as name. */
369
+ const registration = (call, verb) => {
370
+ const [first, second] = call.arguments;
371
+ const named = isStringName(first);
372
+ if (!named && verb !== "run" && verb !== "config")
373
+ return;
374
+ const name = named ? first.text : verb;
375
+ const arg = named ? second : first;
376
+ const callee = call.expression;
377
+ const start = named ? first.getStart(sf) : callee.name.getStart(sf);
378
+ if (verb === "component" && arg && ts.isObjectLiteralExpression(arg)) {
379
+ nested({ start, end: arg.end, kind: "variable", name, exported: true, signature: `component ${name}`, namePos: arg.getStart(sf), nameless: true });
380
+ objectMembers(arg, name, true);
381
+ return;
382
+ }
383
+ const fn = registeredFunction(arg);
384
+ if (!fn)
385
+ return;
386
+ const inject = arg && ts.isArrayLiteralExpression(arg) ? ` [${arg.elements.slice(0, -1).map((e) => e.getText(sf)).join(", ")}]` : "";
387
+ nested({
388
+ start,
389
+ end: arg.end,
390
+ kind: "function",
391
+ name,
392
+ qualifiedName: name,
393
+ parentName: verb,
394
+ exported: true,
395
+ signature: `${verb} ${name}${paramsText(fn, sf)}`,
396
+ namePos: (ts.isFunctionExpression(fn) && fn.name ? fn.name : fn).getStart(sf),
397
+ nameless: true,
398
+ });
399
+ // The DI array is compared, not displayed: reordering injections is a contract change.
400
+ if (inject)
401
+ raws[raws.length - 1].signature += `\n${inject.trim()}`;
402
+ if (CONSTRUCTOR_VERBS.has(verb))
403
+ thisMembers(fn.body, name, true);
404
+ if (verb === "factory" || verb === "service")
405
+ returnedMembers(fn.body, name);
406
+ };
407
+ /** Registrations along a call chain (`angular.module('x').controller(...).service(...)`), in source order. */
408
+ const callChain = (expr) => {
409
+ const calls = [];
410
+ let e = unwrap(expr);
411
+ while (ts.isCallExpression(e)) {
412
+ const callee = e.expression;
413
+ if (ts.isPropertyAccessExpression(callee)) {
414
+ calls.push({ call: e, name: callee.name.text });
415
+ e = callee.expression;
416
+ }
417
+ else {
418
+ break;
419
+ }
420
+ }
421
+ for (const { call, name } of calls.reverse()) {
422
+ const callee = call.expression;
423
+ const receiver = compact(callee.expression, sf);
424
+ const first = call.arguments[0];
425
+ if (receiver === "Meteor" && name === "methods" && first && ts.isObjectLiteralExpression(first)) {
426
+ objectMembers(first, "methods", true);
427
+ }
428
+ else if (receiver.startsWith("Template") && (name === "helpers" || name === "events") && first && ts.isObjectLiteralExpression(first)) {
429
+ objectMembers(first, `${receiver}.${name}`, true);
430
+ }
431
+ else if (receiver.startsWith("Template") && TEMPLATE_HOOKS.has(name) && asFunction(first)) {
432
+ nested({ start: call.getStart(sf), end: call.end, kind: "method", name, parentName: receiver, exported: true, signature: `${receiver}.${headUntil(first, asFunction(first), sf)}`, namePos: callee.name.getStart(sf) });
433
+ }
434
+ else if (REGISTRATION_VERBS.has(name)) {
435
+ registration(call, name);
436
+ }
437
+ }
438
+ };
439
+ /** `exports.x = fn`, `module.exports = {...}`, `Foo.prototype.bar = fn`, `Foo.bar = fn`. */
440
+ const assignment = (statement, expr) => {
441
+ const left = expr.left;
442
+ const right = unwrap(expr.right);
443
+ if (!ts.isPropertyAccessExpression(left))
444
+ return;
445
+ const target = compact(left, sf);
446
+ if (target === "module.exports") {
447
+ if (ts.isObjectLiteralExpression(right))
448
+ objectMembers(right, "exports", true);
449
+ return;
450
+ }
451
+ const owner = left.expression;
452
+ if (ts.isPropertyAccessExpression(owner) && owner.name.text === "prototype" && ts.isIdentifier(owner.expression)) {
453
+ const cls = owner.expression.text;
454
+ const fn = asFunction(right);
455
+ if (fn)
456
+ nested({ start: statement.getStart(sf), end: statement.end, kind: "method", name: left.name.text, parentName: cls, exported: topLevelExported(cls), signature: headUntil(expr, fn, sf), namePos: left.name.getStart(sf) });
457
+ return;
458
+ }
459
+ if (ts.isIdentifier(left.name) && left.name.text === "prototype" && ts.isIdentifier(owner) && ts.isObjectLiteralExpression(right)) {
460
+ objectMembers(right, owner.text, topLevelExported(owner.text));
461
+ return;
462
+ }
463
+ const fn = asFunction(right);
464
+ if (!fn)
465
+ return;
466
+ const holder = target === `module.exports.${left.name.text}` ? "exports" : ts.isIdentifier(owner) ? owner.text : undefined;
467
+ if (holder === undefined)
468
+ return;
469
+ const exported = holder === "exports" || topLevelExported(holder);
470
+ nested({ start: statement.getStart(sf), end: statement.end, kind: "method", name: left.name.text, parentName: holder, exported, signature: headUntil(expr, fn, sf), namePos: left.name.getStart(sf) });
471
+ };
472
+ /** Script-style patterns of one module-level statement; `inIife` adds the IIFE's own functions. */
473
+ const scriptStatement = (statement, inIife) => {
474
+ if (ts.isFunctionDeclaration(statement)) {
475
+ if (!statement.name || !statement.body)
476
+ return;
477
+ if (inIife)
478
+ add(statement, statement.name, statement.name.text, false);
479
+ if (isConstructorName(statement.name.text))
480
+ thisMembers(statement.body, statement.name.text, inIife ? false : topLevelExported(statement.name.text));
481
+ }
482
+ else if (ts.isVariableStatement(statement)) {
483
+ for (const decl of statement.declarationList.declarations) {
484
+ if (!ts.isIdentifier(decl.name) || !decl.initializer)
485
+ continue;
486
+ const name = decl.name.text;
487
+ const fn = asFunction(decl.initializer);
488
+ if (inIife && fn)
489
+ add(decl, decl.name, name, false);
490
+ const exported = inIife ? false : topLevelExported(name);
491
+ const init = unwrap(decl.initializer);
492
+ if (ts.isObjectLiteralExpression(init))
493
+ objectMembers(init, name, exported);
494
+ else if (fn && isConstructorName(name))
495
+ thisMembers(fn.body, name, exported);
496
+ else {
497
+ const body = iifeBody(decl.initializer);
498
+ if (body)
499
+ scriptStatements(body, true);
500
+ else
501
+ callChain(decl.initializer);
502
+ }
503
+ }
504
+ }
505
+ else if (ts.isExpressionStatement(statement)) {
506
+ const expr = unwrap(statement.expression);
507
+ const body = iifeBody(expr);
508
+ if (body)
509
+ scriptStatements(body, true);
510
+ else if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.EqualsToken)
511
+ assignment(statement, expr);
512
+ else
513
+ callChain(expr);
514
+ }
515
+ else if (ts.isIfStatement(statement)) {
516
+ scriptStatement(statement.thenStatement, inIife);
517
+ if (statement.elseStatement)
518
+ scriptStatement(statement.elseStatement, inIife);
519
+ }
520
+ else if (ts.isBlock(statement)) {
521
+ scriptStatements(statement.statements, inIife);
522
+ }
523
+ };
524
+ const scriptStatements = (statements, inIife) => {
525
+ for (const statement of statements)
526
+ scriptStatement(statement, inIife);
527
+ };
254
528
  const visitStatements = (statements, parent, parentExported) => {
255
529
  for (const statement of statements) {
256
530
  if (ts.isVariableStatement(statement)) {
@@ -260,6 +534,8 @@ export function collectDeclarations(sf, file) {
260
534
  const exported = parent ? parentExported && hasModifier(statement, ts.SyntaxKind.ExportKeyword) : isTopLevelExported(statement, decl.name.text, local);
261
535
  add(decl, decl.name, decl.name.text, exported, parent);
262
536
  }
537
+ if (!parent)
538
+ scriptStatement(statement, false);
263
539
  continue;
264
540
  }
265
541
  if (ts.isFunctionDeclaration(statement) ||
@@ -307,11 +583,39 @@ export function collectDeclarations(sf, file) {
307
583
  else if (ts.isModuleDeclaration(statement) && statement.body && ts.isModuleBlock(statement.body)) {
308
584
  visitStatements(statement.body.statements, qualified, exported);
309
585
  }
586
+ else if (ts.isFunctionDeclaration(statement) && !parent) {
587
+ scriptStatement(statement, false);
588
+ }
589
+ continue;
310
590
  }
591
+ if (!parent)
592
+ scriptStatement(statement, false);
311
593
  }
312
594
  };
313
595
  visitStatements(sf.statements, undefined, true);
314
- return result;
596
+ // Nested declarations are found after their holder statement; source order is start order.
597
+ const ordered = raws.map((r, i) => ({ r, i })).sort((a, b) => a.r.start - b.r.start || a.i - b.i);
598
+ return ordered.map(({ r }) => {
599
+ const startLc = sf.getLineAndCharacterOfPosition(r.start);
600
+ const full = r.signature;
601
+ return {
602
+ file,
603
+ line: startLc.line + 1,
604
+ col: startLc.character + 1,
605
+ endLine: sf.getLineAndCharacterOfPosition(r.end).line + 1,
606
+ kind: r.kind,
607
+ name: r.name,
608
+ qualifiedName: r.qualifiedName,
609
+ exported: r.exported,
610
+ signature: clip(full.split("\n")[0]),
611
+ ...(r.parentName !== undefined ? { parentName: r.parentName } : {}),
612
+ namePos: r.namePos,
613
+ nameLine: sf.getLineAndCharacterOfPosition(r.namePos).line + 1,
614
+ hasBody: r.hasBody,
615
+ fullSignature: full,
616
+ ...(r.nameless ? { nameless: true } : {}),
617
+ };
618
+ });
315
619
  }
316
620
  /** Declarations sharing file and qualified name (overloads, merging) are one Symbol. */
317
621
  export function symbolKey(decl) {
@@ -0,0 +1,96 @@
1
+ // The one file listing for every walk (implicit project, home count, workspace detection,
2
+ // importer index, outside-project search): `git ls-files` inside a repo, else an fs walk with
3
+ // one skip rule. Node builtins only: the home view imports it.
4
+ import { execFileSync } from "node:child_process";
5
+ import { existsSync, readdirSync } from "node:fs";
6
+ import { join, relative, sep } from "node:path";
7
+ /** Build output, dependencies, caches and tool/editor state (agent worktrees live in `.claude/`). */
8
+ export const SKIP_DIRS = new Set([
9
+ "node_modules", "dist", "build", "coverage", "tmp", ".git", ".nx", ".angular", ".claude", ".cursor", ".idea", ".vscode",
10
+ ]);
11
+ /** A directory below the root that holds its own `.git` (nested repo or worktree) is another checkout. */
12
+ export function isNestedCheckout(dir, root) {
13
+ return dir !== root && existsSync(join(dir, ".git"));
14
+ }
15
+ export function skipDir(name, path, root) {
16
+ return SKIP_DIRS.has(name) || isNestedCheckout(path, root);
17
+ }
18
+ /** Files below `root` (absolute paths) under the shared skip rule; `capped` when a bound stopped it. */
19
+ export function walkFiles(root, options) {
20
+ const files = [];
21
+ const queue = [root];
22
+ let capped = false;
23
+ while (queue.length > 0) {
24
+ if ((options.maxFiles !== undefined && files.length >= options.maxFiles) || (options.deadline !== undefined && Date.now() > options.deadline)) {
25
+ capped = true;
26
+ break;
27
+ }
28
+ const dir = options.breadthFirst ? queue.shift() : queue.pop();
29
+ let entries;
30
+ try {
31
+ entries = readdirSync(dir, { withFileTypes: true });
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ if (options.breadthFirst)
37
+ entries.sort((a, b) => a.name.localeCompare(b.name));
38
+ for (const entry of entries) {
39
+ const path = join(dir, entry.name);
40
+ if (entry.isDirectory()) {
41
+ if (!skipDir(entry.name, path, root))
42
+ queue.push(path);
43
+ }
44
+ else if (entry.isFile() && options.accept(entry.name)) {
45
+ files.push(path);
46
+ }
47
+ }
48
+ }
49
+ return { files, capped };
50
+ }
51
+ /** Shortest path first, then alphabetical: the canonical copy before nested duplicates. */
52
+ export function byDepthThenPath(a, b) {
53
+ return a.split("/").length - b.split("/").length || a.localeCompare(b);
54
+ }
55
+ /**
56
+ * Tracked + untracked-not-ignored files, repo-relative (one local, read-only `git ls-files` call):
57
+ * git never descends into nested repos or worktrees, and gitignored dirs drop out. Undefined
58
+ * when `root` is not a git checkout (`.git` dir, or file for a linked worktree) or git is missing.
59
+ */
60
+ export function gitFiles(root) {
61
+ if (!existsSync(join(root, ".git")))
62
+ return undefined;
63
+ try {
64
+ const raw = execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
65
+ cwd: root,
66
+ encoding: "utf8",
67
+ maxBuffer: 512 * 1024 * 1024,
68
+ stdio: ["ignore", "pipe", "ignore"],
69
+ });
70
+ // A nested repo shows up as one `dir/` entry; deleted-but-tracked files are dropped by the caller's accept/exists check.
71
+ return [...new Set(raw.split("\0").filter((f) => f !== "" && !f.endsWith("/")))];
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ }
77
+ /**
78
+ * Files below `root` whose basename passes `accept`: the git listing (skip-dir segments removed,
79
+ * missing files dropped) or, outside git, the bounded fs walk.
80
+ */
81
+ export function listFiles(root, accept, bounds = {}) {
82
+ const git = gitFiles(root);
83
+ if (git) {
84
+ const files = git
85
+ .filter((f) => accept(f.slice(f.lastIndexOf("/") + 1)) && !f.split("/").some((seg) => SKIP_DIRS.has(seg)))
86
+ .map((f) => join(root, f))
87
+ .filter((f) => existsSync(f))
88
+ .sort();
89
+ return { files, capped: false, listedBy: "git" };
90
+ }
91
+ const walked = walkFiles(root, { ...bounds, accept });
92
+ return { files: walked.files.sort(), capped: walked.capped, listedBy: "walk" };
93
+ }
94
+ export function toRel(root, abs) {
95
+ return relative(root, abs).split(sep).join("/");
96
+ }