jsdoczoom 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/barrel.js CHANGED
@@ -19,8 +19,8 @@ import { basename, dirname, resolve } from "node:path";
19
19
  * @returns true if the file is a barrel (index.ts or index.tsx)
20
20
  */
21
21
  export function isBarrel(filePath) {
22
- const name = basename(filePath);
23
- return name === "index.ts" || name === "index.tsx";
22
+ const name = basename(filePath);
23
+ return name === "index.ts" || name === "index.tsx";
24
24
  }
25
25
  /**
26
26
  * Discover the children of a barrel file.
@@ -35,36 +35,37 @@ export function isBarrel(filePath) {
35
35
  * @returns Sorted array of absolute paths to child files
36
36
  */
37
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();
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
+ } catch {
57
+ // Directory unreadable (permissions, deleted, etc.) — no children discoverable
58
+ return [];
59
+ }
60
+ return entries.sort();
62
61
  }
63
62
  /**
64
63
  * Check if a filename is a .ts or .tsx file (excluding .d.ts).
65
64
  */
66
65
  function isTsFile(name) {
67
- return ((name.endsWith(".ts") || name.endsWith(".tsx")) && !name.endsWith(".d.ts"));
66
+ return (
67
+ (name.endsWith(".ts") || name.endsWith(".tsx")) && !name.endsWith(".d.ts")
68
+ );
68
69
  }
69
70
  /**
70
71
  * Find the barrel file in a subdirectory.
@@ -72,13 +73,13 @@ function isTsFile(name) {
72
73
  * Returns the absolute path to the barrel, or null if none found.
73
74
  */
74
75
  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;
76
+ const tsPath = resolve(subdirPath, "index.ts");
77
+ if (existsSync(tsPath)) {
78
+ return tsPath;
79
+ }
80
+ const tsxPath = resolve(subdirPath, "index.tsx");
81
+ if (existsSync(tsxPath)) {
82
+ return tsxPath;
83
+ }
84
+ return null;
84
85
  }
package/dist/cli.js CHANGED
@@ -4,9 +4,10 @@ import { drilldown, drilldownFiles } from "./drilldown.js";
4
4
  import { JsdocError } from "./errors.js";
5
5
  import { lint } from "./lint.js";
6
6
  import { parseSelector } from "./selector.js";
7
- import { SKILL_TEXT } from "./skill-text.js";
7
+ import { RULE_EXPLANATIONS, SKILL_TEXT } from "./skill-text.js";
8
8
  import { VALIDATION_STATUS_PRIORITY } from "./types.js";
9
9
  import { validate } from "./validate.js";
10
+
10
11
  /**
11
12
  * Parses argv flags (--help, --validate, --lint, --skill, --pretty, --limit,
12
13
  * --no-gitignore), dispatches to drilldown, validation, or lint mode, and
@@ -27,8 +28,9 @@ Options:
27
28
  -l, --lint Run lint mode (comprehensive JSDoc quality)
28
29
  -s, --skill Print JSDoc writing guidelines
29
30
  --pretty Format JSON output with 2-space indent
30
- --limit N Max results shown (default 100)
31
+ --limit N Max results shown (default 500)
31
32
  --no-gitignore Include files ignored by .gitignore
33
+ --explain-rule R Explain a lint rule with examples (e.g. jsdoc/informative-docs)
32
34
 
33
35
  Selector:
34
36
  A glob pattern or file path, optionally with @depth suffix (1-4).
@@ -51,6 +53,15 @@ Barrel gating (glob mode):
51
53
  index.ts files with a @summary gate sibling files at depths 1-2.
52
54
  At depth 3 the barrel disappears and its children appear at depth 1.
53
55
 
56
+ Modes:
57
+ -v Validate file-level structure (has JSDoc block, @summary, description)
58
+ -l Lint comprehensive JSDoc quality (file-level + function-level tags)
59
+
60
+ Exit codes:
61
+ 0 Success (all files pass)
62
+ 1 Runtime error (invalid arguments, missing files)
63
+ 2 Validation or lint failures found
64
+
54
65
  Workflow:
55
66
  $ jsdoczoom src/**/*.ts
56
67
  { "items": [{ "next_id": "src/utils@2", "text": "..." }, ...] }
@@ -66,222 +77,296 @@ Workflow:
66
77
  * Parse CLI arguments into flags and positional args.
67
78
  */
68
79
  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
- };
80
+ let help = false;
81
+ let validateMode = false;
82
+ let lintMode = false;
83
+ let skillMode = false;
84
+ let pretty = false;
85
+ let limit = 500;
86
+ let gitignore = true;
87
+ let explainRule;
88
+ let selectorArg;
89
+ for (let i = 0; i < args.length; i++) {
90
+ const arg = args[i];
91
+ if (arg === "-h" || arg === "--help") {
92
+ help = true;
93
+ } else if (arg === "-v" || arg === "--validate") {
94
+ validateMode = true;
95
+ } else if (arg === "-l" || arg === "--lint") {
96
+ lintMode = true;
97
+ } else if (arg === "-s" || arg === "--skill") {
98
+ skillMode = true;
99
+ } else if (arg === "--pretty") {
100
+ pretty = true;
101
+ } else if (arg === "--limit") {
102
+ const next = args[++i];
103
+ limit = Number(next);
104
+ } else if (arg === "--no-gitignore") {
105
+ gitignore = false;
106
+ } else if (arg === "--explain-rule") {
107
+ const next = args[++i];
108
+ explainRule = next;
109
+ } else if (arg.startsWith("-")) {
110
+ throw new JsdocError("INVALID_SELECTOR", `Unrecognized option: ${arg}`);
111
+ } else if (selectorArg === undefined) {
112
+ selectorArg = arg;
113
+ }
114
+ }
115
+ return {
116
+ help,
117
+ validateMode,
118
+ lintMode,
119
+ skillMode,
120
+ pretty,
121
+ limit,
122
+ gitignore,
123
+ explainRule,
124
+ selectorArg,
125
+ };
118
126
  }
119
127
  /**
120
128
  * Resolve stdin file paths to absolute paths.
121
129
  */
122
130
  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));
131
+ return stdin
132
+ .split("\n")
133
+ .map((line) => line.trim())
134
+ .filter((line) => line.length > 0)
135
+ .map((line) => resolve(cwd, line));
128
136
  }
129
137
  /**
130
138
  * Extract depth from a selector argument string.
131
139
  * Returns undefined if no @depth suffix is present.
132
140
  */
133
141
  function extractDepthFromArg(selectorArg) {
134
- const parsed = parseSelector(selectorArg);
135
- return parsed.depth;
142
+ const parsed = parseSelector(selectorArg);
143
+ return parsed.depth;
136
144
  }
137
145
  /**
138
146
  * Process stdin mode: file paths piped in.
139
147
  */
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
- }
148
+ async function processStdin(
149
+ stdin,
150
+ selectorArg,
151
+ validateMode,
152
+ lintMode,
153
+ pretty,
154
+ limit,
155
+ cwd,
156
+ ) {
157
+ const stdinPaths = parseStdinPaths(stdin, cwd);
158
+ const depth =
159
+ selectorArg !== undefined ? extractDepthFromArg(selectorArg) : undefined;
160
+ if (lintMode) {
161
+ const { lintFiles } = await import("./lint.js");
162
+ const result = await lintFiles(stdinPaths, cwd, limit);
163
+ writeLintResult(result, pretty);
164
+ } else if (validateMode) {
165
+ const { validateFiles } = await import("./validate.js");
166
+ const result = await validateFiles(stdinPaths, cwd, limit);
167
+ writeValidationResult(result, pretty);
168
+ } else {
169
+ const result = drilldownFiles(stdinPaths, depth, cwd, limit);
170
+ writeResult(result, pretty);
171
+ }
157
172
  }
158
173
  /**
159
174
  * Process selector mode: glob or path argument.
160
175
  */
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
- }
176
+ async function processSelector(
177
+ selectorArg,
178
+ validateMode,
179
+ lintMode,
180
+ pretty,
181
+ limit,
182
+ gitignore,
183
+ cwd,
184
+ ) {
185
+ const selector = selectorArg
186
+ ? parseSelector(selectorArg)
187
+ : { type: "glob", pattern: "**/*.{ts,tsx}", depth: undefined };
188
+ if (lintMode) {
189
+ const result = await lint(selector, cwd, limit, gitignore);
190
+ writeLintResult(result, pretty);
191
+ } else if (validateMode) {
192
+ const result = await validate(selector, cwd, limit, gitignore);
193
+ writeValidationResult(result, pretty);
194
+ } else {
195
+ const result = drilldown(selector, cwd, gitignore, limit);
196
+ writeResult(result, pretty);
197
+ }
177
198
  }
178
199
  /**
179
200
  * Write an error to stderr as JSON and set exit code.
180
201
  */
181
202
  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;
203
+ if (error instanceof JsdocError) {
204
+ process.stderr.write(`${JSON.stringify(error.toJSON())}\n`);
205
+ process.exitCode = 1;
206
+ return;
207
+ }
208
+ const message = error instanceof Error ? error.message : String(error);
209
+ process.stderr.write(
210
+ `${JSON.stringify({ error: { code: "INTERNAL_ERROR", message } })}\n`,
211
+ );
212
+ process.exitCode = 1;
190
213
  }
191
214
  /**
192
215
  * Main CLI entry point. Exported for testability.
193
216
  */
194
217
  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
- }
218
+ try {
219
+ const {
220
+ help,
221
+ validateMode,
222
+ lintMode,
223
+ skillMode,
224
+ pretty,
225
+ limit,
226
+ gitignore,
227
+ explainRule,
228
+ selectorArg,
229
+ } = parseArgs(args);
230
+ if (help) {
231
+ process.stdout.write(HELP_TEXT);
232
+ return;
233
+ }
234
+ if (skillMode) {
235
+ process.stdout.write(SKILL_TEXT);
236
+ return;
237
+ }
238
+ if (explainRule !== undefined) {
239
+ const explanation = RULE_EXPLANATIONS[explainRule];
240
+ if (explanation) {
241
+ process.stdout.write(explanation);
242
+ } else {
243
+ const available = Object.keys(RULE_EXPLANATIONS).join(", ");
244
+ writeError(
245
+ new JsdocError(
246
+ "INVALID_SELECTOR",
247
+ `Unknown rule: ${explainRule}. Available rules: ${available}`,
248
+ ),
249
+ );
250
+ }
251
+ return;
252
+ }
253
+ if (validateMode && lintMode) {
254
+ writeError(
255
+ new JsdocError("INVALID_SELECTOR", "Cannot use -v and -l together"),
256
+ );
257
+ return;
258
+ }
259
+ const cwd = process.cwd();
260
+ if (stdin !== undefined) {
261
+ await processStdin(
262
+ stdin,
263
+ selectorArg,
264
+ validateMode,
265
+ lintMode,
266
+ pretty,
267
+ limit,
268
+ cwd,
269
+ );
270
+ } else {
271
+ await processSelector(
272
+ selectorArg,
273
+ validateMode,
274
+ lintMode,
275
+ pretty,
276
+ limit,
277
+ gitignore,
278
+ cwd,
279
+ );
280
+ }
281
+ } catch (error) {
282
+ writeError(error);
283
+ }
220
284
  }
221
285
  /**
222
286
  * Write a result to stdout as JSON.
223
287
  */
224
288
  function writeResult(result, pretty) {
225
- const json = pretty
226
- ? JSON.stringify(result, null, 2)
227
- : JSON.stringify(result);
228
- process.stdout.write(`${json}\n`);
289
+ const json = pretty
290
+ ? JSON.stringify(result, null, 2)
291
+ : JSON.stringify(result);
292
+ process.stdout.write(`${json}\n`);
229
293
  }
230
294
  /**
231
295
  * Count invalid files across all validation groups.
232
296
  */
233
297
  function countInvalid(result) {
234
- return VALIDATION_STATUS_PRIORITY.reduce((sum, status) => sum + (result[status]?.files.length ?? 0), 0);
298
+ return VALIDATION_STATUS_PRIORITY.reduce(
299
+ (sum, status) => sum + (result[status]?.files.length ?? 0),
300
+ 0,
301
+ );
235
302
  }
236
303
  /**
237
304
  * Write validation result to stdout and set exit code.
238
305
  * Adds success/message fields to the output.
239
306
  */
240
307
  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
- }
308
+ const invalidCount = countInvalid(result);
309
+ if (invalidCount === 0) {
310
+ writeResult(
311
+ { success: true, message: "All files passed validation" },
312
+ pretty,
313
+ );
314
+ } else {
315
+ writeResult(
316
+ {
317
+ ...result,
318
+ success: false,
319
+ error: {
320
+ code: "VALIDATION_FAILED",
321
+ message: `${invalidCount} file(s) failed validation`,
322
+ },
323
+ },
324
+ pretty,
325
+ );
326
+ process.exitCode = 2;
327
+ }
250
328
  }
251
329
  /**
252
330
  * Write lint result to stdout and set exit code 2 if issues found.
253
331
  */
254
332
  function writeLintResult(result, pretty) {
255
- writeResult(result, pretty);
256
- if (result.summary.filesWithIssues > 0) {
257
- process.exitCode = 2;
258
- }
333
+ writeResult(result, pretty);
334
+ if (result.summary.filesWithIssues > 0) {
335
+ process.exitCode = 2;
336
+ // Warn if output was truncated
337
+ if (result.summary.filesWithIssues > result.files.length) {
338
+ process.stderr.write(
339
+ `Warning: output truncated to ${result.files.length} of ${result.summary.filesWithIssues} files with issues. Use --limit to see more.\n`,
340
+ );
341
+ }
342
+ }
259
343
  }
260
344
  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");
345
+ const chunks = [];
346
+ for await (const chunk of process.stdin) {
347
+ chunks.push(chunk);
348
+ }
349
+ return Buffer.concat(chunks).toString("utf-8");
266
350
  }
267
351
  // Auto-invoke when run as CLI
268
352
  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
- }
353
+ if (!process.argv[1]) return false;
354
+ try {
355
+ const scriptPath = process.argv[1].replace(/\\/g, "/");
356
+ return (
357
+ import.meta.url.endsWith(scriptPath) ||
358
+ import.meta.url.endsWith("/cli.js")
359
+ );
360
+ } catch {
361
+ // Cannot determine script path — safe to skip auto-invoke
362
+ return false;
363
+ }
280
364
  }
281
365
  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
- });
366
+ // Only read stdin when it's explicitly piped (isTTY === false, not just undefined)
367
+ const stdinText =
368
+ process.stdin.isTTY === false ? await readStdin() : undefined;
369
+ main(process.argv.slice(2), stdinText).catch(() => {
370
+ // Error already handled in main
371
+ });
287
372
  }