jsdoczoom 0.1.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 ADDED
@@ -0,0 +1,84 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { basename, dirname, resolve } from "node:path";
3
+ /**
4
+ * Barrel detection and child discovery for index.ts/index.tsx files.
5
+ * A barrel is strictly `index.ts` or `index.tsx`; other index variants
6
+ * (e.g. `index.test.ts`, `index.d.ts`) are excluded. Children include
7
+ * sibling .ts/.tsx files and child barrels in immediate subdirectories,
8
+ * with index.ts taking priority over index.tsx in the same subdirectory.
9
+ *
10
+ * @summary Detect barrel files and discover their children for drill-down gating
11
+ */
12
+ /**
13
+ * Check if a file path refers to a barrel file (index.ts or index.tsx).
14
+ *
15
+ * A barrel is a file named exactly `index.ts` or `index.tsx`.
16
+ * Files like `index.test.ts`, `index.d.ts`, `index.stories.tsx` are NOT barrels.
17
+ *
18
+ * @param filePath - Absolute or relative file path to check
19
+ * @returns true if the file is a barrel (index.ts or index.tsx)
20
+ */
21
+ export function isBarrel(filePath) {
22
+ const name = basename(filePath);
23
+ return name === "index.ts" || name === "index.tsx";
24
+ }
25
+ /**
26
+ * Discover the children of a barrel file.
27
+ *
28
+ * Children include:
29
+ * - Sibling .ts/.tsx files in the same directory (excluding the barrel itself, excluding .d.ts)
30
+ * - Child barrels (index.ts or index.tsx) in immediate subdirectories
31
+ * - index.ts takes priority over index.tsx in the same subdirectory
32
+ *
33
+ * @param barrelPath - Absolute path to the barrel file (index.ts or index.tsx)
34
+ * @param _cwd - Working directory (unused, kept for API consistency)
35
+ * @returns Sorted array of absolute paths to child files
36
+ */
37
+ export function getBarrelChildren(barrelPath, _cwd) {
38
+ const dir = dirname(barrelPath);
39
+ const barrelName = basename(barrelPath);
40
+ let entries;
41
+ try {
42
+ entries = readdirSync(dir, { withFileTypes: true })
43
+ .map((e) => ({ name: e.name, isDirectory: e.isDirectory() }))
44
+ .flatMap((entry) => {
45
+ if (entry.isDirectory) {
46
+ // Check for child barrel in this subdirectory
47
+ const childBarrel = findChildBarrel(resolve(dir, entry.name));
48
+ return childBarrel ? [childBarrel] : [];
49
+ }
50
+ // Sibling file: must be .ts/.tsx, not .d.ts, not the barrel itself
51
+ if (isTsFile(entry.name) && entry.name !== barrelName) {
52
+ return [resolve(dir, entry.name)];
53
+ }
54
+ return [];
55
+ });
56
+ }
57
+ catch {
58
+ // Directory unreadable (permissions, deleted, etc.) — no children discoverable
59
+ return [];
60
+ }
61
+ return entries.sort();
62
+ }
63
+ /**
64
+ * Check if a filename is a .ts or .tsx file (excluding .d.ts).
65
+ */
66
+ function isTsFile(name) {
67
+ return ((name.endsWith(".ts") || name.endsWith(".tsx")) && !name.endsWith(".d.ts"));
68
+ }
69
+ /**
70
+ * Find the barrel file in a subdirectory.
71
+ * index.ts takes priority over index.tsx.
72
+ * Returns the absolute path to the barrel, or null if none found.
73
+ */
74
+ function findChildBarrel(subdirPath) {
75
+ const tsPath = resolve(subdirPath, "index.ts");
76
+ if (existsSync(tsPath)) {
77
+ return tsPath;
78
+ }
79
+ const tsxPath = resolve(subdirPath, "index.tsx");
80
+ if (existsSync(tsxPath)) {
81
+ return tsxPath;
82
+ }
83
+ return null;
84
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { drilldown, drilldownFiles } from "./drilldown.js";
4
+ import { JsdocError } from "./errors.js";
5
+ import { lint } from "./lint.js";
6
+ import { parseSelector } from "./selector.js";
7
+ import { SKILL_TEXT } from "./skill-text.js";
8
+ import { VALIDATION_STATUS_PRIORITY } from "./types.js";
9
+ import { validate } from "./validate.js";
10
+ /**
11
+ * Parses argv flags (--help, --validate, --lint, --skill, --pretty, --limit,
12
+ * --no-gitignore), dispatches to drilldown, validation, or lint mode, and
13
+ * handles stdin piping. Errors are written to stderr as JSON; validation and
14
+ * lint failures use exit code 2 while other errors use exit code 1.
15
+ *
16
+ * @summary CLI entry point -- argument parsing, mode dispatch, and exit code handling
17
+ */
18
+ const HELP_TEXT = `Usage: jsdoczoom [options] [selector]
19
+
20
+ Progressively explore TypeScript codebase documentation.
21
+ Each file has four detail levels (1-indexed): @1 summary, @2 description,
22
+ @3 type declarations, @4 full source. Higher depth = more detail.
23
+
24
+ Options:
25
+ -h, --help Show this help text
26
+ -v, --validate Run validation mode
27
+ -l, --lint Run lint mode (comprehensive JSDoc quality)
28
+ -s, --skill Print JSDoc writing guidelines
29
+ --pretty Format JSON output with 2-space indent
30
+ --limit N Max results shown (default 100)
31
+ --no-gitignore Include files ignored by .gitignore
32
+
33
+ Selector:
34
+ A glob pattern or file path, optionally with @depth suffix (1-4).
35
+ Examples:
36
+ jsdoczoom src/**/*.ts # All .ts files at depth 1 (summary)
37
+ jsdoczoom src/index.ts@2 # Single file at depth 2 (description)
38
+ jsdoczoom **/*.ts@3 # All .ts files at depth 3 (type decls)
39
+
40
+ Stdin:
41
+ Pipe file paths one per line:
42
+ find . -name "*.ts" | jsdoczoom
43
+ find . -name "*.ts" | jsdoczoom @2
44
+ find . -name "*.ts" | jsdoczoom -v
45
+
46
+ Output:
47
+ JSON items. Items with "next_id" have more detail; use that value as the next
48
+ selector. Items with "id" (no next_id) are at terminal depth.
49
+
50
+ Barrel gating (glob mode):
51
+ index.ts files with a @summary gate sibling files at depths 1-2.
52
+ At depth 3 the barrel disappears and its children appear at depth 1.
53
+
54
+ Workflow:
55
+ $ jsdoczoom src/**/*.ts
56
+ { "items": [{ "next_id": "src/utils@2", "text": "..." }, ...] }
57
+
58
+ $ jsdoczoom src/utils@2 # use next_id from above
59
+ { "items": [{ "next_id": "src/utils@3", "text": "..." }] }
60
+
61
+ $ jsdoczoom src/utils@3 # use next_id again
62
+ { "items": [{ "id": "src/utils@3", "text": "..." }] }
63
+ # "id" instead of "next_id" means terminal depth — stop here
64
+ `;
65
+ /**
66
+ * Parse CLI arguments into flags and positional args.
67
+ */
68
+ function parseArgs(args) {
69
+ let help = false;
70
+ let validateMode = false;
71
+ let lintMode = false;
72
+ let skillMode = false;
73
+ let pretty = false;
74
+ let limit = 100;
75
+ let gitignore = true;
76
+ let selectorArg;
77
+ for (let i = 0; i < args.length; i++) {
78
+ const arg = args[i];
79
+ if (arg === "-h" || arg === "--help") {
80
+ help = true;
81
+ }
82
+ else if (arg === "-v" || arg === "--validate") {
83
+ validateMode = true;
84
+ }
85
+ else if (arg === "-l" || arg === "--lint") {
86
+ lintMode = true;
87
+ }
88
+ else if (arg === "-s" || arg === "--skill") {
89
+ skillMode = true;
90
+ }
91
+ else if (arg === "--pretty") {
92
+ pretty = true;
93
+ }
94
+ else if (arg === "--limit") {
95
+ const next = args[++i];
96
+ limit = Number(next);
97
+ }
98
+ else if (arg === "--no-gitignore") {
99
+ gitignore = false;
100
+ }
101
+ else if (arg.startsWith("-")) {
102
+ throw new JsdocError("INVALID_SELECTOR", `Unrecognized option: ${arg}`);
103
+ }
104
+ else if (selectorArg === undefined) {
105
+ selectorArg = arg;
106
+ }
107
+ }
108
+ return {
109
+ help,
110
+ validateMode,
111
+ lintMode,
112
+ skillMode,
113
+ pretty,
114
+ limit,
115
+ gitignore,
116
+ selectorArg,
117
+ };
118
+ }
119
+ /**
120
+ * Resolve stdin file paths to absolute paths.
121
+ */
122
+ function parseStdinPaths(stdin, cwd) {
123
+ return stdin
124
+ .split("\n")
125
+ .map((line) => line.trim())
126
+ .filter((line) => line.length > 0)
127
+ .map((line) => resolve(cwd, line));
128
+ }
129
+ /**
130
+ * Extract depth from a selector argument string.
131
+ * Returns undefined if no @depth suffix is present.
132
+ */
133
+ function extractDepthFromArg(selectorArg) {
134
+ const parsed = parseSelector(selectorArg);
135
+ return parsed.depth;
136
+ }
137
+ /**
138
+ * Process stdin mode: file paths piped in.
139
+ */
140
+ async function processStdin(stdin, selectorArg, validateMode, lintMode, pretty, limit, cwd) {
141
+ const stdinPaths = parseStdinPaths(stdin, cwd);
142
+ const depth = selectorArg !== undefined ? extractDepthFromArg(selectorArg) : undefined;
143
+ if (lintMode) {
144
+ const { lintFiles } = await import("./lint.js");
145
+ const result = await lintFiles(stdinPaths, cwd, limit);
146
+ writeLintResult(result, pretty);
147
+ }
148
+ else if (validateMode) {
149
+ const { validateFiles } = await import("./validate.js");
150
+ const result = await validateFiles(stdinPaths, cwd, limit);
151
+ writeValidationResult(result, pretty);
152
+ }
153
+ else {
154
+ const result = drilldownFiles(stdinPaths, depth, cwd, limit);
155
+ writeResult(result, pretty);
156
+ }
157
+ }
158
+ /**
159
+ * Process selector mode: glob or path argument.
160
+ */
161
+ async function processSelector(selectorArg, validateMode, lintMode, pretty, limit, gitignore, cwd) {
162
+ const selector = selectorArg
163
+ ? parseSelector(selectorArg)
164
+ : { type: "glob", pattern: "**/*.{ts,tsx}", depth: undefined };
165
+ if (lintMode) {
166
+ const result = await lint(selector, cwd, limit, gitignore);
167
+ writeLintResult(result, pretty);
168
+ }
169
+ else if (validateMode) {
170
+ const result = await validate(selector, cwd, limit, gitignore);
171
+ writeValidationResult(result, pretty);
172
+ }
173
+ else {
174
+ const result = drilldown(selector, cwd, gitignore, limit);
175
+ writeResult(result, pretty);
176
+ }
177
+ }
178
+ /**
179
+ * Write an error to stderr as JSON and set exit code.
180
+ */
181
+ function writeError(error) {
182
+ if (error instanceof JsdocError) {
183
+ process.stderr.write(`${JSON.stringify(error.toJSON())}\n`);
184
+ process.exitCode = 1;
185
+ return;
186
+ }
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ process.stderr.write(`${JSON.stringify({ error: { code: "INTERNAL_ERROR", message } })}\n`);
189
+ process.exitCode = 1;
190
+ }
191
+ /**
192
+ * Main CLI entry point. Exported for testability.
193
+ */
194
+ export async function main(args, stdin) {
195
+ try {
196
+ const { help, validateMode, lintMode, skillMode, pretty, limit, gitignore, selectorArg, } = parseArgs(args);
197
+ if (help) {
198
+ process.stdout.write(HELP_TEXT);
199
+ return;
200
+ }
201
+ if (skillMode) {
202
+ process.stdout.write(SKILL_TEXT);
203
+ return;
204
+ }
205
+ if (validateMode && lintMode) {
206
+ writeError(new JsdocError("INVALID_SELECTOR", "Cannot use -v and -l together"));
207
+ return;
208
+ }
209
+ const cwd = process.cwd();
210
+ if (stdin !== undefined) {
211
+ await processStdin(stdin, selectorArg, validateMode, lintMode, pretty, limit, cwd);
212
+ }
213
+ else {
214
+ await processSelector(selectorArg, validateMode, lintMode, pretty, limit, gitignore, cwd);
215
+ }
216
+ }
217
+ catch (error) {
218
+ writeError(error);
219
+ }
220
+ }
221
+ /**
222
+ * Write a result to stdout as JSON.
223
+ */
224
+ function writeResult(result, pretty) {
225
+ const json = pretty
226
+ ? JSON.stringify(result, null, 2)
227
+ : JSON.stringify(result);
228
+ process.stdout.write(`${json}\n`);
229
+ }
230
+ /**
231
+ * Count invalid files across all validation groups.
232
+ */
233
+ function countInvalid(result) {
234
+ return VALIDATION_STATUS_PRIORITY.reduce((sum, status) => sum + (result[status]?.files.length ?? 0), 0);
235
+ }
236
+ /**
237
+ * Write validation result to stdout and set exit code.
238
+ * Adds success/message fields to the output.
239
+ */
240
+ function writeValidationResult(result, pretty) {
241
+ const invalidCount = countInvalid(result);
242
+ if (invalidCount === 0) {
243
+ writeResult({ success: true, message: "All files passed validation" }, pretty);
244
+ }
245
+ else {
246
+ writeResult({ ...result, success: false }, pretty);
247
+ process.stderr.write(`${JSON.stringify(new JsdocError("VALIDATION_FAILED", `${invalidCount} file(s) failed validation`).toJSON())}\n`);
248
+ process.exitCode = 2;
249
+ }
250
+ }
251
+ /**
252
+ * Write lint result to stdout and set exit code 2 if issues found.
253
+ */
254
+ function writeLintResult(result, pretty) {
255
+ writeResult(result, pretty);
256
+ if (result.summary.filesWithIssues > 0) {
257
+ process.exitCode = 2;
258
+ }
259
+ }
260
+ async function readStdin() {
261
+ const chunks = [];
262
+ for await (const chunk of process.stdin) {
263
+ chunks.push(chunk);
264
+ }
265
+ return Buffer.concat(chunks).toString("utf-8");
266
+ }
267
+ // Auto-invoke when run as CLI
268
+ function isDirectRun() {
269
+ if (!process.argv[1])
270
+ return false;
271
+ try {
272
+ const scriptPath = process.argv[1].replace(/\\/g, "/");
273
+ return (import.meta.url.endsWith(scriptPath) ||
274
+ import.meta.url.endsWith("/cli.js"));
275
+ }
276
+ catch {
277
+ // Cannot determine script path — safe to skip auto-invoke
278
+ return false;
279
+ }
280
+ }
281
+ if (isDirectRun()) {
282
+ // Only read stdin when it's explicitly piped (isTTY === false, not just undefined)
283
+ const stdinText = process.stdin.isTTY === false ? await readStdin() : undefined;
284
+ main(process.argv.slice(2), stdinText).catch(() => {
285
+ // Error already handled in main
286
+ });
287
+ }
@@ -0,0 +1,286 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, relative } from "node:path";
3
+ import { getBarrelChildren, isBarrel } from "./barrel.js";
4
+ import { JsdocError } from "./errors.js";
5
+ import { discoverFiles } from "./file-discovery.js";
6
+ import { parseFileSummaries } from "./jsdoc-parser.js";
7
+ import { generateTypeDeclarations } from "./type-declarations.js";
8
+ /** Terminal level (1-indexed): 1=summary, 2=description, 3=type declarations, 4=full file. */
9
+ const TERMINAL_LEVEL = 4;
10
+ /**
11
+ * Build the fixed drill-down level array for a file.
12
+ *
13
+ * - Level 1 (index 0): @summary text (null if absent)
14
+ * - Level 2 (index 1): description text (null if absent)
15
+ * - Level 3 (index 2): type declarations (always present)
16
+ * - Level 4 (index 3): full file content (always present, terminal)
17
+ *
18
+ * The array always has 4 entries. Null entries are skipped during processing.
19
+ */
20
+ function buildLevels(info) {
21
+ const { summary, description } = info;
22
+ return [
23
+ summary !== null ? { text: () => summary } : null,
24
+ description !== null ? { text: () => description } : null,
25
+ { text: () => generateTypeDeclarations(info.path) },
26
+ { text: () => readFileSync(info.path, "utf-8") },
27
+ ];
28
+ }
29
+ /**
30
+ * Compute the display id path for a file. Barrel files (index.ts/index.tsx)
31
+ * use their parent directory path so they represent the directory.
32
+ */
33
+ function displayPath(filePath, cwd) {
34
+ const rel = relative(cwd, filePath);
35
+ if (isBarrel(filePath)) {
36
+ const dir = relative(cwd, dirname(filePath));
37
+ return dir || ".";
38
+ }
39
+ return rel;
40
+ }
41
+ /**
42
+ * Extract the sort key from an output entry (either next_id or id).
43
+ */
44
+ function sortKey(entry) {
45
+ if ("next_id" in entry)
46
+ return entry.next_id;
47
+ return entry.id;
48
+ }
49
+ /**
50
+ * Process a single file at a given depth through the drill-down levels.
51
+ *
52
+ * Levels are 1-indexed: 1=summary, 2=description, 3=type declarations, 4=full file.
53
+ * If the requested depth level is null (empty), advance to the next non-null level.
54
+ * Output contains next_id when more detail is available, or id at terminal level.
55
+ */
56
+ function processFile(info, depth, cwd) {
57
+ const idPath = displayPath(info.path, cwd);
58
+ const levels = buildLevels(info);
59
+ // Clamp depth to [1, TERMINAL_LEVEL], advance past null levels
60
+ let effectiveDepth = Math.max(1, Math.min(depth, TERMINAL_LEVEL));
61
+ while (effectiveDepth < TERMINAL_LEVEL &&
62
+ levels[effectiveDepth - 1] === null) {
63
+ effectiveDepth++;
64
+ }
65
+ const level = levels[effectiveDepth - 1];
66
+ if (effectiveDepth < TERMINAL_LEVEL) {
67
+ return {
68
+ next_id: `${idPath}@${effectiveDepth + 1}`,
69
+ text: level.text(),
70
+ };
71
+ }
72
+ return {
73
+ id: `${idPath}@${effectiveDepth}`,
74
+ text: level.text(),
75
+ };
76
+ }
77
+ /**
78
+ * Create an OutputErrorItem for a PARSE_ERROR.
79
+ */
80
+ function makeParseErrorItem(filePath, error, cwd) {
81
+ return {
82
+ id: displayPath(filePath, cwd),
83
+ error: { code: error.code, message: error.message },
84
+ };
85
+ }
86
+ /**
87
+ * Check if an error is a PARSE_ERROR JsdocError.
88
+ */
89
+ function isParseError(error) {
90
+ return error instanceof JsdocError && error.code === "PARSE_ERROR";
91
+ }
92
+ /**
93
+ * Process a file safely, returning an OutputErrorItem on PARSE_ERROR.
94
+ * Rethrows non-PARSE_ERROR exceptions.
95
+ */
96
+ function processFileSafe(filePath, depth, cwd) {
97
+ try {
98
+ const info = parseFileSummaries(filePath);
99
+ return processFile(info, depth, cwd);
100
+ }
101
+ catch (error) {
102
+ if (isParseError(error))
103
+ return makeParseErrorItem(filePath, error, cwd);
104
+ throw error;
105
+ }
106
+ }
107
+ /**
108
+ * Gather barrel info and error entries from a list of barrel file paths.
109
+ * Returns successfully parsed barrel infos and error entries for unparseable barrels.
110
+ */
111
+ function gatherBarrelInfos(barrelPaths, cwd) {
112
+ const infos = [];
113
+ const errors = [];
114
+ for (const barrelPath of barrelPaths) {
115
+ try {
116
+ const info = parseFileSummaries(barrelPath);
117
+ const children = getBarrelChildren(barrelPath, cwd);
118
+ infos.push({
119
+ path: barrelPath,
120
+ hasSummary: info.summary !== null,
121
+ hasDescription: info.description !== null,
122
+ children,
123
+ });
124
+ }
125
+ catch (error) {
126
+ if (isParseError(error)) {
127
+ errors.push(makeParseErrorItem(barrelPath, error, cwd));
128
+ continue;
129
+ }
130
+ throw error;
131
+ }
132
+ }
133
+ return { infos, errors };
134
+ }
135
+ /**
136
+ * Build the set of files gated by barrels that have summaries.
137
+ */
138
+ function buildGatedFileSet(barrelInfos) {
139
+ const gated = new Set();
140
+ for (const barrel of barrelInfos) {
141
+ if (barrel.hasSummary) {
142
+ for (const child of barrel.children) {
143
+ gated.add(child);
144
+ }
145
+ }
146
+ }
147
+ return gated;
148
+ }
149
+ /**
150
+ * Process a single barrel at the given depth (1-indexed).
151
+ * Barrels without summaries appear as regular files (no gating).
152
+ * Barrels with summaries gate children for two depths (L1 summary, L2 description),
153
+ * then transition at depth 3 where the barrel disappears and children appear.
154
+ */
155
+ function processBarrelAtDepth(barrel, depth, cwd) {
156
+ if (!barrel.hasSummary)
157
+ return [processFileSafe(barrel.path, depth, cwd)];
158
+ // Null-skip: if depth 2 but no description, advance to transition depth
159
+ let effectiveDepth = depth;
160
+ if (effectiveDepth === 2 && !barrel.hasDescription) {
161
+ effectiveDepth = 3;
162
+ }
163
+ if (effectiveDepth < 3) {
164
+ // Show barrel's own L1 (summary) or L2 (description)
165
+ return [processFileSafe(barrel.path, effectiveDepth, cwd)];
166
+ }
167
+ // Barrel transitions: barrel disappears, children appear
168
+ const childDepth = effectiveDepth - 2;
169
+ return collectSafeResults(barrel.children, childDepth, cwd);
170
+ }
171
+ /**
172
+ * Process a list of files through processFileSafe.
173
+ */
174
+ function collectSafeResults(files, depth, cwd) {
175
+ return files.map((filePath) => processFileSafe(filePath, depth, cwd));
176
+ }
177
+ /**
178
+ * Process files discovered via glob with barrel gating.
179
+ *
180
+ * When a glob discovers an index.ts barrel:
181
+ * 1. If the barrel has a summary and depth < 3: show barrel summary/description (gates children)
182
+ * 2. If depth >= 3: barrel transitions -- barrel disappears and children appear at depth - 2
183
+ * 3. If barrel has no summary: not a tree node, children appear as leaves
184
+ *
185
+ * A barrel that is itself gated by a parent barrel is not processed independently.
186
+ * Non-barrel files that are not gated by any barrel are processed normally.
187
+ */
188
+ function processGlobWithBarrels(files, depth, cwd) {
189
+ const barrelPaths = [];
190
+ const nonBarrelPaths = [];
191
+ for (const filePath of files) {
192
+ if (isBarrel(filePath)) {
193
+ barrelPaths.push(filePath);
194
+ }
195
+ else {
196
+ nonBarrelPaths.push(filePath);
197
+ }
198
+ }
199
+ if (barrelPaths.length === 0) {
200
+ return collectSafeResults(nonBarrelPaths, depth, cwd);
201
+ }
202
+ const { infos: barrelInfos, errors: barrelErrors } = gatherBarrelInfos(barrelPaths, cwd);
203
+ const gatedFiles = buildGatedFileSet(barrelInfos);
204
+ const results = [...barrelErrors];
205
+ for (const barrel of barrelInfos) {
206
+ if (gatedFiles.has(barrel.path))
207
+ continue;
208
+ results.push(...processBarrelAtDepth(barrel, depth, cwd));
209
+ }
210
+ const ungatedNonBarrels = nonBarrelPaths.filter((f) => !gatedFiles.has(f));
211
+ results.push(...collectSafeResults(ungatedNonBarrels, depth, cwd));
212
+ return results;
213
+ }
214
+ /**
215
+ * Main entry point for normal-mode processing.
216
+ *
217
+ * Resolves files from a selector, processes each through the drill-down model,
218
+ * and returns an array of output entries. Barrel gating is applied in glob mode.
219
+ * Depth is 1-indexed (default 1).
220
+ *
221
+ * @param selector - Parsed selector with type, pattern, and optional depth
222
+ * @param cwd - Working directory for file resolution
223
+ * @returns Array of output entries sorted alphabetically by path
224
+ * @throws {JsdocError} FILE_NOT_FOUND for missing path selector target
225
+ * @throws {JsdocError} NO_FILES_MATCHED for empty glob results
226
+ * @throws {JsdocError} PARSE_ERROR for path selector targeting file with syntax errors
227
+ */
228
+ export function drilldown(selector, cwd, gitignore = true, limit = 100) {
229
+ const depth = selector.depth ?? 1;
230
+ if (selector.type === "path") {
231
+ const files = discoverFiles(selector.pattern, cwd, gitignore);
232
+ if (files.length > 1) {
233
+ // Directory expanded to multiple files — route through glob pipeline
234
+ const results = processGlobWithBarrels(files, depth, cwd);
235
+ const sorted = results.sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
236
+ const total = sorted.length;
237
+ const truncated = total > limit;
238
+ return {
239
+ items: sorted.slice(0, limit),
240
+ truncated,
241
+ };
242
+ }
243
+ // Single file path — errors are fatal, no barrel gating
244
+ const filePath = files[0];
245
+ const info = parseFileSummaries(filePath);
246
+ const items = [processFile(info, depth, cwd)];
247
+ return { items, truncated: false };
248
+ }
249
+ // Glob selector — apply barrel gating
250
+ const files = discoverFiles(selector.pattern, cwd, gitignore);
251
+ if (files.length === 0) {
252
+ throw new JsdocError("NO_FILES_MATCHED", `No files matched: ${selector.pattern}`);
253
+ }
254
+ const results = processGlobWithBarrels(files, depth, cwd);
255
+ // Sort alphabetically by key
256
+ const sorted = results.sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
257
+ const total = sorted.length;
258
+ const truncated = total > limit;
259
+ return {
260
+ items: sorted.slice(0, limit),
261
+ truncated,
262
+ };
263
+ }
264
+ /**
265
+ * Process an explicit list of file paths at a given depth.
266
+ *
267
+ * Used for stdin input. Always treats paths as leaf files (no barrel gating).
268
+ * Filters to .ts/.tsx only. Depth is 1-indexed (default 1).
269
+ *
270
+ * @param filePaths - Array of absolute file paths
271
+ * @param depth - Drill-down depth, 1-indexed (defaults to 1 if undefined)
272
+ * @param cwd - Working directory for relative path output
273
+ * @returns Array of output entries sorted alphabetically by path
274
+ */
275
+ export function drilldownFiles(filePaths, depth, cwd, limit = 100) {
276
+ const d = depth ?? 1;
277
+ const tsFiles = filePaths.filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
278
+ const results = collectSafeResults(tsFiles, d, cwd);
279
+ const sorted = results.sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
280
+ const total = sorted.length;
281
+ const truncated = total > limit;
282
+ return {
283
+ items: sorted.slice(0, limit),
284
+ truncated,
285
+ };
286
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * JsdocError extends Error with an error code and serializes to the
3
+ * `{ error: { code, message } }` JSON shape. This is the single error type
4
+ * used across the entire tool for both programmatic and CLI error output.
5
+ *
6
+ * @summary Structured error type with JSON serialization for CLI and programmatic use
7
+ */
8
+ /** Custom error class that serializes to the documented JSON error contract */
9
+ export class JsdocError extends Error {
10
+ code;
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.code = code;
14
+ this.name = "JsdocError";
15
+ }
16
+ toJSON() {
17
+ return {
18
+ error: {
19
+ code: this.code,
20
+ message: this.message,
21
+ },
22
+ };
23
+ }
24
+ }