jsdoczoom 0.3.1 → 0.4.10

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.
Files changed (62) hide show
  1. package/dist/src/cache.js +97 -0
  2. package/dist/{cli.js → src/cli.js} +183 -110
  3. package/dist/{drilldown.js → src/drilldown.js} +163 -53
  4. package/dist/{file-discovery.js → src/file-discovery.js} +1 -2
  5. package/dist/src/index.js +19 -0
  6. package/dist/{lint.js → src/lint.js} +30 -12
  7. package/dist/{skill-text.js → src/skill-text.js} +7 -6
  8. package/dist/{types.js → src/types.js} +4 -0
  9. package/dist/{validate.js → src/validate.js} +29 -11
  10. package/dist/test/barrel.test.js +77 -0
  11. package/dist/test/cache.test.js +222 -0
  12. package/dist/test/cli.test.js +479 -0
  13. package/dist/test/drilldown-barrel.test.js +383 -0
  14. package/dist/test/drilldown.test.js +469 -0
  15. package/dist/test/errors.test.js +26 -0
  16. package/dist/test/eslint-engine.test.js +130 -0
  17. package/dist/test/eslint-plugin.test.js +291 -0
  18. package/dist/test/file-discovery.test.js +72 -0
  19. package/dist/test/jsdoc-parser.test.js +353 -0
  20. package/dist/test/lint.test.js +413 -0
  21. package/dist/test/selector.test.js +93 -0
  22. package/dist/test/type-declarations.test.js +321 -0
  23. package/dist/test/validate.test.js +361 -0
  24. package/package.json +2 -2
  25. package/types/src/cache.d.ts +74 -0
  26. package/types/{drilldown.d.ts → src/drilldown.d.ts} +5 -3
  27. package/types/{file-discovery.d.ts → src/file-discovery.d.ts} +14 -0
  28. package/types/{index.d.ts → src/index.d.ts} +9 -4
  29. package/types/{lint.d.ts → src/lint.d.ts} +5 -1
  30. package/types/{skill-text.d.ts → src/skill-text.d.ts} +1 -1
  31. package/types/{types.d.ts → src/types.d.ts} +9 -0
  32. package/types/{validate.d.ts → src/validate.d.ts} +6 -1
  33. package/types/test/barrel.test.d.ts +1 -0
  34. package/types/test/cache.test.d.ts +8 -0
  35. package/types/test/cli.test.d.ts +1 -0
  36. package/types/test/drilldown-barrel.test.d.ts +1 -0
  37. package/types/test/drilldown.test.d.ts +1 -0
  38. package/types/test/errors.test.d.ts +1 -0
  39. package/types/test/eslint-engine.test.d.ts +6 -0
  40. package/types/test/eslint-plugin.test.d.ts +1 -0
  41. package/types/test/file-discovery.test.d.ts +1 -0
  42. package/types/test/jsdoc-parser.test.d.ts +1 -0
  43. package/types/test/lint.test.d.ts +9 -0
  44. package/types/test/selector.test.d.ts +1 -0
  45. package/types/test/type-declarations.test.d.ts +1 -0
  46. package/types/test/validate.test.d.ts +1 -0
  47. package/dist/index.js +0 -17
  48. /package/dist/{barrel.js → src/barrel.js} +0 -0
  49. /package/dist/{errors.js → src/errors.js} +0 -0
  50. /package/dist/{eslint-engine.js → src/eslint-engine.js} +0 -0
  51. /package/dist/{eslint-plugin.js → src/eslint-plugin.js} +0 -0
  52. /package/dist/{jsdoc-parser.js → src/jsdoc-parser.js} +0 -0
  53. /package/dist/{selector.js → src/selector.js} +0 -0
  54. /package/dist/{type-declarations.js → src/type-declarations.js} +0 -0
  55. /package/types/{barrel.d.ts → src/barrel.d.ts} +0 -0
  56. /package/types/{cli.d.ts → src/cli.d.ts} +0 -0
  57. /package/types/{errors.d.ts → src/errors.d.ts} +0 -0
  58. /package/types/{eslint-engine.d.ts → src/eslint-engine.d.ts} +0 -0
  59. /package/types/{eslint-plugin.d.ts → src/eslint-plugin.d.ts} +0 -0
  60. /package/types/{jsdoc-parser.d.ts → src/jsdoc-parser.d.ts} +0 -0
  61. /package/types/{selector.d.ts → src/selector.d.ts} +0 -0
  62. /package/types/{type-declarations.d.ts → src/type-declarations.d.ts} +0 -0
@@ -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
+ }
@@ -4,11 +4,11 @@ import { dirname, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { drilldown, drilldownFiles } from "./drilldown.js";
6
6
  import { JsdocError } from "./errors.js";
7
- import { lint } from "./lint.js";
7
+ import { lint, lintFiles } from "./lint.js";
8
8
  import { parseSelector } from "./selector.js";
9
9
  import { RULE_EXPLANATIONS, SKILL_TEXT } from "./skill-text.js";
10
- import { VALIDATION_STATUS_PRIORITY } from "./types.js";
11
- import { validate } from "./validate.js";
10
+ import { DEFAULT_CACHE_DIR, VALIDATION_STATUS_PRIORITY } from "./types.js";
11
+ import { validate, validateFiles } from "./validate.js";
12
12
 
13
13
  /**
14
14
  * Parses argv flags (--help, --version, --check, --lint, --skill, --pretty,
@@ -33,6 +33,8 @@ Options:
33
33
  --pretty Format JSON output with 2-space indent
34
34
  --limit N Max results shown (default 500)
35
35
  --no-gitignore Include files ignored by .gitignore
36
+ --disable-cache Skip all cache operations
37
+ --cache-directory Override cache directory (default: system temp)
36
38
  --explain-rule R Explain a lint rule with examples (e.g. jsdoc/informative-docs)
37
39
 
38
40
  Selector:
@@ -53,8 +55,10 @@ Output:
53
55
  selector. Items with "id" (no next_id) are at terminal depth.
54
56
 
55
57
  Barrel gating (glob mode):
56
- index.ts files with a @summary gate sibling files at depths 1-2.
57
- 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.
58
62
 
59
63
  Modes:
60
64
  -c Validate file-level structure (has JSDoc block, @summary, description)
@@ -76,60 +80,95 @@ Workflow:
76
80
  { "items": [{ "id": "src/utils@3", "text": "..." }] }
77
81
  # "id" instead of "next_id" means terminal depth — stop here
78
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
+ }
79
90
  /**
80
91
  * Parse CLI arguments into flags and positional args.
81
92
  */
82
93
  function parseArgs(args) {
83
- let help = false;
84
- let version = false;
85
- let checkMode = false;
86
- let lintMode = false;
87
- let skillMode = false;
88
- let pretty = false;
89
- let limit = 500;
90
- let gitignore = true;
91
- let explainRule;
92
- 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
+ };
93
108
  for (let i = 0; i < args.length; i++) {
94
109
  const arg = args[i];
110
+ // Boolean flags
95
111
  if (arg === "-h" || arg === "--help") {
96
- help = true;
97
- } else if (arg === "-v" || arg === "--version") {
98
- version = true;
99
- } else if (arg === "-c" || arg === "--check") {
100
- checkMode = true;
101
- } else if (arg === "-l" || arg === "--lint") {
102
- lintMode = true;
103
- } else if (arg === "-s" || arg === "--skill") {
104
- skillMode = true;
105
- } else if (arg === "--pretty") {
106
- pretty = true;
107
- } else if (arg === "--limit") {
108
- const next = args[++i];
109
- limit = Number(next);
110
- } else if (arg === "--no-gitignore") {
111
- gitignore = false;
112
- } else if (arg === "--explain-rule") {
113
- const next = args[++i];
114
- explainRule = next;
115
- } 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("-")) {
116
164
  throw new JsdocError("INVALID_SELECTOR", `Unrecognized option: ${arg}`);
117
- } else if (selectorArg === undefined) {
118
- selectorArg = arg;
165
+ }
166
+ // Positional selector arg
167
+ if (parsed.selectorArg === undefined) {
168
+ parsed.selectorArg = arg;
119
169
  }
120
170
  }
121
- return {
122
- help,
123
- version,
124
- checkMode,
125
- lintMode,
126
- skillMode,
127
- pretty,
128
- limit,
129
- gitignore,
130
- explainRule,
131
- selectorArg,
132
- };
171
+ return parsed;
133
172
  }
134
173
  /**
135
174
  * Resolve stdin file paths to absolute paths.
@@ -160,20 +199,25 @@ async function processStdin(
160
199
  pretty,
161
200
  limit,
162
201
  cwd,
202
+ cacheConfig,
163
203
  ) {
164
204
  const stdinPaths = parseStdinPaths(stdin, cwd);
165
205
  const depth =
166
206
  selectorArg !== undefined ? extractDepthFromArg(selectorArg) : undefined;
167
207
  if (lintMode) {
168
- const { lintFiles } = await import("./lint.js");
169
- const result = await lintFiles(stdinPaths, cwd, limit);
208
+ const result = await lintFiles(stdinPaths, cwd, limit, cacheConfig);
170
209
  writeLintResult(result, pretty);
171
210
  } else if (checkMode) {
172
- const { validateFiles } = await import("./validate.js");
173
- const result = await validateFiles(stdinPaths, cwd, limit);
211
+ const result = await validateFiles(stdinPaths, cwd, limit, cacheConfig);
174
212
  writeValidationResult(result, pretty);
175
213
  } else {
176
- const result = drilldownFiles(stdinPaths, depth, cwd, limit);
214
+ const result = await drilldownFiles(
215
+ stdinPaths,
216
+ depth,
217
+ cwd,
218
+ limit,
219
+ cacheConfig,
220
+ );
177
221
  writeResult(result, pretty);
178
222
  }
179
223
  }
@@ -188,18 +232,25 @@ async function processSelector(
188
232
  limit,
189
233
  gitignore,
190
234
  cwd,
235
+ cacheConfig,
191
236
  ) {
192
237
  const selector = selectorArg
193
238
  ? parseSelector(selectorArg)
194
239
  : { type: "glob", pattern: "**/*.{ts,tsx}", depth: undefined };
195
240
  if (lintMode) {
196
- const result = await lint(selector, cwd, limit, gitignore);
241
+ const result = await lint(selector, cwd, limit, gitignore, cacheConfig);
197
242
  writeLintResult(result, pretty);
198
243
  } else if (checkMode) {
199
- const result = await validate(selector, cwd, limit, gitignore);
244
+ const result = await validate(selector, cwd, limit, gitignore, cacheConfig);
200
245
  writeValidationResult(result, pretty);
201
246
  } else {
202
- const result = drilldown(selector, cwd, gitignore, limit);
247
+ const result = await drilldown(
248
+ selector,
249
+ cwd,
250
+ gitignore,
251
+ limit,
252
+ cacheConfig,
253
+ );
203
254
  writeResult(result, pretty);
204
255
  }
205
256
  }
@@ -208,92 +259,114 @@ async function processSelector(
208
259
  */
209
260
  function writeError(error) {
210
261
  if (error instanceof JsdocError) {
211
- process.stderr.write(`${JSON.stringify(error.toJSON())}\n`);
262
+ void process.stderr.write(`${JSON.stringify(error.toJSON())}\n`);
212
263
  process.exitCode = 1;
213
264
  return;
214
265
  }
215
266
  const message = error instanceof Error ? error.message : String(error);
216
- process.stderr.write(
267
+ void process.stderr.write(
217
268
  `${JSON.stringify({ error: { code: "INTERNAL_ERROR", message } })}\n`,
218
269
  );
219
270
  process.exitCode = 1;
220
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
+ }
221
313
  /**
222
314
  * Main CLI entry point. Exported for testability.
223
315
  */
224
316
  export async function main(args, stdin) {
225
317
  try {
226
- const {
227
- help,
228
- version,
229
- checkMode,
230
- lintMode,
231
- skillMode,
232
- pretty,
233
- limit,
234
- gitignore,
235
- explainRule,
236
- selectorArg,
237
- } = parseArgs(args);
238
- if (help) {
239
- process.stdout.write(HELP_TEXT);
318
+ const parsed = parseArgs(args);
319
+ // Handle early-exit flags
320
+ if (parsed.help) {
321
+ handleHelp();
240
322
  return;
241
323
  }
242
- if (version) {
243
- const pkgPath = resolve(
244
- dirname(fileURLToPath(import.meta.url)),
245
- "..",
246
- "package.json",
247
- );
248
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
249
- process.stdout.write(`${pkg.version}\n`);
324
+ if (parsed.version) {
325
+ handleVersion();
250
326
  return;
251
327
  }
252
- if (skillMode) {
253
- process.stdout.write(SKILL_TEXT);
328
+ if (parsed.skillMode) {
329
+ handleSkill();
254
330
  return;
255
331
  }
256
- if (explainRule !== undefined) {
257
- const explanation = RULE_EXPLANATIONS[explainRule];
258
- if (explanation) {
259
- process.stdout.write(explanation);
260
- } else {
261
- const available = Object.keys(RULE_EXPLANATIONS).join(", ");
262
- writeError(
263
- new JsdocError(
264
- "INVALID_SELECTOR",
265
- `Unknown rule: ${explainRule}. Available rules: ${available}`,
266
- ),
267
- );
268
- }
332
+ if (parsed.explainRule !== undefined) {
333
+ handleExplainRule(parsed.explainRule);
269
334
  return;
270
335
  }
271
- if (checkMode && lintMode) {
336
+ // Validate mode combinations
337
+ if (parsed.checkMode && parsed.lintMode) {
272
338
  writeError(
273
339
  new JsdocError("INVALID_SELECTOR", "Cannot use -c and -l together"),
274
340
  );
275
341
  return;
276
342
  }
343
+ // Build cache config and process files
344
+ const cacheConfig = {
345
+ enabled: !parsed.disableCache,
346
+ directory: parsed.cacheDirectory ?? DEFAULT_CACHE_DIR,
347
+ };
277
348
  const cwd = process.cwd();
278
349
  if (stdin !== undefined) {
279
350
  await processStdin(
280
351
  stdin,
281
- selectorArg,
282
- checkMode,
283
- lintMode,
284
- pretty,
285
- limit,
352
+ parsed.selectorArg,
353
+ parsed.checkMode,
354
+ parsed.lintMode,
355
+ parsed.pretty,
356
+ parsed.limit,
286
357
  cwd,
358
+ cacheConfig,
287
359
  );
288
360
  } else {
289
361
  await processSelector(
290
- selectorArg,
291
- checkMode,
292
- lintMode,
293
- pretty,
294
- limit,
295
- gitignore,
362
+ parsed.selectorArg,
363
+ parsed.checkMode,
364
+ parsed.lintMode,
365
+ parsed.pretty,
366
+ parsed.limit,
367
+ parsed.gitignore,
296
368
  cwd,
369
+ cacheConfig,
297
370
  );
298
371
  }
299
372
  } catch (error) {
@@ -307,7 +380,7 @@ function writeResult(result, pretty) {
307
380
  const json = pretty
308
381
  ? JSON.stringify(result, null, 2)
309
382
  : JSON.stringify(result);
310
- process.stdout.write(`${json}\n`);
383
+ void process.stdout.write(`${json}\n`);
311
384
  }
312
385
  /**
313
386
  * Count invalid files across all validation groups.
@@ -353,7 +426,7 @@ function writeLintResult(result, pretty) {
353
426
  process.exitCode = 2;
354
427
  // Warn if output was truncated
355
428
  if (result.summary.filesWithIssues > result.files.length) {
356
- process.stderr.write(
429
+ void process.stderr.write(
357
430
  `Warning: output truncated to ${result.files.length} of ${result.summary.filesWithIssues} files with issues. Use --limit to see more.\n`,
358
431
  );
359
432
  }