jsdoczoom 0.3.0 → 0.4.5

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,5 +1,5 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
- import { basename, dirname, resolve } from "node:path";
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.
5
5
  * A barrel is strictly `index.ts` or `index.tsx`; other index variants
@@ -83,3 +83,28 @@ function findChildBarrel(subdirPath) {
83
83
  }
84
84
  return null;
85
85
  }
86
+ /** Minimum number of .ts/.tsx files in a directory to require a barrel. */
87
+ export const BARREL_THRESHOLD = 3;
88
+ /**
89
+ * Find directories with more than BARREL_THRESHOLD .ts/.tsx files
90
+ * that lack a barrel file (index.ts or index.tsx).
91
+ */
92
+ export function findMissingBarrels(filePaths, cwd) {
93
+ const dirCounts = new Map();
94
+ for (const filePath of filePaths) {
95
+ if (isBarrel(filePath)) continue;
96
+ const dir = dirname(filePath);
97
+ dirCounts.set(dir, (dirCounts.get(dir) ?? 0) + 1);
98
+ }
99
+ 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
+ }
109
+ return missing.sort();
110
+ }
package/dist/cache.js ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Content-hash-based disk caching layer for expensive operations (TypeScript
3
+ * compilation, ESLint linting, JSDoc parsing). Uses SHA-256 hashing to cache
4
+ * results keyed by file content, enabling near-instantaneous repeated runs
5
+ * when files haven't changed. All cache errors degrade gracefully without
6
+ * propagating to callers.
7
+ *
8
+ * @summary Content-hash-based disk cache with graceful error handling
9
+ */
10
+ import { createHash } from "node:crypto";
11
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ /**
14
+ * Compute a SHA-256 content hash for the given string.
15
+ *
16
+ * @param content - The content to hash
17
+ * @returns Hex-encoded SHA-256 digest
18
+ */
19
+ export function computeContentHash(content) {
20
+ return createHash("sha256").update(content).digest("hex");
21
+ }
22
+ /**
23
+ * Ensure the cache directory exists for the given operation mode.
24
+ * Silently swallows all errors (mkdir failures are handled by read/write ops).
25
+ *
26
+ * @param config - Cache configuration
27
+ * @param mode - Operation mode (drilldown, validate, or lint)
28
+ */
29
+ export async function ensureCacheDir(config, mode) {
30
+ try {
31
+ await mkdir(join(config.directory, mode), { recursive: true });
32
+ } catch {
33
+ // Silently swallow - subsequent reads will miss, writes will fail silently
34
+ }
35
+ }
36
+ /**
37
+ * Read a cached entry from disk. Returns null on any error (cache miss, ENOENT,
38
+ * corrupted JSON, permission denied, etc.). Never throws.
39
+ *
40
+ * @param config - Cache configuration
41
+ * @param mode - Operation mode namespace
42
+ * @param hash - Content hash key
43
+ * @returns Parsed cached data or null on any error
44
+ */
45
+ export async function readCacheEntry(config, mode, hash) {
46
+ try {
47
+ const filePath = join(config.directory, mode, `${hash}.json`);
48
+ const content = await readFile(filePath, "utf-8");
49
+ return JSON.parse(content);
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+ /**
55
+ * Write a cache entry to disk using atomic write (write to .tmp then rename).
56
+ * Silently swallows all errors (permission denied, disk full, etc.). Never throws.
57
+ *
58
+ * @param config - Cache configuration
59
+ * @param mode - Operation mode namespace
60
+ * @param hash - Content hash key
61
+ * @param data - Data to cache (must be JSON-serializable)
62
+ */
63
+ export async function writeCacheEntry(config, mode, hash, data) {
64
+ try {
65
+ const filePath = join(config.directory, mode, `${hash}.json`);
66
+ const tmpPath = `${filePath}.tmp`;
67
+ await writeFile(tmpPath, JSON.stringify(data), "utf-8");
68
+ await rename(tmpPath, filePath);
69
+ } catch {
70
+ // Silently swallow - cache write failures should never affect correctness
71
+ }
72
+ }
73
+ /**
74
+ * Execute a computation with caching. If cache is enabled and entry exists,
75
+ * returns cached result. Otherwise, computes result, stores it (fire-and-forget),
76
+ * and returns it. Degrades gracefully on all cache errors by always computing.
77
+ *
78
+ * @param config - Cache configuration
79
+ * @param mode - Operation mode namespace
80
+ * @param content - File content to hash for cache key
81
+ * @param compute - Function to compute the result on cache miss
82
+ * @returns Cached or computed result
83
+ */
84
+ export async function processWithCache(config, mode, content, compute) {
85
+ if (!config.enabled) {
86
+ return await compute();
87
+ }
88
+ const hash = computeContentHash(content);
89
+ await ensureCacheDir(config, mode);
90
+ const cached = await readCacheEntry(config, mode, hash);
91
+ if (cached !== null) {
92
+ return cached;
93
+ }
94
+ const result = await compute();
95
+ await writeCacheEntry(config, mode, hash, result);
96
+ return result;
97
+ }
package/dist/cli.js CHANGED
@@ -1,18 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { resolve } from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
3
5
  import { drilldown, drilldownFiles } from "./drilldown.js";
4
6
  import { JsdocError } from "./errors.js";
5
- import { lint } from "./lint.js";
7
+ import { lint, lintFiles } from "./lint.js";
6
8
  import { parseSelector } from "./selector.js";
7
9
  import { RULE_EXPLANATIONS, SKILL_TEXT } from "./skill-text.js";
8
- import { VALIDATION_STATUS_PRIORITY } from "./types.js";
9
- import { validate } from "./validate.js";
10
+ import { DEFAULT_CACHE_DIR, VALIDATION_STATUS_PRIORITY } from "./types.js";
11
+ import { validate, validateFiles } from "./validate.js";
10
12
 
11
13
  /**
12
- * Parses argv flags (--help, --validate, --lint, --skill, --pretty, --limit,
13
- * --no-gitignore), dispatches to drilldown, validation, or lint mode, and
14
- * handles stdin piping. Errors are written to stderr as JSON; validation and
15
- * lint failures use exit code 2 while other errors use exit code 1.
14
+ * Parses argv flags (--help, --version, --check, --lint, --skill, --pretty,
15
+ * --limit, --no-gitignore), dispatches to drilldown, validation, or lint mode,
16
+ * and handles stdin piping. Errors are written to stderr as JSON; validation
17
+ * and lint failures use exit code 2 while other errors use exit code 1.
16
18
  *
17
19
  * @summary CLI entry point -- argument parsing, mode dispatch, and exit code handling
18
20
  */
@@ -24,12 +26,15 @@ Each file has four detail levels (1-indexed): @1 summary, @2 description,
24
26
 
25
27
  Options:
26
28
  -h, --help Show this help text
27
- -v, --validate Run validation mode
29
+ -v, --version Show version number
30
+ -c, --check Run validation mode
28
31
  -l, --lint Run lint mode (comprehensive JSDoc quality)
29
32
  -s, --skill Print JSDoc writing guidelines
30
33
  --pretty Format JSON output with 2-space indent
31
34
  --limit N Max results shown (default 500)
32
35
  --no-gitignore Include files ignored by .gitignore
36
+ --disable-cache Skip all cache operations
37
+ --cache-directory Override cache directory (default: system temp)
33
38
  --explain-rule R Explain a lint rule with examples (e.g. jsdoc/informative-docs)
34
39
 
35
40
  Selector:
@@ -43,18 +48,20 @@ Stdin:
43
48
  Pipe file paths one per line:
44
49
  find . -name "*.ts" | jsdoczoom
45
50
  find . -name "*.ts" | jsdoczoom @2
46
- find . -name "*.ts" | jsdoczoom -v
51
+ find . -name "*.ts" | jsdoczoom -c
47
52
 
48
53
  Output:
49
54
  JSON items. Items with "next_id" have more detail; use that value as the next
50
55
  selector. Items with "id" (no next_id) are at terminal depth.
51
56
 
52
57
  Barrel gating (glob mode):
53
- index.ts files with a @summary gate sibling files at depths 1-2.
54
- At depth 3 the barrel disappears and its children appear at depth 1.
58
+ A barrel's @summary and description reflect the cumulative functionality
59
+ of its directory's children, not the barrel file itself. Barrels with a
60
+ @summary gate sibling files at depths 1-2. At depth 3 the barrel
61
+ disappears and its children appear at depth 1.
55
62
 
56
63
  Modes:
57
- -v Validate file-level structure (has JSDoc block, @summary, description)
64
+ -c Validate file-level structure (has JSDoc block, @summary, description)
58
65
  -l Lint comprehensive JSDoc quality (file-level + function-level tags)
59
66
 
60
67
  Exit codes:
@@ -73,56 +80,95 @@ Workflow:
73
80
  { "items": [{ "id": "src/utils@3", "text": "..." }] }
74
81
  # "id" instead of "next_id" means terminal depth — stop here
75
82
  `;
83
+ /**
84
+ * Parse a flag that requires a value argument.
85
+ * @returns Updated index after consuming the value
86
+ */
87
+ function parseValueFlag(args, index) {
88
+ return { value: args[index + 1], nextIndex: index + 1 };
89
+ }
76
90
  /**
77
91
  * Parse CLI arguments into flags and positional args.
78
92
  */
79
93
  function parseArgs(args) {
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;
94
+ const parsed = {
95
+ help: false,
96
+ version: false,
97
+ checkMode: false,
98
+ lintMode: false,
99
+ skillMode: false,
100
+ pretty: false,
101
+ limit: 500,
102
+ gitignore: true,
103
+ disableCache: false,
104
+ cacheDirectory: undefined,
105
+ explainRule: undefined,
106
+ selectorArg: undefined,
107
+ };
89
108
  for (let i = 0; i < args.length; i++) {
90
109
  const arg = args[i];
110
+ // Boolean flags
91
111
  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("-")) {
112
+ parsed.help = true;
113
+ continue;
114
+ }
115
+ if (arg === "-v" || arg === "--version") {
116
+ parsed.version = true;
117
+ continue;
118
+ }
119
+ if (arg === "-c" || arg === "--check") {
120
+ parsed.checkMode = true;
121
+ continue;
122
+ }
123
+ if (arg === "-l" || arg === "--lint") {
124
+ parsed.lintMode = true;
125
+ continue;
126
+ }
127
+ if (arg === "-s" || arg === "--skill") {
128
+ parsed.skillMode = true;
129
+ continue;
130
+ }
131
+ if (arg === "--pretty") {
132
+ parsed.pretty = true;
133
+ continue;
134
+ }
135
+ if (arg === "--no-gitignore") {
136
+ parsed.gitignore = false;
137
+ continue;
138
+ }
139
+ if (arg === "--disable-cache") {
140
+ parsed.disableCache = true;
141
+ continue;
142
+ }
143
+ // Value flags
144
+ if (arg === "--limit") {
145
+ const { value, nextIndex } = parseValueFlag(args, i);
146
+ parsed.limit = Number(value);
147
+ i = nextIndex;
148
+ continue;
149
+ }
150
+ if (arg === "--cache-directory") {
151
+ const { value, nextIndex } = parseValueFlag(args, i);
152
+ parsed.cacheDirectory = value;
153
+ i = nextIndex;
154
+ continue;
155
+ }
156
+ if (arg === "--explain-rule") {
157
+ const { value, nextIndex } = parseValueFlag(args, i);
158
+ parsed.explainRule = value;
159
+ i = nextIndex;
160
+ continue;
161
+ }
162
+ // Unknown flag or positional arg
163
+ if (arg.startsWith("-")) {
110
164
  throw new JsdocError("INVALID_SELECTOR", `Unrecognized option: ${arg}`);
111
- } else if (selectorArg === undefined) {
112
- selectorArg = arg;
165
+ }
166
+ // Positional selector arg
167
+ if (parsed.selectorArg === undefined) {
168
+ parsed.selectorArg = arg;
113
169
  }
114
170
  }
115
- return {
116
- help,
117
- validateMode,
118
- lintMode,
119
- skillMode,
120
- pretty,
121
- limit,
122
- gitignore,
123
- explainRule,
124
- selectorArg,
125
- };
171
+ return parsed;
126
172
  }
127
173
  /**
128
174
  * Resolve stdin file paths to absolute paths.
@@ -148,25 +194,30 @@ function extractDepthFromArg(selectorArg) {
148
194
  async function processStdin(
149
195
  stdin,
150
196
  selectorArg,
151
- validateMode,
197
+ checkMode,
152
198
  lintMode,
153
199
  pretty,
154
200
  limit,
155
201
  cwd,
202
+ cacheConfig,
156
203
  ) {
157
204
  const stdinPaths = parseStdinPaths(stdin, cwd);
158
205
  const depth =
159
206
  selectorArg !== undefined ? extractDepthFromArg(selectorArg) : undefined;
160
207
  if (lintMode) {
161
- const { lintFiles } = await import("./lint.js");
162
- const result = await lintFiles(stdinPaths, cwd, limit);
208
+ const result = await lintFiles(stdinPaths, cwd, limit, cacheConfig);
163
209
  writeLintResult(result, pretty);
164
- } else if (validateMode) {
165
- const { validateFiles } = await import("./validate.js");
166
- const result = await validateFiles(stdinPaths, cwd, limit);
210
+ } else if (checkMode) {
211
+ const result = await validateFiles(stdinPaths, cwd, limit, cacheConfig);
167
212
  writeValidationResult(result, pretty);
168
213
  } else {
169
- const result = drilldownFiles(stdinPaths, depth, cwd, limit);
214
+ const result = await drilldownFiles(
215
+ stdinPaths,
216
+ depth,
217
+ cwd,
218
+ limit,
219
+ cacheConfig,
220
+ );
170
221
  writeResult(result, pretty);
171
222
  }
172
223
  }
@@ -175,24 +226,31 @@ async function processStdin(
175
226
  */
176
227
  async function processSelector(
177
228
  selectorArg,
178
- validateMode,
229
+ checkMode,
179
230
  lintMode,
180
231
  pretty,
181
232
  limit,
182
233
  gitignore,
183
234
  cwd,
235
+ cacheConfig,
184
236
  ) {
185
237
  const selector = selectorArg
186
238
  ? parseSelector(selectorArg)
187
239
  : { type: "glob", pattern: "**/*.{ts,tsx}", depth: undefined };
188
240
  if (lintMode) {
189
- const result = await lint(selector, cwd, limit, gitignore);
241
+ const result = await lint(selector, cwd, limit, gitignore, cacheConfig);
190
242
  writeLintResult(result, pretty);
191
- } else if (validateMode) {
192
- const result = await validate(selector, cwd, limit, gitignore);
243
+ } else if (checkMode) {
244
+ const result = await validate(selector, cwd, limit, gitignore, cacheConfig);
193
245
  writeValidationResult(result, pretty);
194
246
  } else {
195
- const result = drilldown(selector, cwd, gitignore, limit);
247
+ const result = await drilldown(
248
+ selector,
249
+ cwd,
250
+ gitignore,
251
+ limit,
252
+ cacheConfig,
253
+ );
196
254
  writeResult(result, pretty);
197
255
  }
198
256
  }
@@ -201,81 +259,114 @@ async function processSelector(
201
259
  */
202
260
  function writeError(error) {
203
261
  if (error instanceof JsdocError) {
204
- process.stderr.write(`${JSON.stringify(error.toJSON())}\n`);
262
+ void process.stderr.write(`${JSON.stringify(error.toJSON())}\n`);
205
263
  process.exitCode = 1;
206
264
  return;
207
265
  }
208
266
  const message = error instanceof Error ? error.message : String(error);
209
- process.stderr.write(
267
+ void process.stderr.write(
210
268
  `${JSON.stringify({ error: { code: "INTERNAL_ERROR", message } })}\n`,
211
269
  );
212
270
  process.exitCode = 1;
213
271
  }
272
+ /**
273
+ * Handle --help flag by printing help text.
274
+ */
275
+ function handleHelp() {
276
+ void process.stdout.write(HELP_TEXT);
277
+ }
278
+ /**
279
+ * Handle --version flag by reading and printing version from package.json.
280
+ */
281
+ function handleVersion() {
282
+ const pkgPath = resolve(
283
+ dirname(fileURLToPath(import.meta.url)),
284
+ "..",
285
+ "package.json",
286
+ );
287
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
288
+ void process.stdout.write(`${pkg.version}\n`);
289
+ }
290
+ /**
291
+ * Handle --skill flag by printing JSDoc writing guidelines.
292
+ */
293
+ function handleSkill() {
294
+ void process.stdout.write(SKILL_TEXT);
295
+ }
296
+ /**
297
+ * Handle --explain-rule flag by printing rule explanation.
298
+ */
299
+ function handleExplainRule(ruleName) {
300
+ const explanation = RULE_EXPLANATIONS[ruleName];
301
+ if (explanation) {
302
+ void process.stdout.write(explanation);
303
+ return;
304
+ }
305
+ const available = Object.keys(RULE_EXPLANATIONS).join(", ");
306
+ writeError(
307
+ new JsdocError(
308
+ "INVALID_SELECTOR",
309
+ `Unknown rule: ${ruleName}. Available rules: ${available}`,
310
+ ),
311
+ );
312
+ }
214
313
  /**
215
314
  * Main CLI entry point. Exported for testability.
216
315
  */
217
316
  export async function main(args, stdin) {
218
317
  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);
318
+ const parsed = parseArgs(args);
319
+ // Handle early-exit flags
320
+ if (parsed.help) {
321
+ handleHelp();
322
+ return;
323
+ }
324
+ if (parsed.version) {
325
+ handleVersion();
232
326
  return;
233
327
  }
234
- if (skillMode) {
235
- process.stdout.write(SKILL_TEXT);
328
+ if (parsed.skillMode) {
329
+ handleSkill();
236
330
  return;
237
331
  }
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
- }
332
+ if (parsed.explainRule !== undefined) {
333
+ handleExplainRule(parsed.explainRule);
251
334
  return;
252
335
  }
253
- if (validateMode && lintMode) {
336
+ // Validate mode combinations
337
+ if (parsed.checkMode && parsed.lintMode) {
254
338
  writeError(
255
- new JsdocError("INVALID_SELECTOR", "Cannot use -v and -l together"),
339
+ new JsdocError("INVALID_SELECTOR", "Cannot use -c and -l together"),
256
340
  );
257
341
  return;
258
342
  }
343
+ // Build cache config and process files
344
+ const cacheConfig = {
345
+ enabled: !parsed.disableCache,
346
+ directory: parsed.cacheDirectory ?? DEFAULT_CACHE_DIR,
347
+ };
259
348
  const cwd = process.cwd();
260
349
  if (stdin !== undefined) {
261
350
  await processStdin(
262
351
  stdin,
263
- selectorArg,
264
- validateMode,
265
- lintMode,
266
- pretty,
267
- limit,
352
+ parsed.selectorArg,
353
+ parsed.checkMode,
354
+ parsed.lintMode,
355
+ parsed.pretty,
356
+ parsed.limit,
268
357
  cwd,
358
+ cacheConfig,
269
359
  );
270
360
  } else {
271
361
  await processSelector(
272
- selectorArg,
273
- validateMode,
274
- lintMode,
275
- pretty,
276
- limit,
277
- gitignore,
362
+ parsed.selectorArg,
363
+ parsed.checkMode,
364
+ parsed.lintMode,
365
+ parsed.pretty,
366
+ parsed.limit,
367
+ parsed.gitignore,
278
368
  cwd,
369
+ cacheConfig,
279
370
  );
280
371
  }
281
372
  } catch (error) {
@@ -289,7 +380,7 @@ function writeResult(result, pretty) {
289
380
  const json = pretty
290
381
  ? JSON.stringify(result, null, 2)
291
382
  : JSON.stringify(result);
292
- process.stdout.write(`${json}\n`);
383
+ void process.stdout.write(`${json}\n`);
293
384
  }
294
385
  /**
295
386
  * Count invalid files across all validation groups.
@@ -335,7 +426,7 @@ function writeLintResult(result, pretty) {
335
426
  process.exitCode = 2;
336
427
  // Warn if output was truncated
337
428
  if (result.summary.filesWithIssues > result.files.length) {
338
- process.stderr.write(
429
+ void process.stderr.write(
339
430
  `Warning: output truncated to ${result.files.length} of ${result.summary.filesWithIssues} files with issues. Use --limit to see more.\n`,
340
431
  );
341
432
  }