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/drilldown.js CHANGED
@@ -1,13 +1,20 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFile } from "node:fs/promises";
2
2
  import { dirname, relative } from "node:path";
3
3
  import { getBarrelChildren, isBarrel } from "./barrel.js";
4
+ import { processWithCache } from "./cache.js";
4
5
  import { JsdocError } from "./errors.js";
5
- import { discoverFiles } from "./file-discovery.js";
6
+ import { discoverFiles, loadGitignore } from "./file-discovery.js";
6
7
  import { parseFileSummaries } from "./jsdoc-parser.js";
7
8
  import { generateTypeDeclarations } from "./type-declarations.js";
9
+ import { DEFAULT_CACHE_DIR } from "./types.js";
8
10
 
9
11
  /** Terminal level (1-indexed): 1=summary, 2=description, 3=type declarations, 4=full file. */
10
12
  const TERMINAL_LEVEL = 4;
13
+ /** Default cache configuration used when no config is provided. */
14
+ const DEFAULT_CACHE_CONFIG = {
15
+ enabled: true,
16
+ directory: DEFAULT_CACHE_DIR,
17
+ };
11
18
  /**
12
19
  * Build the fixed drill-down level array for a file.
13
20
  *
@@ -17,14 +24,16 @@ const TERMINAL_LEVEL = 4;
17
24
  * - Level 4 (index 3): full file content (always present, terminal)
18
25
  *
19
26
  * The array always has 4 entries. Null entries are skipped during processing.
27
+ * Type declarations and file content are pre-computed and passed in to support
28
+ * async cache integration.
20
29
  */
21
- function buildLevels(info) {
30
+ function buildLevels(info, typeDeclarations, fileContent) {
22
31
  const { summary, description } = info;
23
32
  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") },
33
+ summary !== null ? { text: summary } : null,
34
+ description !== null ? { text: description } : null,
35
+ { text: typeDeclarations },
36
+ { text: fileContent },
28
37
  ];
29
38
  }
30
39
  /**
@@ -53,9 +62,9 @@ function sortKey(entry) {
53
62
  * If the requested depth level is null (empty), advance to the next non-null level.
54
63
  * Output contains next_id when more detail is available, or id at terminal level.
55
64
  */
56
- function processFile(info, depth, cwd) {
65
+ function processFile(info, typeDeclarations, fileContent, depth, cwd) {
57
66
  const idPath = displayPath(info.path, cwd);
58
- const levels = buildLevels(info);
67
+ const levels = buildLevels(info, typeDeclarations, fileContent);
59
68
  // Clamp depth to [1, TERMINAL_LEVEL], advance past null levels
60
69
  let effectiveDepth = Math.max(1, Math.min(depth, TERMINAL_LEVEL));
61
70
  while (
@@ -68,12 +77,12 @@ function processFile(info, depth, cwd) {
68
77
  if (effectiveDepth < TERMINAL_LEVEL) {
69
78
  return {
70
79
  next_id: `${idPath}@${effectiveDepth + 1}`,
71
- text: level.text(),
80
+ text: level.text,
72
81
  };
73
82
  }
74
83
  return {
75
84
  id: `${idPath}@${effectiveDepth}`,
76
- text: level.text(),
85
+ text: level.text,
77
86
  };
78
87
  }
79
88
  /**
@@ -91,14 +100,56 @@ function makeParseErrorItem(filePath, error, cwd) {
91
100
  function isParseError(error) {
92
101
  return error instanceof JsdocError && error.code === "PARSE_ERROR";
93
102
  }
103
+ /**
104
+ * Determine the effective depth after null-level advancement.
105
+ * Advances past null summary (L1) and null description (L2) levels.
106
+ */
107
+ function computeEffectiveDepth(depth, info) {
108
+ let effectiveDepth = Math.max(1, Math.min(depth, TERMINAL_LEVEL));
109
+ if (effectiveDepth < TERMINAL_LEVEL && info.summary === null) {
110
+ effectiveDepth = Math.max(effectiveDepth, 2);
111
+ }
112
+ if (effectiveDepth < TERMINAL_LEVEL && info.description === null) {
113
+ effectiveDepth = Math.max(effectiveDepth, 3);
114
+ }
115
+ return effectiveDepth;
116
+ }
117
+ /**
118
+ * Conditionally compute type declarations if effective depth requires it (L3+).
119
+ * Uses cache to avoid redundant TypeScript compilation.
120
+ */
121
+ async function computeTypeDeclarationsIfNeeded(
122
+ content,
123
+ filePath,
124
+ effectiveDepth,
125
+ config,
126
+ ) {
127
+ if (effectiveDepth < 3) {
128
+ return "";
129
+ }
130
+ return processWithCache(config, "drilldown", `${content}\0typedecl`, () =>
131
+ generateTypeDeclarations(filePath),
132
+ );
133
+ }
94
134
  /**
95
135
  * Process a file safely, returning an OutputErrorItem on PARSE_ERROR.
96
- * Rethrows non-PARSE_ERROR exceptions.
136
+ * Rethrows non-PARSE_ERROR exceptions. Uses cache for expensive operations
137
+ * (parseFileSummaries and generateTypeDeclarations).
97
138
  */
98
- function processFileSafe(filePath, depth, cwd) {
139
+ async function processFileSafe(filePath, depth, cwd, config) {
99
140
  try {
100
- const info = parseFileSummaries(filePath);
101
- return processFile(info, depth, cwd);
141
+ const content = await readFile(filePath, "utf-8");
142
+ const info = await processWithCache(config, "drilldown", content, () =>
143
+ parseFileSummaries(filePath),
144
+ );
145
+ const effectiveDepth = computeEffectiveDepth(depth, info);
146
+ const typeDeclarations = await computeTypeDeclarationsIfNeeded(
147
+ content,
148
+ filePath,
149
+ effectiveDepth,
150
+ config,
151
+ );
152
+ return processFile(info, typeDeclarations, content, depth, cwd);
102
153
  } catch (error) {
103
154
  if (isParseError(error)) return makeParseErrorItem(filePath, error, cwd);
104
155
  throw error;
@@ -107,26 +158,44 @@ function processFileSafe(filePath, depth, cwd) {
107
158
  /**
108
159
  * Gather barrel info and error entries from a list of barrel file paths.
109
160
  * Returns successfully parsed barrel infos and error entries for unparseable barrels.
161
+ * Uses cache for parseFileSummaries calls.
110
162
  */
111
- function gatherBarrelInfos(barrelPaths, cwd) {
163
+ async function gatherBarrelInfos(barrelPaths, cwd, config) {
112
164
  const infos = [];
113
165
  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;
166
+ const results = await Promise.all(
167
+ barrelPaths.map(async (barrelPath) => {
168
+ try {
169
+ const content = await readFile(barrelPath, "utf-8");
170
+ const info = await processWithCache(config, "drilldown", content, () =>
171
+ parseFileSummaries(barrelPath),
172
+ );
173
+ const children = getBarrelChildren(barrelPath, cwd);
174
+ return {
175
+ type: "info",
176
+ data: {
177
+ path: barrelPath,
178
+ hasSummary: info.summary !== null,
179
+ hasDescription: info.description !== null,
180
+ children,
181
+ },
182
+ };
183
+ } catch (error) {
184
+ if (isParseError(error)) {
185
+ return {
186
+ type: "error",
187
+ data: makeParseErrorItem(barrelPath, error, cwd),
188
+ };
189
+ }
190
+ throw error;
128
191
  }
129
- throw error;
192
+ }),
193
+ );
194
+ for (const result of results) {
195
+ if (result.type === "info") {
196
+ infos.push(result.data);
197
+ } else {
198
+ errors.push(result.data);
130
199
  }
131
200
  }
132
201
  return { infos, errors };
@@ -151,8 +220,9 @@ function buildGatedFileSet(barrelInfos) {
151
220
  * Barrels with summaries gate children for two depths (L1 summary, L2 description),
152
221
  * then transition at depth 3 where the barrel disappears and children appear.
153
222
  */
154
- function processBarrelAtDepth(barrel, depth, cwd) {
155
- if (!barrel.hasSummary) return [processFileSafe(barrel.path, depth, cwd)];
223
+ async function processBarrelAtDepth(barrel, depth, cwd, config) {
224
+ if (!barrel.hasSummary)
225
+ return [await processFileSafe(barrel.path, depth, cwd, config)];
156
226
  // Null-skip: if depth 2 but no description, advance to transition depth
157
227
  let effectiveDepth = depth;
158
228
  if (effectiveDepth === 2 && !barrel.hasDescription) {
@@ -160,7 +230,12 @@ function processBarrelAtDepth(barrel, depth, cwd) {
160
230
  }
161
231
  if (effectiveDepth < 3) {
162
232
  // Show barrel's own L1 (summary) or L2 (description)
163
- const entry = processFileSafe(barrel.path, effectiveDepth, cwd);
233
+ const entry = await processFileSafe(
234
+ barrel.path,
235
+ effectiveDepth,
236
+ cwd,
237
+ config,
238
+ );
164
239
  if ("next_id" in entry) {
165
240
  entry.children = barrel.children.map((c) => relative(cwd, c));
166
241
  }
@@ -168,13 +243,15 @@ function processBarrelAtDepth(barrel, depth, cwd) {
168
243
  }
169
244
  // Barrel transitions: barrel disappears, children appear
170
245
  const childDepth = effectiveDepth - 2;
171
- return collectSafeResults(barrel.children, childDepth, cwd);
246
+ return collectSafeResults(barrel.children, childDepth, cwd, config);
172
247
  }
173
248
  /**
174
- * Process a list of files through processFileSafe.
249
+ * Process a list of files through processFileSafe in parallel.
175
250
  */
176
- function collectSafeResults(files, depth, cwd) {
177
- return files.map((filePath) => processFileSafe(filePath, depth, cwd));
251
+ async function collectSafeResults(files, depth, cwd, config) {
252
+ return Promise.all(
253
+ files.map((filePath) => processFileSafe(filePath, depth, cwd, config)),
254
+ );
178
255
  }
179
256
  /**
180
257
  * Process files discovered via glob with barrel gating.
@@ -187,7 +264,7 @@ function collectSafeResults(files, depth, cwd) {
187
264
  * A barrel that is itself gated by a parent barrel is not processed independently.
188
265
  * Non-barrel files that are not gated by any barrel are processed normally.
189
266
  */
190
- function processGlobWithBarrels(files, depth, cwd) {
267
+ async function processGlobWithBarrels(files, depth, cwd, config) {
191
268
  const barrelPaths = [];
192
269
  const nonBarrelPaths = [];
193
270
  for (const filePath of files) {
@@ -198,20 +275,27 @@ function processGlobWithBarrels(files, depth, cwd) {
198
275
  }
199
276
  }
200
277
  if (barrelPaths.length === 0) {
201
- return collectSafeResults(nonBarrelPaths, depth, cwd);
278
+ return collectSafeResults(nonBarrelPaths, depth, cwd, config);
202
279
  }
203
- const { infos: barrelInfos, errors: barrelErrors } = gatherBarrelInfos(
280
+ const { infos: barrelInfos, errors: barrelErrors } = await gatherBarrelInfos(
204
281
  barrelPaths,
205
282
  cwd,
283
+ config,
206
284
  );
207
285
  const gatedFiles = buildGatedFileSet(barrelInfos);
208
286
  const results = [...barrelErrors];
209
- for (const barrel of barrelInfos) {
210
- if (gatedFiles.has(barrel.path)) continue;
211
- results.push(...processBarrelAtDepth(barrel, depth, cwd));
287
+ const barrelResults = await Promise.all(
288
+ barrelInfos
289
+ .filter((barrel) => !gatedFiles.has(barrel.path))
290
+ .map((barrel) => processBarrelAtDepth(barrel, depth, cwd, config)),
291
+ );
292
+ for (const entries of barrelResults) {
293
+ results.push(...entries);
212
294
  }
213
295
  const ungatedNonBarrels = nonBarrelPaths.filter((f) => !gatedFiles.has(f));
214
- results.push(...collectSafeResults(ungatedNonBarrels, depth, cwd));
296
+ results.push(
297
+ ...(await collectSafeResults(ungatedNonBarrels, depth, cwd, config)),
298
+ );
215
299
  return results;
216
300
  }
217
301
  /**
@@ -228,13 +312,19 @@ function processGlobWithBarrels(files, depth, cwd) {
228
312
  * @throws {JsdocError} NO_FILES_MATCHED for empty glob results
229
313
  * @throws {JsdocError} PARSE_ERROR for path selector targeting file with syntax errors
230
314
  */
231
- export function drilldown(selector, cwd, gitignore = true, limit = 100) {
315
+ export async function drilldown(
316
+ selector,
317
+ cwd,
318
+ gitignore = true,
319
+ limit = 100,
320
+ config = DEFAULT_CACHE_CONFIG,
321
+ ) {
232
322
  const depth = selector.depth ?? 1;
233
323
  if (selector.type === "path") {
234
324
  const files = discoverFiles(selector.pattern, cwd, gitignore);
235
325
  if (files.length > 1) {
236
326
  // Directory expanded to multiple files — route through glob pipeline
237
- const results = processGlobWithBarrels(files, depth, cwd);
327
+ const results = await processGlobWithBarrels(files, depth, cwd, config);
238
328
  const sorted = results.sort((a, b) =>
239
329
  sortKey(a).localeCompare(sortKey(b)),
240
330
  );
@@ -247,8 +337,18 @@ export function drilldown(selector, cwd, gitignore = true, limit = 100) {
247
337
  }
248
338
  // Single file path — errors are fatal, no barrel gating
249
339
  const filePath = files[0];
250
- const info = parseFileSummaries(filePath);
251
- const items = [processFile(info, depth, cwd)];
340
+ const content = await readFile(filePath, "utf-8");
341
+ const info = await processWithCache(config, "drilldown", content, () =>
342
+ parseFileSummaries(filePath),
343
+ );
344
+ const effectiveDepth = computeEffectiveDepth(depth, info);
345
+ const typeDeclarations = await computeTypeDeclarationsIfNeeded(
346
+ content,
347
+ filePath,
348
+ effectiveDepth,
349
+ config,
350
+ );
351
+ const items = [processFile(info, typeDeclarations, content, depth, cwd)];
252
352
  return { items, truncated: false };
253
353
  }
254
354
  // Glob selector — apply barrel gating
@@ -259,7 +359,7 @@ export function drilldown(selector, cwd, gitignore = true, limit = 100) {
259
359
  `No files matched: ${selector.pattern}`,
260
360
  );
261
361
  }
262
- const results = processGlobWithBarrels(files, depth, cwd);
362
+ const results = await processGlobWithBarrels(files, depth, cwd, config);
263
363
  // Sort alphabetically by key
264
364
  const sorted = results.sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
265
365
  const total = sorted.length;
@@ -280,12 +380,22 @@ export function drilldown(selector, cwd, gitignore = true, limit = 100) {
280
380
  * @param cwd - Working directory for relative path output
281
381
  * @returns Array of output entries sorted alphabetically by path
282
382
  */
283
- export function drilldownFiles(filePaths, depth, cwd, limit = 100) {
383
+ export async function drilldownFiles(
384
+ filePaths,
385
+ depth,
386
+ cwd,
387
+ limit = 100,
388
+ config = DEFAULT_CACHE_CONFIG,
389
+ ) {
284
390
  const d = depth ?? 1;
391
+ const ig = loadGitignore(cwd);
285
392
  const tsFiles = filePaths.filter(
286
- (f) => f.endsWith(".ts") || f.endsWith(".tsx"),
393
+ (f) =>
394
+ (f.endsWith(".ts") || f.endsWith(".tsx")) &&
395
+ !f.endsWith(".d.ts") &&
396
+ !ig.ignores(relative(cwd, f)),
287
397
  );
288
- const results = collectSafeResults(tsFiles, d, cwd);
398
+ const results = await collectSafeResults(tsFiles, d, cwd, config);
289
399
  const sorted = results.sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
290
400
  const total = sorted.length;
291
401
  const truncated = total > limit;
@@ -3,7 +3,6 @@ import { dirname, join, relative, resolve } from "node:path";
3
3
  import { globSync } from "glob";
4
4
  import ignore from "ignore";
5
5
  import { JsdocError } from "./errors.js";
6
-
7
6
  /**
8
7
  * Walks .gitignore files from cwd to filesystem root, building an ignore
9
8
  * filter that glob results pass through. Direct-path lookups bypass the
@@ -16,7 +15,7 @@ import { JsdocError } from "./errors.js";
16
15
  * Walk from `cwd` up to the filesystem root, collecting .gitignore entries.
17
16
  * Returns an Ignore instance loaded with all discovered rules.
18
17
  */
19
- function loadGitignore(cwd) {
18
+ export function loadGitignore(cwd) {
20
19
  const ig = ignore();
21
20
  let dir = resolve(cwd);
22
21
  while (true) {
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Re-exports all public functions, classes, and types from internal modules.
3
- * This is the sole entry point for programmatic consumers of the jsdoczoom
4
- * package.
2
+ * Progressively explores TypeScript codebase documentation through a 4-level
3
+ * drill-down (summary, description, type declarations, full source), validates
4
+ * file-level JSDoc structure, and lints comprehensive JSDoc quality using
5
+ * ESLint. Barrel files gate their children at shallow depths, revealing
6
+ * individual files only at deeper levels.
5
7
  *
6
- * @summary Public API barrel re-exporting all functions, types, and classes
8
+ * @summary Progressive JSDoc exploration, validation, and linting for TypeScript codebases
7
9
  */
8
10
  export { getBarrelChildren, isBarrel } from "./barrel.js";
9
11
  export { drilldown, drilldownFiles } from "./drilldown.js";
@@ -13,5 +15,5 @@ export { extractFileJsdoc, parseFileSummaries } from "./jsdoc-parser.js";
13
15
  export { lint, lintFiles } from "./lint.js";
14
16
  export { parseSelector } from "./selector.js";
15
17
  export { generateTypeDeclarations } from "./type-declarations.js";
16
- export { VALIDATION_STATUS_PRIORITY } from "./types.js";
18
+ export { DEFAULT_CACHE_DIR, VALIDATION_STATUS_PRIORITY } from "./types.js";
17
19
  export { validate, validateFiles } from "./validate.js";
package/dist/lint.js CHANGED
@@ -8,11 +8,14 @@
8
8
  *
9
9
  * @summary Lint files for comprehensive JSDoc quality using ESLint engine
10
10
  */
11
- import { readFileSync } from "node:fs";
11
+ import { readFile } from "node:fs/promises";
12
12
  import { relative } from "node:path";
13
+ import { findMissingBarrels } from "./barrel.js";
14
+ import { processWithCache } from "./cache.js";
13
15
  import { JsdocError } from "./errors.js";
14
16
  import { createLintLinter, lintFileForLint } from "./eslint-engine.js";
15
17
  import { discoverFiles } from "./file-discovery.js";
18
+ import { DEFAULT_CACHE_DIR } from "./types.js";
16
19
 
17
20
  /**
18
21
  * Lint a single file and return per-file diagnostics.
@@ -20,15 +23,18 @@ import { discoverFiles } from "./file-discovery.js";
20
23
  * @param eslint - Reusable ESLint instance with correct cwd
21
24
  * @param filePath - Absolute path to the file
22
25
  * @param cwd - Working directory for computing relative paths
26
+ * @param config - Cache configuration
23
27
  * @returns File result with relative path and diagnostics array
24
28
  */
25
- async function lintSingleFile(eslint, filePath, cwd) {
26
- const sourceText = readFileSync(filePath, "utf-8");
27
- const diagnostics = await lintFileForLint(eslint, sourceText, filePath);
28
- return {
29
- filePath: relative(cwd, filePath),
30
- diagnostics,
31
- };
29
+ async function lintSingleFile(eslint, filePath, cwd, config) {
30
+ const sourceText = await readFile(filePath, "utf-8");
31
+ return processWithCache(config, "lint", sourceText, async () => {
32
+ const diagnostics = await lintFileForLint(eslint, sourceText, filePath);
33
+ return {
34
+ filePath: relative(cwd, filePath),
35
+ diagnostics,
36
+ };
37
+ });
32
38
  }
33
39
  /**
34
40
  * Build a LintResult from per-file results, applying a limit to files with issues.
@@ -36,9 +42,10 @@ async function lintSingleFile(eslint, filePath, cwd) {
36
42
  * @param fileResults - All per-file lint results
37
43
  * @param totalFiles - Total number of files that were linted
38
44
  * @param limit - Maximum number of files with issues to include
45
+ * @param missingBarrels - Directories missing barrel files
39
46
  * @returns Aggregated lint result with summary statistics
40
47
  */
41
- function buildLintResult(fileResults, totalFiles, limit) {
48
+ function buildLintResult(fileResults, totalFiles, limit, missingBarrels) {
42
49
  const filesWithIssues = fileResults.filter((f) => f.diagnostics.length > 0);
43
50
  const totalDiagnostics = filesWithIssues.reduce(
44
51
  (sum, f) => sum + f.diagnostics.length,
@@ -48,6 +55,7 @@ function buildLintResult(fileResults, totalFiles, limit) {
48
55
  const truncated = filesWithIssues.length > limit;
49
56
  return {
50
57
  files: cappedFiles,
58
+ ...(missingBarrels.length > 0 ? { missingBarrels } : {}),
51
59
  summary: {
52
60
  totalFiles,
53
61
  filesWithIssues: filesWithIssues.length,
@@ -67,10 +75,17 @@ function buildLintResult(fileResults, totalFiles, limit) {
67
75
  * @param cwd - Working directory for resolving paths
68
76
  * @param limit - Max number of files with issues to include (default 100)
69
77
  * @param gitignore - Whether to respect .gitignore rules (default true)
78
+ * @param config - Cache configuration (default enabled with DEFAULT_CACHE_DIR)
70
79
  * @returns Lint result with per-file diagnostics and summary
71
80
  * @throws {JsdocError} NO_FILES_MATCHED if glob selector matches no files
72
81
  */
73
- export async function lint(selector, cwd, limit = 100, gitignore = true) {
82
+ export async function lint(
83
+ selector,
84
+ cwd,
85
+ limit = 100,
86
+ gitignore = true,
87
+ config = { enabled: true, directory: DEFAULT_CACHE_DIR },
88
+ ) {
74
89
  const files = discoverFiles(selector.pattern, cwd, gitignore);
75
90
  if (files.length === 0 && selector.type === "glob") {
76
91
  throw new JsdocError(
@@ -81,9 +96,10 @@ export async function lint(selector, cwd, limit = 100, gitignore = true) {
81
96
  const tsFiles = files.filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
82
97
  const eslint = createLintLinter(cwd);
83
98
  const fileResults = await Promise.all(
84
- tsFiles.map((f) => lintSingleFile(eslint, f, cwd)),
99
+ tsFiles.map((f) => lintSingleFile(eslint, f, cwd, config)),
85
100
  );
86
- return buildLintResult(fileResults, tsFiles.length, limit);
101
+ const missingBarrels = findMissingBarrels(tsFiles, cwd);
102
+ return buildLintResult(fileResults, tsFiles.length, limit, missingBarrels);
87
103
  }
88
104
  /**
89
105
  * Lint an explicit list of file paths for comprehensive JSDoc quality.
@@ -94,15 +110,22 @@ export async function lint(selector, cwd, limit = 100, gitignore = true) {
94
110
  * @param filePaths - List of absolute file paths to lint
95
111
  * @param cwd - Working directory for computing relative paths
96
112
  * @param limit - Max number of files with issues to include (default 100)
113
+ * @param config - Cache configuration (default enabled with DEFAULT_CACHE_DIR)
97
114
  * @returns Lint result with per-file diagnostics and summary
98
115
  */
99
- export async function lintFiles(filePaths, cwd, limit = 100) {
116
+ export async function lintFiles(
117
+ filePaths,
118
+ cwd,
119
+ limit = 100,
120
+ config = { enabled: true, directory: DEFAULT_CACHE_DIR },
121
+ ) {
100
122
  const tsFiles = filePaths.filter(
101
123
  (f) => f.endsWith(".ts") || f.endsWith(".tsx"),
102
124
  );
103
125
  const eslint = createLintLinter(cwd);
104
126
  const fileResults = await Promise.all(
105
- tsFiles.map((f) => lintSingleFile(eslint, f, cwd)),
127
+ tsFiles.map((f) => lintSingleFile(eslint, f, cwd, config)),
106
128
  );
107
- return buildLintResult(fileResults, tsFiles.length, limit);
129
+ const missingBarrels = findMissingBarrels(tsFiles, cwd);
130
+ return buildLintResult(fileResults, tsFiles.length, limit, missingBarrels);
108
131
  }
@@ -55,7 +55,7 @@ Each file must have exactly one \`@summary\` tag. Remove the extra \`@summary\`
55
55
 
56
56
  Directories with more than 3 TypeScript files should have an \`index.ts\` barrel file. The barrel provides a module-level \`@summary\` and description that helps agents understand what the directory contains before drilling into individual files.
57
57
 
58
- Create an \`index.ts\` that re-exports the directory's public API and add a file-level JSDoc block describing the module's capabilities.`,
58
+ Create an \`index.ts\` that re-exports the directory's public API and add a file-level JSDoc block describing what the child files collectively accomplish — not the re-export mechanism itself.`,
59
59
  missing_description: `### The description paragraph
60
60
 
61
61
  The description is prose that **must appear before the first \`@\` tag** in the file-level JSDoc block. Text placed after tags is not recognized as description content.
@@ -220,10 +220,10 @@ The description is prose that appears before any \`@\` tags. It provides the dee
220
220
 
221
221
  ### Barrel files (index.ts / index.tsx)
222
222
 
223
- Barrel files represent their directory. The \`@summary\` and description describe the module, not individual files.
223
+ Barrel files represent their directory. Their \`@summary\` and description should describe the **cumulative functionality of the directory's children**, not the barrel file itself.
224
224
 
225
- - **\`@summary\`**: What the module does as a unit
226
- - **Description**: The module's capabilities and concernsdescribe concepts, not child filenames
225
+ - **\`@summary\`**: The collective purpose of the files in this directory
226
+ - **Description**: The combined capabilities and responsibilities of child modules what they do together, not their filenames or the re-export mechanism
227
227
 
228
228
  \`\`\`typescript
229
229
  // packages/auth/src/index.ts
@@ -238,9 +238,10 @@ Barrel files represent their directory. The \`@summary\` and description describ
238
238
  \`\`\`
239
239
 
240
240
  **Avoid:**
241
- - Listing child filenames in the description
241
+ - \`@summary Re-exports all functions and types\` — describes the barrel mechanism, not what the children do
242
242
  - \`@summary Exports for the auth module\` — describes the mechanism, not the purpose
243
- - \`@summary Index file\` — no information about what the module does
243
+ - \`@summary Public API barrel\` — names the pattern rather than describing functionality
244
+ - Listing child filenames or saying "This is the entry point"
244
245
 
245
246
  ### Placement
246
247
 
@@ -48,8 +48,9 @@ export function resolveCompilerOptions(filePath) {
48
48
  );
49
49
  // Use cache key based on tsconfig path (or "__default__" if none found)
50
50
  const cacheKey = tsconfigPath ?? "__default__";
51
- if (compilerOptionsCache.has(cacheKey)) {
52
- return compilerOptionsCache.get(cacheKey);
51
+ const cached = compilerOptionsCache.get(cacheKey);
52
+ if (cached) {
53
+ return cached;
53
54
  }
54
55
  let result;
55
56
  if (!tsconfigPath) {
@@ -109,8 +110,9 @@ export function resolveCompilerOptions(filePath) {
109
110
  */
110
111
  export function getLanguageService(tsconfigPath, compilerOptions) {
111
112
  const cacheKey = tsconfigPath ?? "__default__";
112
- if (serviceCache.has(cacheKey)) {
113
- return serviceCache.get(cacheKey);
113
+ const cachedService = serviceCache.get(cacheKey);
114
+ if (cachedService) {
115
+ return cachedService;
114
116
  }
115
117
  // Create a mutable set to track files for this service
116
118
  const files = new Set();
package/dist/types.js CHANGED
@@ -5,6 +5,8 @@
5
5
  *
6
6
  * @summary Shared type definitions for output shapes, error codes, and parsed structures
7
7
  */
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
8
10
  /** Priority order for validation status categories */
9
11
  export const VALIDATION_STATUS_PRIORITY = [
10
12
  "syntax_error",
@@ -29,3 +31,5 @@ export function appendText(existing, addition) {
29
31
  if (existing.length === 0) return addition;
30
32
  return `${existing} ${addition}`;
31
33
  }
34
+ /** Default cache directory under os.tmpdir() */
35
+ export const DEFAULT_CACHE_DIR = join(tmpdir(), ".jsdoczoom-cache");