jsdoczoom 0.1.0 → 0.3.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/drilldown.js CHANGED
@@ -5,6 +5,7 @@ import { JsdocError } from "./errors.js";
5
5
  import { discoverFiles } from "./file-discovery.js";
6
6
  import { parseFileSummaries } from "./jsdoc-parser.js";
7
7
  import { generateTypeDeclarations } from "./type-declarations.js";
8
+
8
9
  /** Terminal level (1-indexed): 1=summary, 2=description, 3=type declarations, 4=full file. */
9
10
  const TERMINAL_LEVEL = 4;
10
11
  /**
@@ -18,33 +19,32 @@ const TERMINAL_LEVEL = 4;
18
19
  * The array always has 4 entries. Null entries are skipped during processing.
19
20
  */
20
21
  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
- ];
22
+ const { summary, description } = info;
23
+ return [
24
+ summary !== null ? { text: () => summary } : null,
25
+ description !== null ? { text: () => description } : null,
26
+ { text: () => generateTypeDeclarations(info.path) },
27
+ { text: () => readFileSync(info.path, "utf-8") },
28
+ ];
28
29
  }
29
30
  /**
30
31
  * Compute the display id path for a file. Barrel files (index.ts/index.tsx)
31
32
  * use their parent directory path so they represent the directory.
32
33
  */
33
34
  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;
35
+ const rel = relative(cwd, filePath);
36
+ if (isBarrel(filePath)) {
37
+ const dir = relative(cwd, dirname(filePath));
38
+ return dir || ".";
39
+ }
40
+ return rel;
40
41
  }
41
42
  /**
42
43
  * Extract the sort key from an output entry (either next_id or id).
43
44
  */
44
45
  function sortKey(entry) {
45
- if ("next_id" in entry)
46
- return entry.next_id;
47
- return entry.id;
46
+ if ("next_id" in entry) return entry.next_id;
47
+ return entry.id;
48
48
  }
49
49
  /**
50
50
  * Process a single file at a given depth through the drill-down levels.
@@ -54,97 +54,96 @@ function sortKey(entry) {
54
54
  * Output contains next_id when more detail is available, or id at terminal level.
55
55
  */
56
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
- };
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 (
62
+ effectiveDepth < TERMINAL_LEVEL &&
63
+ levels[effectiveDepth - 1] === null
64
+ ) {
65
+ effectiveDepth++;
66
+ }
67
+ const level = levels[effectiveDepth - 1];
68
+ if (effectiveDepth < TERMINAL_LEVEL) {
69
+ return {
70
+ next_id: `${idPath}@${effectiveDepth + 1}`,
71
+ text: level.text(),
72
+ };
73
+ }
74
+ return {
75
+ id: `${idPath}@${effectiveDepth}`,
76
+ text: level.text(),
77
+ };
76
78
  }
77
79
  /**
78
80
  * Create an OutputErrorItem for a PARSE_ERROR.
79
81
  */
80
82
  function makeParseErrorItem(filePath, error, cwd) {
81
- return {
82
- id: displayPath(filePath, cwd),
83
- error: { code: error.code, message: error.message },
84
- };
83
+ return {
84
+ id: displayPath(filePath, cwd),
85
+ error: { code: error.code, message: error.message },
86
+ };
85
87
  }
86
88
  /**
87
89
  * Check if an error is a PARSE_ERROR JsdocError.
88
90
  */
89
91
  function isParseError(error) {
90
- return error instanceof JsdocError && error.code === "PARSE_ERROR";
92
+ return error instanceof JsdocError && error.code === "PARSE_ERROR";
91
93
  }
92
94
  /**
93
95
  * Process a file safely, returning an OutputErrorItem on PARSE_ERROR.
94
96
  * Rethrows non-PARSE_ERROR exceptions.
95
97
  */
96
98
  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
- }
99
+ try {
100
+ const info = parseFileSummaries(filePath);
101
+ return processFile(info, depth, cwd);
102
+ } catch (error) {
103
+ if (isParseError(error)) return makeParseErrorItem(filePath, error, cwd);
104
+ throw error;
105
+ }
106
106
  }
107
107
  /**
108
108
  * Gather barrel info and error entries from a list of barrel file paths.
109
109
  * Returns successfully parsed barrel infos and error entries for unparseable barrels.
110
110
  */
111
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 };
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
+ } catch (error) {
125
+ if (isParseError(error)) {
126
+ errors.push(makeParseErrorItem(barrelPath, error, cwd));
127
+ continue;
128
+ }
129
+ throw error;
130
+ }
131
+ }
132
+ return { infos, errors };
134
133
  }
135
134
  /**
136
135
  * Build the set of files gated by barrels that have summaries.
137
136
  */
138
137
  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;
138
+ const gated = new Set();
139
+ for (const barrel of barrelInfos) {
140
+ if (barrel.hasSummary) {
141
+ for (const child of barrel.children) {
142
+ gated.add(child);
143
+ }
144
+ }
145
+ }
146
+ return gated;
148
147
  }
149
148
  /**
150
149
  * Process a single barrel at the given depth (1-indexed).
@@ -153,26 +152,29 @@ function buildGatedFileSet(barrelInfos) {
153
152
  * then transition at depth 3 where the barrel disappears and children appear.
154
153
  */
155
154
  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);
155
+ if (!barrel.hasSummary) return [processFileSafe(barrel.path, depth, cwd)];
156
+ // Null-skip: if depth 2 but no description, advance to transition depth
157
+ let effectiveDepth = depth;
158
+ if (effectiveDepth === 2 && !barrel.hasDescription) {
159
+ effectiveDepth = 3;
160
+ }
161
+ if (effectiveDepth < 3) {
162
+ // Show barrel's own L1 (summary) or L2 (description)
163
+ const entry = processFileSafe(barrel.path, effectiveDepth, cwd);
164
+ if ("next_id" in entry) {
165
+ entry.children = barrel.children.map((c) => relative(cwd, c));
166
+ }
167
+ return [entry];
168
+ }
169
+ // Barrel transitions: barrel disappears, children appear
170
+ const childDepth = effectiveDepth - 2;
171
+ return collectSafeResults(barrel.children, childDepth, cwd);
170
172
  }
171
173
  /**
172
174
  * Process a list of files through processFileSafe.
173
175
  */
174
176
  function collectSafeResults(files, depth, cwd) {
175
- return files.map((filePath) => processFileSafe(filePath, depth, cwd));
177
+ return files.map((filePath) => processFileSafe(filePath, depth, cwd));
176
178
  }
177
179
  /**
178
180
  * Process files discovered via glob with barrel gating.
@@ -186,30 +188,31 @@ function collectSafeResults(files, depth, cwd) {
186
188
  * Non-barrel files that are not gated by any barrel are processed normally.
187
189
  */
188
190
  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;
191
+ const barrelPaths = [];
192
+ const nonBarrelPaths = [];
193
+ for (const filePath of files) {
194
+ if (isBarrel(filePath)) {
195
+ barrelPaths.push(filePath);
196
+ } else {
197
+ nonBarrelPaths.push(filePath);
198
+ }
199
+ }
200
+ if (barrelPaths.length === 0) {
201
+ return collectSafeResults(nonBarrelPaths, depth, cwd);
202
+ }
203
+ const { infos: barrelInfos, errors: barrelErrors } = gatherBarrelInfos(
204
+ barrelPaths,
205
+ cwd,
206
+ );
207
+ const gatedFiles = buildGatedFileSet(barrelInfos);
208
+ const results = [...barrelErrors];
209
+ for (const barrel of barrelInfos) {
210
+ if (gatedFiles.has(barrel.path)) continue;
211
+ results.push(...processBarrelAtDepth(barrel, depth, cwd));
212
+ }
213
+ const ungatedNonBarrels = nonBarrelPaths.filter((f) => !gatedFiles.has(f));
214
+ results.push(...collectSafeResults(ungatedNonBarrels, depth, cwd));
215
+ return results;
213
216
  }
214
217
  /**
215
218
  * Main entry point for normal-mode processing.
@@ -226,40 +229,45 @@ function processGlobWithBarrels(files, depth, cwd) {
226
229
  * @throws {JsdocError} PARSE_ERROR for path selector targeting file with syntax errors
227
230
  */
228
231
  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
- };
232
+ const depth = selector.depth ?? 1;
233
+ if (selector.type === "path") {
234
+ const files = discoverFiles(selector.pattern, cwd, gitignore);
235
+ if (files.length > 1) {
236
+ // Directory expanded to multiple files — route through glob pipeline
237
+ const results = processGlobWithBarrels(files, depth, cwd);
238
+ const sorted = results.sort((a, b) =>
239
+ sortKey(a).localeCompare(sortKey(b)),
240
+ );
241
+ const total = sorted.length;
242
+ const truncated = total > limit;
243
+ return {
244
+ items: sorted.slice(0, limit),
245
+ truncated,
246
+ };
247
+ }
248
+ // Single file path — errors are fatal, no barrel gating
249
+ const filePath = files[0];
250
+ const info = parseFileSummaries(filePath);
251
+ const items = [processFile(info, depth, cwd)];
252
+ return { items, truncated: false };
253
+ }
254
+ // Glob selector apply barrel gating
255
+ const files = discoverFiles(selector.pattern, cwd, gitignore);
256
+ if (files.length === 0) {
257
+ throw new JsdocError(
258
+ "NO_FILES_MATCHED",
259
+ `No files matched: ${selector.pattern}`,
260
+ );
261
+ }
262
+ const results = processGlobWithBarrels(files, depth, cwd);
263
+ // Sort alphabetically by key
264
+ const sorted = results.sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
265
+ const total = sorted.length;
266
+ const truncated = total > limit;
267
+ return {
268
+ items: sorted.slice(0, limit),
269
+ truncated,
270
+ };
263
271
  }
264
272
  /**
265
273
  * Process an explicit list of file paths at a given depth.
@@ -273,14 +281,16 @@ export function drilldown(selector, cwd, gitignore = true, limit = 100) {
273
281
  * @returns Array of output entries sorted alphabetically by path
274
282
  */
275
283
  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
- };
284
+ const d = depth ?? 1;
285
+ const tsFiles = filePaths.filter(
286
+ (f) => f.endsWith(".ts") || f.endsWith(".tsx"),
287
+ );
288
+ const results = collectSafeResults(tsFiles, d, cwd);
289
+ const sorted = results.sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
290
+ const total = sorted.length;
291
+ const truncated = total > limit;
292
+ return {
293
+ items: sorted.slice(0, limit),
294
+ truncated,
295
+ };
286
296
  }
package/dist/errors.js CHANGED
@@ -7,18 +7,18 @@
7
7
  */
8
8
  /** Custom error class that serializes to the documented JSON error contract */
9
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
- }
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
24
  }