jsdoczoom 0.4.21 → 1.2.0

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,12 +1,14 @@
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
7
  import { lint, lintFiles } from "./lint.js";
8
+ import { search, searchFiles } from "./search.js";
8
9
  import { parseSelector } from "./selector.js";
9
10
  import { RULE_EXPLANATIONS, SKILL_TEXT } from "./skill-text.js";
11
+ import { formatTextOutput } from "./text-format.js";
10
12
  import { DEFAULT_CACHE_DIR, VALIDATION_STATUS_PRIORITY } from "./types.js";
11
13
  import { validate, validateFiles } from "./validate.js";
12
14
 
@@ -27,32 +29,47 @@ Each file has four detail levels (1-indexed): @1 summary, @2 description,
27
29
  Options:
28
30
  -h, --help Show this help text
29
31
  -v, --version Show version number
30
- -c, --check Run validation mode
31
- -l, --lint Run lint mode (comprehensive JSDoc quality)
32
+ -c, --check Validate file-level structure (has JSDoc block, @summary, description)
33
+ -l, --lint Lint comprehensive JSDoc quality (file-level + function-level tags)
32
34
  -s, --skill Print JSDoc writing guidelines
33
- --pretty Format JSON output with 2-space indent
35
+ --json Output as JSON (default is plain text)
36
+ --pretty Format JSON output with 2-space indent (use with --json)
34
37
  --limit N Max results shown (default 500)
35
38
  --no-gitignore Include files ignored by .gitignore
39
+ --search <query> Search files by regex pattern
36
40
  --disable-cache Skip all cache operations
37
41
  --cache-directory Override cache directory (default: system temp)
38
42
  --explain-rule R Explain a lint rule with examples (e.g. jsdoc/informative-docs)
39
43
 
40
44
  Selector:
41
45
  A glob pattern or file path, optionally with @depth suffix (1-4).
46
+
42
47
  Examples:
43
48
  jsdoczoom src/**/*.ts # All .ts files at depth 1 (summary)
44
- jsdoczoom src/index.ts@2 # Single file at depth 2 (description)
49
+ jsdoczoom src/foo.ts@2 # Single file at depth 2 (description)
45
50
  jsdoczoom **/*.ts@3 # All .ts files at depth 3 (type decls)
46
51
 
47
- Stdin:
48
- Pipe file paths one per line:
49
- find . -name "*.ts" | jsdoczoom
50
- find . -name "*.ts" | jsdoczoom @2
51
- find . -name "*.ts" | jsdoczoom -c
52
+ Search (--search):
53
+ Searches all **/*.{ts,tsx} files (or a selector's file set) by regex.
54
+
55
+ Examples:
56
+ jsdoczoom --search "CacheConfig" # find where CacheConfig is used
57
+ jsdoczoom --search "auth-loader" # searches file name
58
+ jsdoczoom src/*.ts --search "TODO|FIXME" # restrict to a file subset
52
59
 
53
60
  Output:
54
- JSON items. Items with "next_id" have more detail; use that value as the next
55
- selector. Items with "id" (no next_id) are at terminal depth.
61
+ Plain text by default. Each item has a "# path@depth" header followed by
62
+ content. Use the header value as the next selector to drill deeper.
63
+
64
+ Use --json for machine-parseable JSON output, use "next_id" to drill deeper.
65
+
66
+ Type declarations (@3) include source line annotations (// LN or // LN-LM)
67
+ so you can locate the implementation in the source file.
68
+
69
+ Stdin:
70
+ Pipe file paths one per line (useful with -c/-l for targeted validation):
71
+ git diff --name-only | jsdoczoom -c # validate changed files
72
+ cat filelist.txt | jsdoczoom -l # lint a specific set of files
56
73
 
57
74
  Barrel gating (glob mode):
58
75
  A barrel's @summary and description reflect the cumulative functionality
@@ -60,25 +77,16 @@ Barrel gating (glob mode):
60
77
  @summary gate sibling files at depths 1-2. At depth 3 the barrel
61
78
  disappears and its children appear at depth 1.
62
79
 
63
- Modes:
64
- -c Validate file-level structure (has JSDoc block, @summary, description)
65
- -l Lint comprehensive JSDoc quality (file-level + function-level tags)
66
-
67
80
  Exit codes:
68
81
  0 Success (all files pass)
69
82
  1 Runtime error (invalid arguments, missing files)
70
83
  2 Validation or lint failures found
71
84
 
72
85
  Workflow:
73
- $ jsdoczoom src/**/*.ts
74
- { "items": [{ "next_id": "src/utils@2", "text": "..." }, ...] }
75
-
76
- $ jsdoczoom src/utils@2 # use next_id from above
77
- { "items": [{ "next_id": "src/utils@3", "text": "..." }] }
78
-
79
- $ jsdoczoom src/utils@3 # use next_id again
80
- { "items": [{ "id": "src/utils@3", "text": "..." }] }
81
- # "id" instead of "next_id" means terminal depth — stop here
86
+ $ jsdoczoom src/**/*.ts # list summaries
87
+ $ jsdoczoom src/utils@2 # drill into description
88
+ $ jsdoczoom src/utils@3 # see type declarations
89
+ $ jsdoczoom src/**/*.ts | grep "^#" # list all file headers
82
90
  `;
83
91
  /**
84
92
  * Parse a flag that requires a value argument.
@@ -97,6 +105,7 @@ function parseArgs(args) {
97
105
  checkMode: false,
98
106
  lintMode: false,
99
107
  skillMode: false,
108
+ json: false,
100
109
  pretty: false,
101
110
  limit: 500,
102
111
  gitignore: true,
@@ -104,6 +113,7 @@ function parseArgs(args) {
104
113
  cacheDirectory: undefined,
105
114
  explainRule: undefined,
106
115
  selectorArg: undefined,
116
+ searchQuery: undefined,
107
117
  };
108
118
  for (let i = 0; i < args.length; i++) {
109
119
  const arg = args[i];
@@ -128,6 +138,10 @@ function parseArgs(args) {
128
138
  parsed.skillMode = true;
129
139
  continue;
130
140
  }
141
+ if (arg === "--json") {
142
+ parsed.json = true;
143
+ continue;
144
+ }
131
145
  if (arg === "--pretty") {
132
146
  parsed.pretty = true;
133
147
  continue;
@@ -159,6 +173,15 @@ function parseArgs(args) {
159
173
  i = nextIndex;
160
174
  continue;
161
175
  }
176
+ if (arg === "--search") {
177
+ const { value, nextIndex } = parseValueFlag(args, i);
178
+ if (value === undefined) {
179
+ throw new JsdocError("INVALID_SELECTOR", "--search requires a value");
180
+ }
181
+ parsed.searchQuery = value;
182
+ i = nextIndex;
183
+ continue;
184
+ }
162
185
  // Unknown flag or positional arg
163
186
  if (arg.startsWith("-")) {
164
187
  throw new JsdocError("INVALID_SELECTOR", `Unrecognized option: ${arg}`);
@@ -196,14 +219,27 @@ async function processStdin(
196
219
  selectorArg,
197
220
  checkMode,
198
221
  lintMode,
222
+ json,
199
223
  pretty,
200
224
  limit,
201
225
  cwd,
202
226
  cacheConfig,
227
+ searchQuery,
203
228
  ) {
204
229
  const stdinPaths = parseStdinPaths(stdin, cwd);
205
230
  const depth =
206
231
  selectorArg !== undefined ? extractDepthFromArg(selectorArg) : undefined;
232
+ if (searchQuery !== undefined) {
233
+ const result = await searchFiles(
234
+ stdinPaths,
235
+ searchQuery,
236
+ cwd,
237
+ limit,
238
+ cacheConfig,
239
+ );
240
+ writeDrilldownResult(result, json, pretty);
241
+ return;
242
+ }
207
243
  if (lintMode) {
208
244
  const result = await lintFiles(stdinPaths, cwd, limit, cacheConfig);
209
245
  writeLintResult(result, pretty);
@@ -218,7 +254,7 @@ async function processStdin(
218
254
  limit,
219
255
  cacheConfig,
220
256
  );
221
- writeResult(result, pretty);
257
+ writeDrilldownResult(result, json, pretty);
222
258
  }
223
259
  }
224
260
  /**
@@ -228,15 +264,29 @@ async function processSelector(
228
264
  selectorArg,
229
265
  checkMode,
230
266
  lintMode,
267
+ json,
231
268
  pretty,
232
269
  limit,
233
270
  gitignore,
234
271
  cwd,
235
272
  cacheConfig,
273
+ searchQuery,
236
274
  ) {
237
275
  const selector = selectorArg
238
276
  ? parseSelector(selectorArg)
239
277
  : { type: "glob", pattern: "**/*.{ts,tsx}", depth: undefined };
278
+ if (searchQuery !== undefined) {
279
+ const result = await search(
280
+ { type: selector.type, pattern: selector.pattern, depth: undefined },
281
+ searchQuery,
282
+ cwd,
283
+ gitignore,
284
+ limit,
285
+ cacheConfig,
286
+ );
287
+ writeDrilldownResult(result, json, pretty);
288
+ return;
289
+ }
240
290
  if (lintMode) {
241
291
  const result = await lint(selector, cwd, limit, gitignore, cacheConfig);
242
292
  writeLintResult(result, pretty);
@@ -251,7 +301,7 @@ async function processSelector(
251
301
  limit,
252
302
  cacheConfig,
253
303
  );
254
- writeResult(result, pretty);
304
+ writeDrilldownResult(result, json, pretty);
255
305
  }
256
306
  }
257
307
  /**
@@ -278,13 +328,13 @@ function handleHelp() {
278
328
  /**
279
329
  * Handle --version flag by reading and printing version from package.json.
280
330
  */
281
- function handleVersion() {
331
+ async function handleVersion() {
282
332
  const pkgPath = resolve(
283
333
  dirname(fileURLToPath(import.meta.url)),
284
334
  "..",
285
335
  "package.json",
286
336
  );
287
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
337
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
288
338
  void process.stdout.write(`${pkg.version}\n`);
289
339
  }
290
340
  /**
@@ -310,37 +360,59 @@ function handleExplainRule(ruleName) {
310
360
  ),
311
361
  );
312
362
  }
363
+ /**
364
+ * Handle early-exit flags that print output and return without processing files.
365
+ * Returns true if an early-exit flag was handled.
366
+ */
367
+ async function handleEarlyExitFlags(parsed) {
368
+ if (parsed.help) {
369
+ handleHelp();
370
+ return true;
371
+ }
372
+ if (parsed.version) {
373
+ await handleVersion();
374
+ return true;
375
+ }
376
+ if (parsed.skillMode) {
377
+ handleSkill();
378
+ return true;
379
+ }
380
+ if (parsed.explainRule !== undefined) {
381
+ handleExplainRule(parsed.explainRule);
382
+ return true;
383
+ }
384
+ return false;
385
+ }
386
+ /**
387
+ * Validate that mode flags are not used in incompatible combinations.
388
+ * Returns true if validation passed (no conflicts), false if an error was written.
389
+ */
390
+ function validateModeCombinations(parsed) {
391
+ if (parsed.checkMode && parsed.lintMode) {
392
+ writeError(
393
+ new JsdocError("INVALID_SELECTOR", "Cannot use -c and -l together"),
394
+ );
395
+ return false;
396
+ }
397
+ if (
398
+ parsed.searchQuery !== undefined &&
399
+ (parsed.checkMode || parsed.lintMode)
400
+ ) {
401
+ writeError(
402
+ new JsdocError("INVALID_SELECTOR", "Cannot use --search with -c or -l"),
403
+ );
404
+ return false;
405
+ }
406
+ return true;
407
+ }
313
408
  /**
314
409
  * Main CLI entry point. Exported for testability.
315
410
  */
316
411
  export async function main(args, stdin) {
317
412
  try {
318
413
  const parsed = parseArgs(args);
319
- // Handle early-exit flags
320
- if (parsed.help) {
321
- handleHelp();
322
- return;
323
- }
324
- if (parsed.version) {
325
- handleVersion();
326
- return;
327
- }
328
- if (parsed.skillMode) {
329
- handleSkill();
330
- return;
331
- }
332
- if (parsed.explainRule !== undefined) {
333
- handleExplainRule(parsed.explainRule);
334
- return;
335
- }
336
- // Validate mode combinations
337
- if (parsed.checkMode && parsed.lintMode) {
338
- writeError(
339
- new JsdocError("INVALID_SELECTOR", "Cannot use -c and -l together"),
340
- );
341
- return;
342
- }
343
- // Build cache config and process files
414
+ if (await handleEarlyExitFlags(parsed)) return;
415
+ if (!validateModeCombinations(parsed)) return;
344
416
  const cacheConfig = {
345
417
  enabled: !parsed.disableCache,
346
418
  directory: parsed.cacheDirectory ?? DEFAULT_CACHE_DIR,
@@ -352,27 +424,41 @@ export async function main(args, stdin) {
352
424
  parsed.selectorArg,
353
425
  parsed.checkMode,
354
426
  parsed.lintMode,
427
+ parsed.json,
355
428
  parsed.pretty,
356
429
  parsed.limit,
357
430
  cwd,
358
431
  cacheConfig,
432
+ parsed.searchQuery,
359
433
  );
360
434
  } else {
361
435
  await processSelector(
362
436
  parsed.selectorArg,
363
437
  parsed.checkMode,
364
438
  parsed.lintMode,
439
+ parsed.json,
365
440
  parsed.pretty,
366
441
  parsed.limit,
367
442
  parsed.gitignore,
368
443
  cwd,
369
444
  cacheConfig,
445
+ parsed.searchQuery,
370
446
  );
371
447
  }
372
448
  } catch (error) {
373
449
  writeError(error);
374
450
  }
375
451
  }
452
+ /**
453
+ * Write a drilldown result to stdout as text (default) or JSON.
454
+ */
455
+ function writeDrilldownResult(result, json, pretty) {
456
+ if (json) {
457
+ writeResult(result, pretty);
458
+ } else {
459
+ void process.stdout.write(formatTextOutput(result));
460
+ }
461
+ }
376
462
  /**
377
463
  * Write a result to stdout as JSON.
378
464
  */
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,18 +333,19 @@ 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);
335
340
  const sorted = results.sort((a, b) =>
336
341
  sortKey(a).localeCompare(sortKey(b)),
337
342
  );
338
- const total = sorted.length;
339
- const truncated = total > limit;
343
+ const count = sorted.length;
344
+ const isTruncated = count > limit;
340
345
  return {
341
346
  items: sorted.slice(0, limit),
342
- truncated,
347
+ truncated: isTruncated,
348
+ ...(isTruncated ? { total: count } : {}),
343
349
  };
344
350
  }
345
351
  // Single file path — errors are fatal
@@ -357,7 +363,7 @@ export async function drilldown(
357
363
  barrelEffectiveDepth = 3;
358
364
  }
359
365
  if (barrelEffectiveDepth >= 3) {
360
- const children = getBarrelChildren(filePath, cwd);
366
+ const children = await getBarrelChildren(filePath, cwd);
361
367
  const childDepth = barrelEffectiveDepth - 2;
362
368
  const results = await collectSafeResults(
363
369
  children,
@@ -384,7 +390,7 @@ export async function drilldown(
384
390
  return { items, truncated: false };
385
391
  }
386
392
  // Glob selector — apply barrel gating
387
- const files = discoverFiles(selector.pattern, cwd, gitignore);
393
+ const files = await discoverFiles(selector.pattern, cwd, gitignore);
388
394
  if (files.length === 0) {
389
395
  throw new JsdocError(
390
396
  "NO_FILES_MATCHED",
@@ -394,11 +400,12 @@ export async function drilldown(
394
400
  const results = await processGlobWithBarrels(files, depth, cwd, config);
395
401
  // Sort alphabetically by key
396
402
  const sorted = results.sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
397
- const total = sorted.length;
398
- const truncated = total > limit;
403
+ const count = sorted.length;
404
+ const isTruncated = count > limit;
399
405
  return {
400
406
  items: sorted.slice(0, limit),
401
- truncated,
407
+ truncated: isTruncated,
408
+ ...(isTruncated ? { total: count } : {}),
402
409
  };
403
410
  }
404
411
  /**
@@ -420,7 +427,7 @@ export async function drilldownFiles(
420
427
  config = DEFAULT_CACHE_CONFIG,
421
428
  ) {
422
429
  const d = depth ?? 1;
423
- const ig = loadGitignore(cwd);
430
+ const ig = await loadGitignore(cwd);
424
431
  const tsFiles = filePaths.filter((f) => {
425
432
  if (!(f.endsWith(".ts") || f.endsWith(".tsx")) || f.endsWith(".d.ts")) {
426
433
  return false;
@@ -432,10 +439,11 @@ export async function drilldownFiles(
432
439
  });
433
440
  const results = await collectSafeResults(tsFiles, d, cwd, config);
434
441
  const sorted = results.sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
435
- const total = sorted.length;
436
- const truncated = total > limit;
442
+ const count = sorted.length;
443
+ const isTruncated = count > limit;
437
444
  return {
438
445
  items: sorted.slice(0, limit),
439
- truncated,
446
+ truncated: isTruncated,
447
+ ...(isTruncated ? { total: count } : {}),
440
448
  };
441
449
  }
@@ -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,7 +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
- export { generateTypeDeclarations } from "./type-declarations.js";
18
+ export { formatTextOutput } from "./text-format.js";
19
+ export {
20
+ extractSourceBlocks,
21
+ generateTypeDeclarations,
22
+ splitDeclarations,
23
+ } from "./type-declarations.js";
18
24
  export { DEFAULT_CACHE_DIR, VALIDATION_STATUS_PRIORITY } from "./types.js";
19
25
  export { validate, validateFiles } from "./validate.js";
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFile } from "node:fs/promises";
2
2
  import ts from "typescript";
3
3
  import { JsdocError } from "./errors.js";
4
4
  import { appendText, DESCRIPTION_TAGS } from "./types.js";
@@ -220,10 +220,10 @@ function parseJsdocContent(jsdocText) {
220
220
  * Reads the file, extracts the first file-level JSDoc block, and parses the first
221
221
  * @summary tag and free-text description.
222
222
  */
223
- export function parseFileSummaries(filePath) {
223
+ export async function parseFileSummaries(filePath) {
224
224
  let sourceText;
225
225
  try {
226
- sourceText = readFileSync(filePath, "utf-8");
226
+ sourceText = await readFile(filePath, "utf-8");
227
227
  } catch {
228
228
  throw new JsdocError("FILE_NOT_FOUND", `File not found: ${filePath}`);
229
229
  }