jsdoczoom 1.0.0 → 1.2.1

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/dist/barrel.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync } from "node:fs";
1
+ import { access, readdir } from "node:fs/promises";
2
2
  import { basename, dirname, join, relative, resolve } from "node:path";
3
3
  /**
4
4
  * Barrel detection and child discovery for index.ts/index.tsx files.
@@ -34,29 +34,34 @@ export function isBarrel(filePath) {
34
34
  * @param _cwd - Working directory (unused, kept for API consistency)
35
35
  * @returns Sorted array of absolute paths to child files
36
36
  */
37
- export function getBarrelChildren(barrelPath, _cwd) {
37
+ export async function getBarrelChildren(barrelPath, _cwd) {
38
38
  const dir = dirname(barrelPath);
39
39
  const barrelName = basename(barrelPath);
40
- let entries;
40
+ let rawEntries;
41
41
  try {
42
- entries = readdirSync(dir, { withFileTypes: true })
43
- .map((e) => ({ name: e.name, isDirectory: e.isDirectory() }))
44
- .flatMap((entry) => {
42
+ const dirents = await readdir(dir, { withFileTypes: true });
43
+ rawEntries = dirents.map((e) => ({
44
+ name: e.name,
45
+ isDirectory: e.isDirectory(),
46
+ }));
47
+ } catch {
48
+ // Directory unreadable (permissions, deleted, etc.) — no children discoverable
49
+ return [];
50
+ }
51
+ const entries = (
52
+ await Promise.all(
53
+ rawEntries.map(async (entry) => {
45
54
  if (entry.isDirectory) {
46
- // Check for child barrel in this subdirectory
47
- const childBarrel = findChildBarrel(resolve(dir, entry.name));
55
+ const childBarrel = await findChildBarrel(resolve(dir, entry.name));
48
56
  return childBarrel ? [childBarrel] : [];
49
57
  }
50
- // Sibling file: must be .ts/.tsx, not .d.ts, not the barrel itself
51
58
  if (isTsFile(entry.name) && entry.name !== barrelName) {
52
59
  return [resolve(dir, entry.name)];
53
60
  }
54
61
  return [];
55
- });
56
- } catch {
57
- // Directory unreadable (permissions, deleted, etc.) — no children discoverable
58
- return [];
59
- }
62
+ }),
63
+ )
64
+ ).flat();
60
65
  return entries.sort();
61
66
  }
62
67
  /**
@@ -72,16 +77,21 @@ function isTsFile(name) {
72
77
  * index.ts takes priority over index.tsx.
73
78
  * Returns the absolute path to the barrel, or null if none found.
74
79
  */
75
- function findChildBarrel(subdirPath) {
80
+ async function findChildBarrel(subdirPath) {
76
81
  const tsPath = resolve(subdirPath, "index.ts");
77
- if (existsSync(tsPath)) {
82
+ try {
83
+ await access(tsPath);
78
84
  return tsPath;
85
+ } catch {
86
+ // no index.ts
79
87
  }
80
88
  const tsxPath = resolve(subdirPath, "index.tsx");
81
- if (existsSync(tsxPath)) {
89
+ try {
90
+ await access(tsxPath);
82
91
  return tsxPath;
92
+ } catch {
93
+ return null;
83
94
  }
84
- return null;
85
95
  }
86
96
  /** Minimum number of .ts/.tsx files in a directory to require a barrel. */
87
97
  export const BARREL_THRESHOLD = 3;
@@ -89,7 +99,7 @@ export const BARREL_THRESHOLD = 3;
89
99
  * Find directories with more than BARREL_THRESHOLD .ts/.tsx files
90
100
  * that lack a barrel file (index.ts or index.tsx).
91
101
  */
92
- export function findMissingBarrels(filePaths, cwd) {
102
+ export async function findMissingBarrels(filePaths, cwd) {
93
103
  const dirCounts = new Map();
94
104
  for (const filePath of filePaths) {
95
105
  if (isBarrel(filePath)) continue;
@@ -97,14 +107,23 @@ export function findMissingBarrels(filePaths, cwd) {
97
107
  dirCounts.set(dir, (dirCounts.get(dir) ?? 0) + 1);
98
108
  }
99
109
  const missing = [];
100
- for (const [dir, count] of dirCounts) {
101
- if (count <= BARREL_THRESHOLD) continue;
102
- const hasBarrel =
103
- existsSync(join(dir, "index.ts")) || existsSync(join(dir, "index.tsx"));
104
- if (!hasBarrel) {
105
- const rel = relative(cwd, dir) || ".";
106
- missing.push(rel);
107
- }
108
- }
110
+ await Promise.all(
111
+ [...dirCounts.entries()].map(async ([dir, count]) => {
112
+ if (count <= BARREL_THRESHOLD) return;
113
+ const [tsExists, tsxExists] = await Promise.all([
114
+ access(join(dir, "index.ts")).then(
115
+ () => true,
116
+ () => false,
117
+ ),
118
+ access(join(dir, "index.tsx")).then(
119
+ () => true,
120
+ () => false,
121
+ ),
122
+ ]);
123
+ if (!tsExists && !tsxExists) {
124
+ missing.push(relative(cwd, dir) || ".");
125
+ }
126
+ }),
127
+ );
109
128
  return missing.sort();
110
129
  }
package/dist/cli.js CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { drilldown, drilldownFiles } from "./drilldown.js";
6
6
  import { JsdocError } from "./errors.js";
7
+ import { discoverFiles } from "./file-discovery.js";
7
8
  import { lint, lintFiles } from "./lint.js";
9
+ import { search, searchFiles } from "./search.js";
8
10
  import { parseSelector } from "./selector.js";
9
11
  import { RULE_EXPLANATIONS, SKILL_TEXT } from "./skill-text.js";
10
12
  import { formatTextOutput } from "./text-format.js";
@@ -28,48 +30,54 @@ Each file has four detail levels (1-indexed): @1 summary, @2 description,
28
30
  Options:
29
31
  -h, --help Show this help text
30
32
  -v, --version Show version number
31
- -c, --check Run validation mode
32
- -l, --lint Run lint mode (comprehensive JSDoc quality)
33
+ -c, --check Validate file-level structure (has JSDoc block, @summary, description)
34
+ -l, --lint Lint comprehensive JSDoc quality (file-level + function-level tags)
33
35
  -s, --skill Print JSDoc writing guidelines
34
36
  --json Output as JSON (default is plain text)
35
37
  --pretty Format JSON output with 2-space indent (use with --json)
36
38
  --limit N Max results shown (default 500)
37
39
  --no-gitignore Include files ignored by .gitignore
40
+ --search <query> Search files by regex pattern
38
41
  --disable-cache Skip all cache operations
39
42
  --cache-directory Override cache directory (default: system temp)
40
43
  --explain-rule R Explain a lint rule with examples (e.g. jsdoc/informative-docs)
41
44
 
42
45
  Selector:
43
46
  A glob pattern or file path, optionally with @depth suffix (1-4).
47
+
44
48
  Examples:
45
49
  jsdoczoom src/**/*.ts # All .ts files at depth 1 (summary)
46
- jsdoczoom src/index.ts@2 # Single file at depth 2 (description)
50
+ jsdoczoom src/foo.ts@2 # Single file at depth 2 (description)
47
51
  jsdoczoom **/*.ts@3 # All .ts files at depth 3 (type decls)
48
52
 
49
- Stdin:
50
- Pipe file paths one per line:
51
- find . -name "*.ts" | jsdoczoom
52
- find . -name "*.ts" | jsdoczoom @2
53
- find . -name "*.ts" | jsdoczoom -c
53
+ Search (--search):
54
+ Searches all **/*.{ts,tsx} files (or a selector's file set) by regex.
55
+
56
+ Examples:
57
+ jsdoczoom --search "CacheConfig" # find where CacheConfig is used
58
+ jsdoczoom --search "auth-loader" # searches file name
59
+ jsdoczoom src/*.ts --search "TODO|FIXME" # restrict to a file subset
54
60
 
55
61
  Output:
56
62
  Plain text by default. Each item has a "# path@depth" header followed by
57
63
  content. Use the header value as the next selector to drill deeper.
58
- Use --json for machine-parseable JSON output.
64
+
65
+ Use --json for machine-parseable JSON output, use "next_id" to drill deeper.
59
66
 
60
67
  Type declarations (@3) include source line annotations (// LN or // LN-LM)
61
68
  so you can locate the implementation in the source file.
62
69
 
70
+ Stdin:
71
+ Pipe file paths one per line (useful with -c/-l for targeted validation):
72
+ git diff --name-only | jsdoczoom -c # validate changed files
73
+ cat filelist.txt | jsdoczoom -l # lint a specific set of files
74
+
63
75
  Barrel gating (glob mode):
64
76
  A barrel's @summary and description reflect the cumulative functionality
65
77
  of its directory's children, not the barrel file itself. Barrels with a
66
78
  @summary gate sibling files at depths 1-2. At depth 3 the barrel
67
79
  disappears and its children appear at depth 1.
68
80
 
69
- Modes:
70
- -c Validate file-level structure (has JSDoc block, @summary, description)
71
- -l Lint comprehensive JSDoc quality (file-level + function-level tags)
72
-
73
81
  Exit codes:
74
82
  0 Success (all files pass)
75
83
  1 Runtime error (invalid arguments, missing files)
@@ -79,12 +87,7 @@ Workflow:
79
87
  $ jsdoczoom src/**/*.ts # list summaries
80
88
  $ jsdoczoom src/utils@2 # drill into description
81
89
  $ jsdoczoom src/utils@3 # see type declarations
82
-
83
- Pipe examples:
84
- $ jsdoczoom src/utils.ts@3 | grep "functionName" # find symbol + source line
85
- $ jsdoczoom src/utils.ts@3 | grep "// L" # list all declarations with lines
86
- $ jsdoczoom src/**/*.ts | grep "^#" # list all file headers
87
- $ grep -rl "term" src/ --include="*.ts" | jsdoczoom # describe matching files
90
+ $ jsdoczoom src/**/*.ts | grep "^#" # list all file headers
88
91
  `;
89
92
  /**
90
93
  * Parse a flag that requires a value argument.
@@ -111,6 +114,8 @@ function parseArgs(args) {
111
114
  cacheDirectory: undefined,
112
115
  explainRule: undefined,
113
116
  selectorArg: undefined,
117
+ extraArgs: [],
118
+ searchQuery: undefined,
114
119
  };
115
120
  for (let i = 0; i < args.length; i++) {
116
121
  const arg = args[i];
@@ -170,6 +175,15 @@ function parseArgs(args) {
170
175
  i = nextIndex;
171
176
  continue;
172
177
  }
178
+ if (arg === "--search") {
179
+ const { value, nextIndex } = parseValueFlag(args, i);
180
+ if (value === undefined) {
181
+ throw new JsdocError("INVALID_SELECTOR", "--search requires a value");
182
+ }
183
+ parsed.searchQuery = value;
184
+ i = nextIndex;
185
+ continue;
186
+ }
173
187
  // Unknown flag or positional arg
174
188
  if (arg.startsWith("-")) {
175
189
  throw new JsdocError("INVALID_SELECTOR", `Unrecognized option: ${arg}`);
@@ -177,6 +191,8 @@ function parseArgs(args) {
177
191
  // Positional selector arg
178
192
  if (parsed.selectorArg === undefined) {
179
193
  parsed.selectorArg = arg;
194
+ } else {
195
+ parsed.extraArgs.push(arg);
180
196
  }
181
197
  }
182
198
  return parsed;
@@ -200,10 +216,10 @@ function extractDepthFromArg(selectorArg) {
200
216
  return parsed.depth;
201
217
  }
202
218
  /**
203
- * Process stdin mode: file paths piped in.
219
+ * Process an explicit list of resolved file paths.
204
220
  */
205
- async function processStdin(
206
- stdin,
221
+ async function processFileList(
222
+ filePaths,
207
223
  selectorArg,
208
224
  checkMode,
209
225
  lintMode,
@@ -212,19 +228,30 @@ async function processStdin(
212
228
  limit,
213
229
  cwd,
214
230
  cacheConfig,
231
+ searchQuery,
215
232
  ) {
216
- const stdinPaths = parseStdinPaths(stdin, cwd);
217
233
  const depth =
218
234
  selectorArg !== undefined ? extractDepthFromArg(selectorArg) : undefined;
235
+ if (searchQuery !== undefined) {
236
+ const result = await searchFiles(
237
+ filePaths,
238
+ searchQuery,
239
+ cwd,
240
+ limit,
241
+ cacheConfig,
242
+ );
243
+ writeDrilldownResult(result, json, pretty);
244
+ return;
245
+ }
219
246
  if (lintMode) {
220
- const result = await lintFiles(stdinPaths, cwd, limit, cacheConfig);
247
+ const result = await lintFiles(filePaths, cwd, limit, cacheConfig);
221
248
  writeLintResult(result, pretty);
222
249
  } else if (checkMode) {
223
- const result = await validateFiles(stdinPaths, cwd, limit, cacheConfig);
250
+ const result = await validateFiles(filePaths, cwd, limit, cacheConfig);
224
251
  writeValidationResult(result, pretty);
225
252
  } else {
226
253
  const result = await drilldownFiles(
227
- stdinPaths,
254
+ filePaths,
228
255
  depth,
229
256
  cwd,
230
257
  limit,
@@ -233,6 +260,35 @@ async function processStdin(
233
260
  writeDrilldownResult(result, json, pretty);
234
261
  }
235
262
  }
263
+ /**
264
+ * Process stdin mode: file paths piped in.
265
+ */
266
+ async function processStdin(
267
+ stdin,
268
+ selectorArg,
269
+ checkMode,
270
+ lintMode,
271
+ json,
272
+ pretty,
273
+ limit,
274
+ cwd,
275
+ cacheConfig,
276
+ searchQuery,
277
+ ) {
278
+ const stdinPaths = parseStdinPaths(stdin, cwd);
279
+ await processFileList(
280
+ stdinPaths,
281
+ selectorArg,
282
+ checkMode,
283
+ lintMode,
284
+ json,
285
+ pretty,
286
+ limit,
287
+ cwd,
288
+ cacheConfig,
289
+ searchQuery,
290
+ );
291
+ }
236
292
  /**
237
293
  * Process selector mode: glob or path argument.
238
294
  */
@@ -246,10 +302,23 @@ async function processSelector(
246
302
  gitignore,
247
303
  cwd,
248
304
  cacheConfig,
305
+ searchQuery,
249
306
  ) {
250
307
  const selector = selectorArg
251
308
  ? parseSelector(selectorArg)
252
309
  : { type: "glob", pattern: "**/*.{ts,tsx}", depth: undefined };
310
+ if (searchQuery !== undefined) {
311
+ const result = await search(
312
+ { type: selector.type, pattern: selector.pattern, depth: undefined },
313
+ searchQuery,
314
+ cwd,
315
+ gitignore,
316
+ limit,
317
+ cacheConfig,
318
+ );
319
+ writeDrilldownResult(result, json, pretty);
320
+ return;
321
+ }
253
322
  if (lintMode) {
254
323
  const result = await lint(selector, cwd, limit, gitignore, cacheConfig);
255
324
  writeLintResult(result, pretty);
@@ -268,19 +337,27 @@ async function processSelector(
268
337
  }
269
338
  }
270
339
  /**
271
- * Write an error to stderr as JSON and set exit code.
340
+ * Write an error to stderr as JSON or plain text depending on the json flag.
272
341
  */
273
- function writeError(error) {
342
+ function writeError(error, json) {
343
+ process.exitCode = 1;
344
+ if (json) {
345
+ if (error instanceof JsdocError) {
346
+ void process.stderr.write(`${JSON.stringify(error.toJSON())}\n`);
347
+ return;
348
+ }
349
+ const message = error instanceof Error ? error.message : String(error);
350
+ void process.stderr.write(
351
+ `${JSON.stringify({ error: { code: "INTERNAL_ERROR", message } })}\n`,
352
+ );
353
+ return;
354
+ }
274
355
  if (error instanceof JsdocError) {
275
- void process.stderr.write(`${JSON.stringify(error.toJSON())}\n`);
276
- process.exitCode = 1;
356
+ void process.stderr.write(`Error [${error.code}]: ${error.message}\n`);
277
357
  return;
278
358
  }
279
359
  const message = error instanceof Error ? error.message : String(error);
280
- void process.stderr.write(
281
- `${JSON.stringify({ error: { code: "INTERNAL_ERROR", message } })}\n`,
282
- );
283
- process.exitCode = 1;
360
+ void process.stderr.write(`Error: ${message}\n`);
284
361
  }
285
362
  /**
286
363
  * Handle --help flag by printing help text.
@@ -291,13 +368,13 @@ function handleHelp() {
291
368
  /**
292
369
  * Handle --version flag by reading and printing version from package.json.
293
370
  */
294
- function handleVersion() {
371
+ async function handleVersion() {
295
372
  const pkgPath = resolve(
296
373
  dirname(fileURLToPath(import.meta.url)),
297
374
  "..",
298
375
  "package.json",
299
376
  );
300
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
377
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
301
378
  void process.stdout.write(`${pkg.version}\n`);
302
379
  }
303
380
  /**
@@ -309,7 +386,7 @@ function handleSkill() {
309
386
  /**
310
387
  * Handle --explain-rule flag by printing rule explanation.
311
388
  */
312
- function handleExplainRule(ruleName) {
389
+ function handleExplainRule(ruleName, json) {
313
390
  const explanation = RULE_EXPLANATIONS[ruleName];
314
391
  if (explanation) {
315
392
  void process.stdout.write(explanation);
@@ -321,39 +398,65 @@ function handleExplainRule(ruleName) {
321
398
  "INVALID_SELECTOR",
322
399
  `Unknown rule: ${ruleName}. Available rules: ${available}`,
323
400
  ),
401
+ json,
324
402
  );
325
403
  }
404
+ /**
405
+ * Handle early-exit flags that print output and return without processing files.
406
+ * Returns true if an early-exit flag was handled.
407
+ */
408
+ async function handleEarlyExitFlags(parsed, json) {
409
+ if (parsed.help) {
410
+ handleHelp();
411
+ return true;
412
+ }
413
+ if (parsed.version) {
414
+ await handleVersion();
415
+ return true;
416
+ }
417
+ if (parsed.skillMode) {
418
+ handleSkill();
419
+ return true;
420
+ }
421
+ if (parsed.explainRule !== undefined) {
422
+ handleExplainRule(parsed.explainRule, json);
423
+ return true;
424
+ }
425
+ return false;
426
+ }
427
+ /**
428
+ * Validate that mode flags are not used in incompatible combinations.
429
+ * Returns true if validation passed (no conflicts), false if an error was written.
430
+ */
431
+ function validateModeCombinations(parsed, json) {
432
+ if (parsed.checkMode && parsed.lintMode) {
433
+ writeError(
434
+ new JsdocError("INVALID_SELECTOR", "Cannot use -c and -l together"),
435
+ json,
436
+ );
437
+ return false;
438
+ }
439
+ if (
440
+ parsed.searchQuery !== undefined &&
441
+ (parsed.checkMode || parsed.lintMode)
442
+ ) {
443
+ writeError(
444
+ new JsdocError("INVALID_SELECTOR", "Cannot use --search with -c or -l"),
445
+ json,
446
+ );
447
+ return false;
448
+ }
449
+ return true;
450
+ }
326
451
  /**
327
452
  * Main CLI entry point. Exported for testability.
328
453
  */
329
454
  export async function main(args, stdin) {
455
+ const json = args.includes("--json");
330
456
  try {
331
457
  const parsed = parseArgs(args);
332
- // Handle early-exit flags
333
- if (parsed.help) {
334
- handleHelp();
335
- return;
336
- }
337
- if (parsed.version) {
338
- handleVersion();
339
- return;
340
- }
341
- if (parsed.skillMode) {
342
- handleSkill();
343
- return;
344
- }
345
- if (parsed.explainRule !== undefined) {
346
- handleExplainRule(parsed.explainRule);
347
- return;
348
- }
349
- // Validate mode combinations
350
- if (parsed.checkMode && parsed.lintMode) {
351
- writeError(
352
- new JsdocError("INVALID_SELECTOR", "Cannot use -c and -l together"),
353
- );
354
- return;
355
- }
356
- // Build cache config and process files
458
+ if (await handleEarlyExitFlags(parsed, json)) return;
459
+ if (!validateModeCombinations(parsed, json)) return;
357
460
  const cacheConfig = {
358
461
  enabled: !parsed.disableCache,
359
462
  directory: parsed.cacheDirectory ?? DEFAULT_CACHE_DIR,
@@ -370,6 +473,30 @@ export async function main(args, stdin) {
370
473
  parsed.limit,
371
474
  cwd,
372
475
  cacheConfig,
476
+ parsed.searchQuery,
477
+ );
478
+ } else if (parsed.extraArgs.length > 0) {
479
+ // Multiple positional args (e.g. shell-expanded glob): expand each to
480
+ // .ts/.tsx files via discoverFiles (handles directories recursively)
481
+ const allArgPaths = [
482
+ ...(parsed.selectorArg ? [parsed.selectorArg] : []),
483
+ ...parsed.extraArgs,
484
+ ];
485
+ const fileLists = await Promise.all(
486
+ allArgPaths.map((p) => discoverFiles(p, cwd, parsed.gitignore)),
487
+ );
488
+ const filePaths = [...new Set(fileLists.flat())];
489
+ await processFileList(
490
+ filePaths,
491
+ parsed.selectorArg,
492
+ parsed.checkMode,
493
+ parsed.lintMode,
494
+ parsed.json,
495
+ parsed.pretty,
496
+ parsed.limit,
497
+ cwd,
498
+ cacheConfig,
499
+ parsed.searchQuery,
373
500
  );
374
501
  } else {
375
502
  await processSelector(
@@ -382,10 +509,11 @@ export async function main(args, stdin) {
382
509
  parsed.gitignore,
383
510
  cwd,
384
511
  cacheConfig,
512
+ parsed.searchQuery,
385
513
  );
386
514
  }
387
515
  } catch (error) {
388
- writeError(error);
516
+ writeError(error, json);
389
517
  }
390
518
  }
391
519
  /**
package/dist/drilldown.js CHANGED
@@ -32,8 +32,13 @@ function buildLevels(info, typeDeclarations, fileContent) {
32
32
  return [
33
33
  summary !== null ? { text: summary } : null,
34
34
  description !== null ? { text: description } : null,
35
- { text: typeDeclarations },
36
- { text: fileContent },
35
+ {
36
+ text:
37
+ typeDeclarations.length > 0
38
+ ? `\`\`\`typescript\n${typeDeclarations}\n\`\`\``
39
+ : typeDeclarations,
40
+ },
41
+ { text: `\`\`\`typescript\n${fileContent}\n\`\`\`` },
37
42
  ];
38
43
  }
39
44
  /**
@@ -177,7 +182,7 @@ async function gatherBarrelInfos(barrelPaths, cwd, config) {
177
182
  const info = await processWithCache(config, "drilldown", content, () =>
178
183
  parseFileSummaries(barrelPath),
179
184
  );
180
- const children = getBarrelChildren(barrelPath, cwd);
185
+ const children = await getBarrelChildren(barrelPath, cwd);
181
186
  return {
182
187
  type: "info",
183
188
  data: {
@@ -328,7 +333,7 @@ export async function drilldown(
328
333
  ) {
329
334
  const depth = selector.depth ?? 1;
330
335
  if (selector.type === "path") {
331
- const files = discoverFiles(selector.pattern, cwd, gitignore);
336
+ const files = await discoverFiles(selector.pattern, cwd, gitignore);
332
337
  if (files.length > 1) {
333
338
  // Directory expanded to multiple files — route through glob pipeline
334
339
  const results = await processGlobWithBarrels(files, depth, cwd, config);
@@ -358,7 +363,7 @@ export async function drilldown(
358
363
  barrelEffectiveDepth = 3;
359
364
  }
360
365
  if (barrelEffectiveDepth >= 3) {
361
- const children = getBarrelChildren(filePath, cwd);
366
+ const children = await getBarrelChildren(filePath, cwd);
362
367
  const childDepth = barrelEffectiveDepth - 2;
363
368
  const results = await collectSafeResults(
364
369
  children,
@@ -385,7 +390,7 @@ export async function drilldown(
385
390
  return { items, truncated: false };
386
391
  }
387
392
  // Glob selector — apply barrel gating
388
- const files = discoverFiles(selector.pattern, cwd, gitignore);
393
+ const files = await discoverFiles(selector.pattern, cwd, gitignore);
389
394
  if (files.length === 0) {
390
395
  throw new JsdocError(
391
396
  "NO_FILES_MATCHED",
@@ -422,7 +427,7 @@ export async function drilldownFiles(
422
427
  config = DEFAULT_CACHE_CONFIG,
423
428
  ) {
424
429
  const d = depth ?? 1;
425
- const ig = loadGitignore(cwd);
430
+ const ig = await loadGitignore(cwd);
426
431
  const tsFiles = filePaths.filter((f) => {
427
432
  if (!(f.endsWith(".ts") || f.endsWith(".tsx")) || f.endsWith(".d.ts")) {
428
433
  return false;
@@ -1,6 +1,6 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
1
+ import { readFile, stat } from "node:fs/promises";
2
2
  import { dirname, join, relative, resolve } from "node:path";
3
- import { globSync } from "glob";
3
+ import { glob } from "glob";
4
4
  import ignore from "ignore";
5
5
  import { JsdocError } from "./errors.js";
6
6
  /**
@@ -15,13 +15,13 @@ import { JsdocError } from "./errors.js";
15
15
  * Walk from `cwd` up to the filesystem root, collecting .gitignore entries.
16
16
  * Returns an Ignore instance loaded with all discovered rules.
17
17
  */
18
- export function loadGitignore(cwd) {
18
+ export async function loadGitignore(cwd) {
19
19
  const ig = ignore();
20
20
  let dir = resolve(cwd);
21
21
  while (true) {
22
22
  const gitignorePath = join(dir, ".gitignore");
23
- if (existsSync(gitignorePath)) {
24
- const content = readFileSync(gitignorePath, "utf-8");
23
+ try {
24
+ const content = await readFile(gitignorePath, "utf-8");
25
25
  const prefix = relative(cwd, dir);
26
26
  const lines = content
27
27
  .split("\n")
@@ -32,6 +32,8 @@ export function loadGitignore(cwd) {
32
32
  // relative to `cwd`, which is where glob results are anchored.
33
33
  ig.add(prefix ? `${prefix}/${line}` : line);
34
34
  }
35
+ } catch {
36
+ // No .gitignore at this level, continue walking up
35
37
  }
36
38
  const parent = dirname(dir);
37
39
  if (parent === dir) break;
@@ -52,25 +54,28 @@ export function loadGitignore(cwd) {
52
54
  * @returns Array of absolute file paths
53
55
  * @throws {JsdocError} FILE_NOT_FOUND when a direct path does not exist
54
56
  */
55
- export function discoverFiles(pattern, cwd, gitignore = true) {
57
+ export async function discoverFiles(pattern, cwd, gitignore = true) {
56
58
  const hasGlobChars = /[*?[\]{]/.test(pattern);
57
59
  if (hasGlobChars) {
58
- const matches = globSync(pattern, { cwd, absolute: true });
60
+ const matches = await glob(pattern, { cwd, absolute: true });
59
61
  let filtered = matches.filter(
60
62
  (f) => (f.endsWith(".ts") || f.endsWith(".tsx")) && !f.endsWith(".d.ts"),
61
63
  );
62
64
  if (gitignore) {
63
- const ig = loadGitignore(cwd);
65
+ const ig = await loadGitignore(cwd);
64
66
  filtered = filtered.filter((abs) => !ig.ignores(relative(cwd, abs)));
65
67
  }
66
68
  return filtered.sort();
67
69
  }
68
70
  // Direct path
69
71
  const resolved = resolve(cwd, pattern);
70
- if (!existsSync(resolved)) {
72
+ let statResult;
73
+ try {
74
+ statResult = await stat(resolved);
75
+ } catch {
71
76
  throw new JsdocError("FILE_NOT_FOUND", `File not found: ${pattern}`);
72
77
  }
73
- if (statSync(resolved).isDirectory()) {
78
+ if (statResult.isDirectory()) {
74
79
  return discoverFiles(`${resolved}/**`, cwd, gitignore);
75
80
  }
76
81
  return [resolved];
package/dist/index.js CHANGED
@@ -13,8 +13,13 @@ export { JsdocError } from "./errors.js";
13
13
  export { discoverFiles } from "./file-discovery.js";
14
14
  export { extractFileJsdoc, parseFileSummaries } from "./jsdoc-parser.js";
15
15
  export { lint, lintFiles } from "./lint.js";
16
+ export { search, searchFiles } from "./search.js";
16
17
  export { parseSelector } from "./selector.js";
17
18
  export { formatTextOutput } from "./text-format.js";
18
- export { generateTypeDeclarations } from "./type-declarations.js";
19
+ export {
20
+ extractSourceBlocks,
21
+ generateTypeDeclarations,
22
+ splitDeclarations,
23
+ } from "./type-declarations.js";
19
24
  export { DEFAULT_CACHE_DIR, VALIDATION_STATUS_PRIORITY } from "./types.js";
20
25
  export { validate, validateFiles } from "./validate.js";