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.
@@ -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
  }
package/dist/lint.js CHANGED
@@ -89,7 +89,7 @@ export async function lint(
89
89
  gitignore = true,
90
90
  config = { enabled: true, directory: DEFAULT_CACHE_DIR },
91
91
  ) {
92
- const files = discoverFiles(selector.pattern, cwd, gitignore);
92
+ const files = await discoverFiles(selector.pattern, cwd, gitignore);
93
93
  if (files.length === 0 && selector.type === "glob") {
94
94
  throw new JsdocError(
95
95
  "NO_FILES_MATCHED",
@@ -101,7 +101,7 @@ export async function lint(
101
101
  const fileResults = await Promise.all(
102
102
  tsFiles.map((f) => lintSingleFile(eslint, f, cwd, config)),
103
103
  );
104
- const missingBarrels = findMissingBarrels(tsFiles, cwd);
104
+ const missingBarrels = await findMissingBarrels(tsFiles, cwd);
105
105
  return buildLintResult(fileResults, tsFiles.length, limit, missingBarrels);
106
106
  }
107
107
  /**
@@ -129,6 +129,6 @@ export async function lintFiles(
129
129
  const fileResults = await Promise.all(
130
130
  tsFiles.map((f) => lintSingleFile(eslint, f, cwd, config)),
131
131
  );
132
- const missingBarrels = findMissingBarrels(tsFiles, cwd);
132
+ const missingBarrels = await findMissingBarrels(tsFiles, cwd);
133
133
  return buildLintResult(fileResults, tsFiles.length, limit, missingBarrels);
134
134
  }
package/dist/search.js ADDED
@@ -0,0 +1,226 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { relative } from "node:path";
3
+ import { processWithCache } from "./cache.js";
4
+ import { JsdocError } from "./errors.js";
5
+ import { discoverFiles, loadGitignore } from "./file-discovery.js";
6
+ import { parseFileSummaries } from "./jsdoc-parser.js";
7
+ import {
8
+ extractAllSourceBlocks,
9
+ generateTypeDeclarations,
10
+ splitDeclarations,
11
+ } from "./type-declarations.js";
12
+ import { DEFAULT_CACHE_DIR } from "./types.js";
13
+
14
+ /**
15
+ * Searches TypeScript files for a regex query, returning results at the
16
+ * shallowest matching depth level. Processes each file through a 4-level
17
+ * matching hierarchy: filename/path, summary, description/declarations,
18
+ * and full source content.
19
+ *
20
+ * @summary Search TypeScript documentation and source by regex query
21
+ */
22
+ /** Default cache configuration used when no config is provided. */
23
+ const DEFAULT_CACHE_CONFIG = {
24
+ enabled: true,
25
+ directory: DEFAULT_CACHE_DIR,
26
+ };
27
+ /**
28
+ * Compute the display id path for a file.
29
+ * Uses simple relative path — no barrel-to-directory conversion.
30
+ */
31
+ function displayPath(filePath, cwd) {
32
+ return relative(cwd, filePath);
33
+ }
34
+ /**
35
+ * Extract the sort key from an output entry (either next_id or id).
36
+ */
37
+ function sortKey(entry) {
38
+ if ("next_id" in entry) return entry.next_id;
39
+ return entry.id;
40
+ }
41
+ /**
42
+ * Process a single file through the 4-level search hierarchy.
43
+ * Returns an OutputEntry if the file matches the query at any level,
44
+ * or null if there is no match. PARSE_ERROR exceptions are rethrown
45
+ * for the caller to handle silently.
46
+ */
47
+ async function processFileSafe(filePath, regex, cwd, config) {
48
+ const content = await readFile(filePath, "utf-8");
49
+ const idPath = displayPath(filePath, cwd);
50
+ // Parse summaries once for levels 1-3a
51
+ const info = await processWithCache(config, "drilldown", content, () =>
52
+ parseFileSummaries(filePath),
53
+ );
54
+ // Level 1: filename/path match — fall back through levels like drilldown
55
+ if (regex.test(idPath)) {
56
+ if (info.summary !== null) {
57
+ return { next_id: `${idPath}@2`, text: info.summary };
58
+ }
59
+ if (info.description !== null) {
60
+ return { next_id: `${idPath}@3`, text: info.description };
61
+ }
62
+ const dts = await processWithCache(
63
+ config,
64
+ "drilldown",
65
+ `${content}\0typedecl`,
66
+ () => generateTypeDeclarations(filePath),
67
+ );
68
+ if (dts.length > 0) {
69
+ const chunks = splitDeclarations(dts);
70
+ return {
71
+ next_id: `${idPath}@4`,
72
+ text: chunks.map((c) => `\`\`\`typescript\n${c}\n\`\`\``).join("\n\n"),
73
+ };
74
+ }
75
+ return { id: `${idPath}@4`, text: `\`\`\`typescript\n${content}\n\`\`\`` };
76
+ }
77
+ // Level 2: summary match
78
+ if (info.summary !== null && regex.test(info.summary)) {
79
+ return { next_id: `${idPath}@2`, text: info.summary };
80
+ }
81
+ // Level 3a: description match
82
+ if (info.description !== null && regex.test(info.description)) {
83
+ return { next_id: `${idPath}@3`, text: info.description };
84
+ }
85
+ // Level 3b: type declaration match
86
+ const dts = await processWithCache(
87
+ config,
88
+ "drilldown",
89
+ `${content}\0typedecl`,
90
+ () => generateTypeDeclarations(filePath),
91
+ );
92
+ if (dts.length > 0) {
93
+ const chunks = splitDeclarations(dts);
94
+ const matching = chunks.filter((c) => regex.test(c));
95
+ if (matching.length > 0) {
96
+ return {
97
+ next_id: `${idPath}@3`,
98
+ text: matching
99
+ .map((c) => `\`\`\`typescript\n${c}\n\`\`\``)
100
+ .join("\n\n"),
101
+ };
102
+ }
103
+ }
104
+ // Level 4: source match — cache block extraction, filter by regex
105
+ const allBlocks = await processWithCache(
106
+ config,
107
+ "drilldown",
108
+ `${content}\0sourceblocks`,
109
+ () => extractAllSourceBlocks(filePath, content),
110
+ );
111
+ const matchingBlocks = allBlocks.filter((b) => regex.test(b.blockText));
112
+ if (matchingBlocks.length > 0) {
113
+ const fenced = matchingBlocks
114
+ .map((b) => `\`\`\`typescript\n${b.annotation}\n${b.blockText}\n\`\`\``)
115
+ .join("\n\n");
116
+ return { id: `${idPath}@4`, text: fenced };
117
+ }
118
+ if (regex.test(content)) {
119
+ return { id: `${idPath}@4`, text: `\`\`\`typescript\n${content}\n\`\`\`` };
120
+ }
121
+ return null; // no match
122
+ }
123
+ /**
124
+ * Compile a query string into a case-insensitive regex.
125
+ * Throws INVALID_SELECTOR if the query is not a valid regex pattern.
126
+ */
127
+ function compileRegex(query) {
128
+ try {
129
+ return new RegExp(query, "i");
130
+ } catch (_error) {
131
+ throw new JsdocError("INVALID_SELECTOR", `Invalid regex: ${query}`);
132
+ }
133
+ }
134
+ /**
135
+ * Apply limit/truncation to sorted results.
136
+ */
137
+ function applyLimit(sorted, limit) {
138
+ const count = sorted.length;
139
+ const isTruncated = count > limit;
140
+ return {
141
+ items: sorted.slice(0, limit),
142
+ truncated: isTruncated,
143
+ ...(isTruncated ? { total: count } : {}),
144
+ };
145
+ }
146
+ /**
147
+ * Process a list of file paths through the search hierarchy and return sorted results.
148
+ * Files with parse errors are silently skipped.
149
+ */
150
+ async function searchFileList(files, regex, cwd, limit, config) {
151
+ const results = await Promise.all(
152
+ files.map(async (filePath) => {
153
+ try {
154
+ return await processFileSafe(filePath, regex, cwd, config);
155
+ } catch (error) {
156
+ if (error instanceof JsdocError && error.code === "PARSE_ERROR") {
157
+ return null;
158
+ }
159
+ throw error;
160
+ }
161
+ }),
162
+ );
163
+ const matched = results.filter((r) => r !== null);
164
+ const sorted = matched.sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
165
+ return applyLimit(sorted, limit);
166
+ }
167
+ /**
168
+ * Search TypeScript files matching a selector for a regex query.
169
+ *
170
+ * Discovers files from the selector pattern, processes each through
171
+ * the 4-level matching hierarchy, and returns results at the shallowest
172
+ * matching depth. Files with parse errors are silently skipped.
173
+ *
174
+ * @param selector - Parsed selector with type and pattern
175
+ * @param query - Regex query string (case-insensitive)
176
+ * @param cwd - Working directory for file resolution
177
+ * @param gitignore - Whether to respect .gitignore rules (default true)
178
+ * @param limit - Maximum number of results to return (default 100)
179
+ * @param config - Cache configuration
180
+ * @throws {JsdocError} INVALID_SELECTOR for invalid regex
181
+ */
182
+ export async function search(
183
+ selector,
184
+ query,
185
+ cwd,
186
+ gitignore = true,
187
+ limit = 100,
188
+ config = DEFAULT_CACHE_CONFIG,
189
+ ) {
190
+ const regex = compileRegex(query);
191
+ const files = await discoverFiles(selector.pattern, cwd, gitignore);
192
+ return searchFileList(files, regex, cwd, limit, config);
193
+ }
194
+ /**
195
+ * Search an explicit list of file paths for a regex query.
196
+ *
197
+ * Used for stdin input. Filters to .ts/.tsx files, excludes .d.ts.
198
+ * Processes each file through the 4-level matching hierarchy.
199
+ *
200
+ * @param filePaths - Array of absolute file paths
201
+ * @param query - Regex query string (case-insensitive)
202
+ * @param cwd - Working directory for relative path output
203
+ * @param limit - Maximum number of results to return (default 100)
204
+ * @param config - Cache configuration
205
+ * @throws {JsdocError} INVALID_SELECTOR for invalid regex
206
+ */
207
+ export async function searchFiles(
208
+ filePaths,
209
+ query,
210
+ cwd,
211
+ limit = 100,
212
+ config = DEFAULT_CACHE_CONFIG,
213
+ ) {
214
+ const regex = compileRegex(query);
215
+ const ig = await loadGitignore(cwd);
216
+ const tsFiles = filePaths.filter((f) => {
217
+ if (!(f.endsWith(".ts") || f.endsWith(".tsx")) || f.endsWith(".d.ts")) {
218
+ return false;
219
+ }
220
+ const rel = relative(cwd, f);
221
+ // Files outside cwd (traversal paths) are beyond the gitignore scope
222
+ if (rel.startsWith("..")) return true;
223
+ return !ig.ignores(rel);
224
+ });
225
+ return searchFileList(tsFiles, regex, cwd, limit, config);
226
+ }
@@ -19,12 +19,12 @@ function formatEntry(entry) {
19
19
  if (children) {
20
20
  lines.push(`## children: ${children.join(", ")}`);
21
21
  }
22
- if (text) lines.push(text);
22
+ if (text) lines.push("", text);
23
23
  return lines.join("\n");
24
24
  }
25
25
  // Terminal item
26
26
  if (!text) return `# ${entry.id}`;
27
- return `# ${entry.id}\n${text}`;
27
+ return `# ${entry.id}\n\n${text}`;
28
28
  }
29
29
  /**
30
30
  * Format a DrilldownResult as plain text for CLI output.
@@ -32,7 +32,7 @@ function formatEntry(entry) {
32
32
  */
33
33
  export function formatTextOutput(result) {
34
34
  const blocks = result.items.map(formatEntry);
35
- let output = blocks.join("\n\n");
35
+ let output = blocks.join("\n\n\n");
36
36
  if (result.truncated && result.total !== undefined) {
37
37
  output += `\n\n# truncated (showing ${result.items.length} of ${result.total})`;
38
38
  } else if (result.truncated) {
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFile } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
3
  import ts from "typescript";
4
4
  import { JsdocError } from "./errors.js";
@@ -153,12 +153,44 @@ export function resetCache() {
153
153
  compilerOptionsCache.clear();
154
154
  serviceCache.clear();
155
155
  }
156
+ /**
157
+ * Extract exported symbol names from a top-level statement.
158
+ * Returns an array of names (variable statements may have multiple).
159
+ */
160
+ function exportedNamesOf(statement) {
161
+ if (ts.isTypeAliasDeclaration(statement)) return [statement.name.text];
162
+ if (ts.isInterfaceDeclaration(statement)) return [statement.name.text];
163
+ if (ts.isFunctionDeclaration(statement) && statement.name) {
164
+ return [statement.name.text];
165
+ }
166
+ if (ts.isClassDeclaration(statement) && statement.name) {
167
+ return [statement.name.text];
168
+ }
169
+ if (ts.isEnumDeclaration(statement)) return [statement.name.text];
170
+ if (ts.isVariableStatement(statement)) {
171
+ return statement.declarationList.declarations
172
+ .filter((d) => ts.isIdentifier(d.name))
173
+ .map((d) => d.name.text);
174
+ }
175
+ return [];
176
+ }
177
+ /**
178
+ * Return true if the statement has an `export` modifier.
179
+ */
180
+ function isExportedStatement(statement) {
181
+ if (!ts.canHaveModifiers(statement)) return false;
182
+ return (
183
+ ts
184
+ .getModifiers(statement)
185
+ ?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false
186
+ );
187
+ }
156
188
  /**
157
189
  * Build a map of exported symbol names to their source line ranges.
158
190
  * Walks top-level statements looking for export modifiers.
159
191
  */
160
- function buildSourceLineMap(filePath) {
161
- const content = readFileSync(filePath, "utf-8");
192
+ async function buildSourceLineMap(filePath) {
193
+ const content = await readFile(filePath, "utf-8");
162
194
  const sourceFile = ts.createSourceFile(
163
195
  filePath,
164
196
  content,
@@ -167,36 +199,14 @@ function buildSourceLineMap(filePath) {
167
199
  );
168
200
  const map = new Map();
169
201
  for (const statement of sourceFile.statements) {
170
- // Only process exported declarations
171
- const modifiers = ts.canHaveModifiers(statement)
172
- ? ts.getModifiers(statement)
173
- : undefined;
174
- const isExported = modifiers?.some(
175
- (m) => m.kind === ts.SyntaxKind.ExportKeyword,
176
- );
177
- if (!isExported) continue;
202
+ if (!isExportedStatement(statement)) continue;
178
203
  const start =
179
204
  sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile))
180
205
  .line + 1;
181
206
  const end =
182
207
  sourceFile.getLineAndCharacterOfPosition(statement.getEnd()).line + 1;
183
- const addName = (name) => map.set(name, { start, end });
184
- if (ts.isTypeAliasDeclaration(statement)) {
185
- addName(statement.name.text);
186
- } else if (ts.isInterfaceDeclaration(statement)) {
187
- addName(statement.name.text);
188
- } else if (ts.isFunctionDeclaration(statement) && statement.name) {
189
- addName(statement.name.text);
190
- } else if (ts.isClassDeclaration(statement) && statement.name) {
191
- addName(statement.name.text);
192
- } else if (ts.isEnumDeclaration(statement)) {
193
- addName(statement.name.text);
194
- } else if (ts.isVariableStatement(statement)) {
195
- for (const decl of statement.declarationList.declarations) {
196
- if (ts.isIdentifier(decl.name)) {
197
- addName(decl.name.text);
198
- }
199
- }
208
+ for (const name of exportedNamesOf(statement)) {
209
+ map.set(name, { start, end });
200
210
  }
201
211
  }
202
212
  return map;
@@ -234,8 +244,8 @@ function findChunkStart(lines, declLine) {
234
244
  * Inserts GitHub-style `// LN` or `// LN-LM` on a separate line before each
235
245
  * declaration chunk, with a blank line above for visual separation.
236
246
  */
237
- function annotateWithSourceLines(dtsText, filePath) {
238
- const lineMap = buildSourceLineMap(filePath);
247
+ async function annotateWithSourceLines(dtsText, filePath) {
248
+ const lineMap = await buildSourceLineMap(filePath);
239
249
  if (lineMap.size === 0) return dtsText;
240
250
  const lines = dtsText.split("\n");
241
251
  // First pass: map chunk start lines to their annotations
@@ -263,6 +273,127 @@ function annotateWithSourceLines(dtsText, filePath) {
263
273
  }
264
274
  return result.join("\n");
265
275
  }
276
+ /**
277
+ * Splits annotated .d.ts text into individual declaration chunks.
278
+ *
279
+ * Splits on blank-line boundaries that precede a line annotation (`// L`)
280
+ * or a declaration keyword (`export`, `declare`). Each chunk is a complete
281
+ * declaration including its preceding JSDoc comment and line annotation.
282
+ * Trailing whitespace is trimmed from each chunk. Empty chunks are filtered out.
283
+ *
284
+ * @param dtsText - The annotated .d.ts text to split
285
+ * @returns Array of declaration chunks
286
+ */
287
+ export function splitDeclarations(dtsText) {
288
+ if (dtsText === "") return [];
289
+ const lines = dtsText.split("\n");
290
+ const chunks = [];
291
+ let start = 0;
292
+ for (let i = 1; i < lines.length; i++) {
293
+ // A split point is a blank line followed by a line annotation or declaration keyword
294
+ if (lines[i - 1].trim() === "") {
295
+ const nextLine = lines[i].trimStart();
296
+ const isAnnotation = nextLine.startsWith("// L");
297
+ const isDeclaration =
298
+ nextLine.startsWith("export") || nextLine.startsWith("declare");
299
+ if (isAnnotation || isDeclaration) {
300
+ const chunk = lines
301
+ .slice(start, i - 1)
302
+ .join("\n")
303
+ .trimEnd();
304
+ if (chunk !== "") chunks.push(chunk);
305
+ start = i;
306
+ }
307
+ }
308
+ }
309
+ // Push final chunk
310
+ const last = lines.slice(start).join("\n").trimEnd();
311
+ if (last !== "") chunks.push(last);
312
+ return chunks;
313
+ }
314
+ /**
315
+ * Find the 0-based line index where a leading JSDoc block starts above a statement.
316
+ * Skips blank lines above the statement, then looks for a `/** ... * /` block.
317
+ * Returns `statementLine` itself when no leading JSDoc is found.
318
+ */
319
+ function findJsdocStart(lines, statementLine) {
320
+ let line = statementLine - 1;
321
+ // Skip blank lines immediately before the statement
322
+ while (line >= 0 && lines[line].trim() === "") line--;
323
+ if (line < 0) return statementLine;
324
+ const trimmed = lines[line].trimEnd();
325
+ if (trimmed === "*/" || trimmed.endsWith("*/")) {
326
+ // Multi-line JSDoc: walk back to find opening /**
327
+ for (let j = line; j >= 0; j--) {
328
+ if (lines[j].trimStart().startsWith("/**")) return j;
329
+ }
330
+ } else if (trimmed.trimStart().startsWith("/**")) {
331
+ // Single-line JSDoc: /** comment */
332
+ return line;
333
+ }
334
+ return statementLine;
335
+ }
336
+ /**
337
+ * Extracts all top-level source blocks from a TypeScript file.
338
+ * Walks all top-level statements (not just exported), includes leading
339
+ * JSDoc comments, and annotates each block with source line references.
340
+ * Import declarations are excluded.
341
+ *
342
+ * Returns an empty array for empty files.
343
+ *
344
+ * @param filePath - Absolute path to the TypeScript source file
345
+ * @param content - Pre-read file content (avoids redundant disk read)
346
+ */
347
+ export function extractAllSourceBlocks(filePath, content) {
348
+ if (content.trim() === "") return [];
349
+ const lines = content.split("\n");
350
+ const sourceFile = ts.createSourceFile(
351
+ filePath,
352
+ content,
353
+ ts.ScriptTarget.Latest,
354
+ true,
355
+ );
356
+ const blocks = [];
357
+ const seen = new Set();
358
+ for (const statement of sourceFile.statements) {
359
+ // Skip import declarations — callers fall back to full file for import matches
360
+ if (ts.isImportDeclaration(statement)) continue;
361
+ const endLine =
362
+ sourceFile.getLineAndCharacterOfPosition(statement.getEnd()).line + 1;
363
+ const stmtStartLine = sourceFile.getLineAndCharacterOfPosition(
364
+ statement.getStart(sourceFile),
365
+ ).line; // 0-based
366
+ const jsdocStartLine = findJsdocStart(lines, stmtStartLine); // 0-based
367
+ // Skip if we've already included this block (deduplication by start position)
368
+ if (seen.has(jsdocStartLine)) continue;
369
+ seen.add(jsdocStartLine);
370
+ // Extract the source text for this block (lines are 0-based, endLine is 1-based)
371
+ const blockText = lines.slice(jsdocStartLine, endLine).join("\n");
372
+ const startLine1based = jsdocStartLine + 1; // 1-based
373
+ const annotation = formatLineRef({ start: startLine1based, end: endLine });
374
+ blocks.push({ annotation, blockText });
375
+ }
376
+ return blocks;
377
+ }
378
+ /**
379
+ * Extracts top-level source blocks from a file that match a regex.
380
+ *
381
+ * Walks all top-level statements (not just exported ones), includes leading
382
+ * JSDoc comments, and tests the regex against each block's source text.
383
+ * Matching blocks are annotated with `// LN` or `// LN-LM` and joined with
384
+ * blank line separators.
385
+ *
386
+ * @param filePath - Absolute path to the TypeScript source file
387
+ * @param content - Pre-read file content (avoids redundant disk read)
388
+ * @param regex - Regular expression to test against each block's source text
389
+ * @returns Annotated matching blocks joined with blank lines, or null if no match
390
+ */
391
+ export function extractSourceBlocks(filePath, content, regex) {
392
+ const allBlocks = extractAllSourceBlocks(filePath, content);
393
+ const matching = allBlocks.filter((b) => regex.test(b.blockText));
394
+ if (matching.length === 0) return null;
395
+ return matching.map((b) => `${b.annotation}\n${b.blockText}`).join("\n\n");
396
+ }
266
397
  /**
267
398
  * Generates TypeScript declaration output from a source file.
268
399
  *
@@ -282,10 +413,10 @@ function annotateWithSourceLines(dtsText, filePath) {
282
413
  * @returns The declaration output as a string
283
414
  * @throws {JsdocError} If the file cannot be read or parsed
284
415
  */
285
- export function generateTypeDeclarations(filePath) {
416
+ export async function generateTypeDeclarations(filePath) {
286
417
  // Verify the file exists and throw FILE_NOT_FOUND for any read errors
287
418
  try {
288
- readFileSync(filePath, "utf-8");
419
+ await readFile(filePath, "utf-8");
289
420
  } catch (_error) {
290
421
  throw new JsdocError("FILE_NOT_FOUND", `Failed to read file: ${filePath}`);
291
422
  }
@@ -319,7 +450,7 @@ export function generateTypeDeclarations(filePath) {
319
450
  cleaned = "";
320
451
  }
321
452
  if (cleaned.length > 0) {
322
- cleaned = annotateWithSourceLines(cleaned, filePath);
453
+ cleaned = await annotateWithSourceLines(cleaned, filePath);
323
454
  }
324
455
  return cleaned;
325
456
  }
package/dist/validate.js CHANGED
@@ -97,7 +97,7 @@ export async function validate(
97
97
  gitignore = true,
98
98
  config = { enabled: true, directory: DEFAULT_CACHE_DIR },
99
99
  ) {
100
- const files = discoverFiles(selector.pattern, cwd, gitignore);
100
+ const files = await discoverFiles(selector.pattern, cwd, gitignore);
101
101
  if (files.length === 0) {
102
102
  throw new JsdocError(
103
103
  "NO_FILES_MATCHED",
@@ -108,7 +108,7 @@ export async function validate(
108
108
  const statuses = await Promise.all(
109
109
  files.map((f) => classifyFile(eslint, f, cwd, config)),
110
110
  );
111
- const missingBarrels = findMissingBarrels(files, cwd);
111
+ const missingBarrels = await findMissingBarrels(files, cwd);
112
112
  return buildGroupedResult(statuses, missingBarrels, limit);
113
113
  }
114
114
  /**
@@ -135,6 +135,6 @@ export async function validateFiles(
135
135
  const statuses = await Promise.all(
136
136
  tsFiles.map((f) => classifyFile(eslint, f, cwd, config)),
137
137
  );
138
- const missingBarrels = findMissingBarrels(tsFiles, cwd);
138
+ const missingBarrels = await findMissingBarrels(tsFiles, cwd);
139
139
  return buildGroupedResult(statuses, missingBarrels, limit);
140
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsdoczoom",
3
- "version": "1.0.0",
3
+ "version": "1.2.1",
4
4
  "description": "CLI tool for extracting JSDoc summaries at configurable depths",
5
5
  "type": "module",
6
6
  "sideEffects": false,
package/types/barrel.d.ts CHANGED
@@ -32,7 +32,7 @@ export declare function isBarrel(filePath: string): boolean;
32
32
  export declare function getBarrelChildren(
33
33
  barrelPath: string,
34
34
  _cwd: string,
35
- ): string[];
35
+ ): Promise<string[]>;
36
36
  /** Minimum number of .ts/.tsx files in a directory to require a barrel. */
37
37
  export declare const BARREL_THRESHOLD = 3;
38
38
  /**
@@ -42,4 +42,4 @@ export declare const BARREL_THRESHOLD = 3;
42
42
  export declare function findMissingBarrels(
43
43
  filePaths: string[],
44
44
  cwd: string,
45
- ): string[];
45
+ ): Promise<string[]>;
@@ -11,7 +11,7 @@ import { type Ignore } from "ignore";
11
11
  * Walk from `cwd` up to the filesystem root, collecting .gitignore entries.
12
12
  * Returns an Ignore instance loaded with all discovered rules.
13
13
  */
14
- export declare function loadGitignore(cwd: string): Ignore;
14
+ export declare function loadGitignore(cwd: string): Promise<Ignore>;
15
15
  /**
16
16
  * Resolve a selector pattern to a list of .ts/.tsx file paths.
17
17
  *
@@ -29,4 +29,4 @@ export declare function discoverFiles(
29
29
  pattern: string,
30
30
  cwd: string,
31
31
  gitignore?: boolean,
32
- ): string[];
32
+ ): Promise<string[]>;
package/types/index.d.ts CHANGED
@@ -13,9 +13,14 @@ 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 {
20
25
  type CacheConfig,
21
26
  type CacheOperationMode,
@@ -24,4 +24,6 @@ export declare function extractFileJsdoc(
24
24
  * Reads the file, extracts the first file-level JSDoc block, and parses the first
25
25
  * @summary tag and free-text description.
26
26
  */
27
- export declare function parseFileSummaries(filePath: string): ParsedFileInfo;
27
+ export declare function parseFileSummaries(
28
+ filePath: string,
29
+ ): Promise<ParsedFileInfo>;
@@ -0,0 +1,44 @@
1
+ import type { CacheConfig, DrilldownResult, SelectorInfo } from "./types.js";
2
+ /**
3
+ * Search TypeScript files matching a selector for a regex query.
4
+ *
5
+ * Discovers files from the selector pattern, processes each through
6
+ * the 4-level matching hierarchy, and returns results at the shallowest
7
+ * matching depth. Files with parse errors are silently skipped.
8
+ *
9
+ * @param selector - Parsed selector with type and pattern
10
+ * @param query - Regex query string (case-insensitive)
11
+ * @param cwd - Working directory for file resolution
12
+ * @param gitignore - Whether to respect .gitignore rules (default true)
13
+ * @param limit - Maximum number of results to return (default 100)
14
+ * @param config - Cache configuration
15
+ * @throws {JsdocError} INVALID_SELECTOR for invalid regex
16
+ */
17
+ export declare function search(
18
+ selector: SelectorInfo,
19
+ query: string,
20
+ cwd: string,
21
+ gitignore?: boolean,
22
+ limit?: number,
23
+ config?: CacheConfig,
24
+ ): Promise<DrilldownResult>;
25
+ /**
26
+ * Search an explicit list of file paths for a regex query.
27
+ *
28
+ * Used for stdin input. Filters to .ts/.tsx files, excludes .d.ts.
29
+ * Processes each file through the 4-level matching hierarchy.
30
+ *
31
+ * @param filePaths - Array of absolute file paths
32
+ * @param query - Regex query string (case-insensitive)
33
+ * @param cwd - Working directory for relative path output
34
+ * @param limit - Maximum number of results to return (default 100)
35
+ * @param config - Cache configuration
36
+ * @throws {JsdocError} INVALID_SELECTOR for invalid regex
37
+ */
38
+ export declare function searchFiles(
39
+ filePaths: string[],
40
+ query: string,
41
+ cwd: string,
42
+ limit?: number,
43
+ config?: CacheConfig,
44
+ ): Promise<DrilldownResult>;