jsdoczoom 0.2.1 → 0.3.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
@@ -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/cli.js CHANGED
@@ -1,5 +1,7 @@
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
7
  import { lint } from "./lint.js";
@@ -9,10 +11,10 @@ import { VALIDATION_STATUS_PRIORITY } from "./types.js";
9
11
  import { validate } 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,7 +26,8 @@ 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
@@ -43,7 +46,7 @@ Stdin:
43
46
  Pipe file paths one per line:
44
47
  find . -name "*.ts" | jsdoczoom
45
48
  find . -name "*.ts" | jsdoczoom @2
46
- find . -name "*.ts" | jsdoczoom -v
49
+ find . -name "*.ts" | jsdoczoom -c
47
50
 
48
51
  Output:
49
52
  JSON items. Items with "next_id" have more detail; use that value as the next
@@ -54,7 +57,7 @@ Barrel gating (glob mode):
54
57
  At depth 3 the barrel disappears and its children appear at depth 1.
55
58
 
56
59
  Modes:
57
- -v Validate file-level structure (has JSDoc block, @summary, description)
60
+ -c Validate file-level structure (has JSDoc block, @summary, description)
58
61
  -l Lint comprehensive JSDoc quality (file-level + function-level tags)
59
62
 
60
63
  Exit codes:
@@ -78,7 +81,8 @@ Workflow:
78
81
  */
79
82
  function parseArgs(args) {
80
83
  let help = false;
81
- let validateMode = false;
84
+ let version = false;
85
+ let checkMode = false;
82
86
  let lintMode = false;
83
87
  let skillMode = false;
84
88
  let pretty = false;
@@ -90,8 +94,10 @@ function parseArgs(args) {
90
94
  const arg = args[i];
91
95
  if (arg === "-h" || arg === "--help") {
92
96
  help = true;
93
- } else if (arg === "-v" || arg === "--validate") {
94
- validateMode = true;
97
+ } else if (arg === "-v" || arg === "--version") {
98
+ version = true;
99
+ } else if (arg === "-c" || arg === "--check") {
100
+ checkMode = true;
95
101
  } else if (arg === "-l" || arg === "--lint") {
96
102
  lintMode = true;
97
103
  } else if (arg === "-s" || arg === "--skill") {
@@ -114,7 +120,8 @@ function parseArgs(args) {
114
120
  }
115
121
  return {
116
122
  help,
117
- validateMode,
123
+ version,
124
+ checkMode,
118
125
  lintMode,
119
126
  skillMode,
120
127
  pretty,
@@ -148,7 +155,7 @@ function extractDepthFromArg(selectorArg) {
148
155
  async function processStdin(
149
156
  stdin,
150
157
  selectorArg,
151
- validateMode,
158
+ checkMode,
152
159
  lintMode,
153
160
  pretty,
154
161
  limit,
@@ -161,7 +168,7 @@ async function processStdin(
161
168
  const { lintFiles } = await import("./lint.js");
162
169
  const result = await lintFiles(stdinPaths, cwd, limit);
163
170
  writeLintResult(result, pretty);
164
- } else if (validateMode) {
171
+ } else if (checkMode) {
165
172
  const { validateFiles } = await import("./validate.js");
166
173
  const result = await validateFiles(stdinPaths, cwd, limit);
167
174
  writeValidationResult(result, pretty);
@@ -175,7 +182,7 @@ async function processStdin(
175
182
  */
176
183
  async function processSelector(
177
184
  selectorArg,
178
- validateMode,
185
+ checkMode,
179
186
  lintMode,
180
187
  pretty,
181
188
  limit,
@@ -188,7 +195,7 @@ async function processSelector(
188
195
  if (lintMode) {
189
196
  const result = await lint(selector, cwd, limit, gitignore);
190
197
  writeLintResult(result, pretty);
191
- } else if (validateMode) {
198
+ } else if (checkMode) {
192
199
  const result = await validate(selector, cwd, limit, gitignore);
193
200
  writeValidationResult(result, pretty);
194
201
  } else {
@@ -218,7 +225,8 @@ export async function main(args, stdin) {
218
225
  try {
219
226
  const {
220
227
  help,
221
- validateMode,
228
+ version,
229
+ checkMode,
222
230
  lintMode,
223
231
  skillMode,
224
232
  pretty,
@@ -231,6 +239,16 @@ export async function main(args, stdin) {
231
239
  process.stdout.write(HELP_TEXT);
232
240
  return;
233
241
  }
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`);
250
+ return;
251
+ }
234
252
  if (skillMode) {
235
253
  process.stdout.write(SKILL_TEXT);
236
254
  return;
@@ -250,9 +268,9 @@ export async function main(args, stdin) {
250
268
  }
251
269
  return;
252
270
  }
253
- if (validateMode && lintMode) {
271
+ if (checkMode && lintMode) {
254
272
  writeError(
255
- new JsdocError("INVALID_SELECTOR", "Cannot use -v and -l together"),
273
+ new JsdocError("INVALID_SELECTOR", "Cannot use -c and -l together"),
256
274
  );
257
275
  return;
258
276
  }
@@ -261,7 +279,7 @@ export async function main(args, stdin) {
261
279
  await processStdin(
262
280
  stdin,
263
281
  selectorArg,
264
- validateMode,
282
+ checkMode,
265
283
  lintMode,
266
284
  pretty,
267
285
  limit,
@@ -270,7 +288,7 @@ export async function main(args, stdin) {
270
288
  } else {
271
289
  await processSelector(
272
290
  selectorArg,
273
- validateMode,
291
+ checkMode,
274
292
  lintMode,
275
293
  pretty,
276
294
  limit,
@@ -1,3 +1,5 @@
1
+ import { appendText, DESCRIPTION_TAGS } from "./types.js";
2
+
1
3
  /**
2
4
  * Custom ESLint plugin for jsdoczoom file-level JSDoc validation.
3
5
  *
@@ -8,13 +10,6 @@
8
10
  *
9
11
  * @summary Custom ESLint plugin with file-level JSDoc validation rules
10
12
  */
11
- /** Tags whose content is treated as description (free-text). */
12
- const DESCRIPTION_TAGS = new Set([
13
- "desc",
14
- "description",
15
- "file",
16
- "fileoverview",
17
- ]);
18
13
  /**
19
14
  * Find the file-level JSDoc block comment from the source code.
20
15
  *
@@ -51,13 +46,6 @@ function findFileJsdocComment(sourceCode, programBody) {
51
46
  // If no non-import statements, return first JSDoc comment in file
52
47
  return jsdocComments[0] ?? null;
53
48
  }
54
- /**
55
- * Append non-empty text to an accumulator with space separation.
56
- */
57
- function appendText(existing, addition) {
58
- if (!addition) return existing;
59
- return existing ? `${existing} ${addition}` : addition;
60
- }
61
49
  /**
62
50
  * Parse the inner content of a JSDoc block comment.
63
51
  *
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import ts from "typescript";
3
3
  import { JsdocError } from "./errors.js";
4
+ import { appendText, DESCRIPTION_TAGS } from "./types.js";
4
5
  /**
5
6
  * Uses the TypeScript compiler to locate the first JSDoc block before any
6
7
  * code statements, then extracts the first `@summary` tag and free-text
@@ -123,21 +124,6 @@ function getDiagnostics(sourceFile) {
123
124
  if (!diags || diags.length === 0) return [];
124
125
  return diags.map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"));
125
126
  }
126
- /**
127
- * Append non-empty text to an accumulator with space separation.
128
- */
129
- function appendText(existing, addition) {
130
- if (addition.length === 0) return existing;
131
- if (existing.length === 0) return addition;
132
- return `${existing} ${addition}`;
133
- }
134
- /** Tags whose content is treated as description (free-text). */
135
- const DESCRIPTION_TAGS = new Set([
136
- "desc",
137
- "description",
138
- "file",
139
- "fileoverview",
140
- ]);
141
127
  /**
142
128
  * Parse @summary tag and free-text description from raw JSDoc inner text.
143
129
  *
package/dist/lint.js CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { readFileSync } from "node:fs";
12
12
  import { relative } from "node:path";
13
+ import { findMissingBarrels } from "./barrel.js";
13
14
  import { JsdocError } from "./errors.js";
14
15
  import { createLintLinter, lintFileForLint } from "./eslint-engine.js";
15
16
  import { discoverFiles } from "./file-discovery.js";
@@ -36,9 +37,10 @@ async function lintSingleFile(eslint, filePath, cwd) {
36
37
  * @param fileResults - All per-file lint results
37
38
  * @param totalFiles - Total number of files that were linted
38
39
  * @param limit - Maximum number of files with issues to include
40
+ * @param missingBarrels - Directories missing barrel files
39
41
  * @returns Aggregated lint result with summary statistics
40
42
  */
41
- function buildLintResult(fileResults, totalFiles, limit) {
43
+ function buildLintResult(fileResults, totalFiles, limit, missingBarrels) {
42
44
  const filesWithIssues = fileResults.filter((f) => f.diagnostics.length > 0);
43
45
  const totalDiagnostics = filesWithIssues.reduce(
44
46
  (sum, f) => sum + f.diagnostics.length,
@@ -48,6 +50,7 @@ function buildLintResult(fileResults, totalFiles, limit) {
48
50
  const truncated = filesWithIssues.length > limit;
49
51
  return {
50
52
  files: cappedFiles,
53
+ ...(missingBarrels.length > 0 ? { missingBarrels } : {}),
51
54
  summary: {
52
55
  totalFiles,
53
56
  filesWithIssues: filesWithIssues.length,
@@ -83,7 +86,8 @@ export async function lint(selector, cwd, limit = 100, gitignore = true) {
83
86
  const fileResults = await Promise.all(
84
87
  tsFiles.map((f) => lintSingleFile(eslint, f, cwd)),
85
88
  );
86
- return buildLintResult(fileResults, tsFiles.length, limit);
89
+ const missingBarrels = findMissingBarrels(tsFiles, cwd);
90
+ return buildLintResult(fileResults, tsFiles.length, limit, missingBarrels);
87
91
  }
88
92
  /**
89
93
  * Lint an explicit list of file paths for comprehensive JSDoc quality.
@@ -104,5 +108,6 @@ export async function lintFiles(filePaths, cwd, limit = 100) {
104
108
  const fileResults = await Promise.all(
105
109
  tsFiles.map((f) => lintSingleFile(eslint, f, cwd)),
106
110
  );
107
- return buildLintResult(fileResults, tsFiles.length, limit);
111
+ const missingBarrels = findMissingBarrels(tsFiles, cwd);
112
+ return buildLintResult(fileResults, tsFiles.length, limit, missingBarrels);
108
113
  }
@@ -1,6 +1,8 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
2
3
  import ts from "typescript";
3
4
  import { JsdocError } from "./errors.js";
5
+
4
6
  /**
5
7
  * Produces .d.ts-like output from a TypeScript source file using the
6
8
  * TypeScript compiler's declaration emit. Preserves JSDoc comments and
@@ -9,6 +11,147 @@ import { JsdocError } from "./errors.js";
9
11
  *
10
12
  * @summary Generate TypeScript declaration output from source files
11
13
  */
14
+ // Cache for resolved compiler options, keyed by tsconfig path
15
+ const compilerOptionsCache = new Map();
16
+ // Shared document registry for caching parsed source files across language services
17
+ const documentRegistry = ts.createDocumentRegistry();
18
+ // Cache for language services, keyed by tsconfig path
19
+ const serviceCache = new Map();
20
+ /**
21
+ * Resolves compiler options for a given file path by finding and parsing
22
+ * the nearest tsconfig.json file.
23
+ *
24
+ * @internal
25
+ * @param filePath - Absolute path to the TypeScript source file
26
+ * @returns Object containing the tsconfig path and resolved compiler options
27
+ */
28
+ export function resolveCompilerOptions(filePath) {
29
+ // Required overrides that must always be present
30
+ const requiredOverrides = {
31
+ declaration: true,
32
+ emitDeclarationOnly: true,
33
+ removeComments: false,
34
+ skipLibCheck: true,
35
+ };
36
+ // Fallback defaults when no tsconfig is found
37
+ const fallbackDefaults = {
38
+ ...requiredOverrides,
39
+ target: ts.ScriptTarget.Latest,
40
+ module: ts.ModuleKind.NodeNext,
41
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
42
+ };
43
+ // Try to find the nearest tsconfig.json
44
+ const tsconfigPath = ts.findConfigFile(
45
+ dirname(filePath),
46
+ ts.sys.fileExists,
47
+ "tsconfig.json",
48
+ );
49
+ // Use cache key based on tsconfig path (or "__default__" if none found)
50
+ const cacheKey = tsconfigPath ?? "__default__";
51
+ const cached = compilerOptionsCache.get(cacheKey);
52
+ if (cached) {
53
+ return cached;
54
+ }
55
+ let result;
56
+ if (!tsconfigPath) {
57
+ // No tsconfig found - use fallback defaults
58
+ result = {
59
+ tsconfigPath: null,
60
+ options: fallbackDefaults,
61
+ };
62
+ } else {
63
+ // Try to parse the tsconfig
64
+ try {
65
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
66
+ if (configFile.error) {
67
+ // Malformed JSON - fall back to defaults
68
+ result = {
69
+ tsconfigPath: null,
70
+ options: fallbackDefaults,
71
+ };
72
+ } else {
73
+ // Parse the config content
74
+ const parsedConfig = ts.parseJsonConfigFileContent(
75
+ configFile.config,
76
+ ts.sys,
77
+ dirname(tsconfigPath),
78
+ );
79
+ // Merge parsed options with required overrides
80
+ result = {
81
+ tsconfigPath,
82
+ options: {
83
+ ...parsedConfig.options,
84
+ ...requiredOverrides,
85
+ },
86
+ };
87
+ }
88
+ } catch (_error) {
89
+ // Error reading or parsing tsconfig - fall back to defaults
90
+ // Note: This catches file system errors (EACCES, ENOENT) and any unexpected
91
+ // errors from ts.readConfigFile or ts.parseJsonConfigFileContent
92
+ result = {
93
+ tsconfigPath: null,
94
+ options: fallbackDefaults,
95
+ };
96
+ }
97
+ }
98
+ compilerOptionsCache.set(cacheKey, result);
99
+ return result;
100
+ }
101
+ /**
102
+ * Gets or creates a cached language service for the given tsconfig and compiler options.
103
+ * Language services are shared across files in the same project (same tsconfig path) and
104
+ * reuse parsed source files via the document registry.
105
+ *
106
+ * @internal
107
+ * @param tsconfigPath - Path to the tsconfig.json file, or null if using defaults
108
+ * @param compilerOptions - Resolved compiler options for this project
109
+ * @returns Object containing the language service and mutable set of files
110
+ */
111
+ export function getLanguageService(tsconfigPath, compilerOptions) {
112
+ const cacheKey = tsconfigPath ?? "__default__";
113
+ const cachedService = serviceCache.get(cacheKey);
114
+ if (cachedService) {
115
+ return cachedService;
116
+ }
117
+ // Create a mutable set to track files for this service
118
+ const files = new Set();
119
+ // Create a language service host
120
+ const host = {
121
+ getScriptFileNames: () => Array.from(files),
122
+ getScriptVersion: (_fileName) => "0", // Static version - no watch mode
123
+ getScriptSnapshot: (fileName) => {
124
+ const content = ts.sys.readFile(fileName);
125
+ if (content === undefined) {
126
+ return undefined;
127
+ }
128
+ return ts.ScriptSnapshot.fromString(content);
129
+ },
130
+ getCompilationSettings: () => compilerOptions,
131
+ getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
132
+ getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
133
+ fileExists: ts.sys.fileExists,
134
+ readFile: ts.sys.readFile,
135
+ readDirectory: ts.sys.readDirectory,
136
+ directoryExists: ts.sys.directoryExists,
137
+ getDirectories: ts.sys.getDirectories,
138
+ };
139
+ // Create the language service with the document registry for caching
140
+ const service = ts.createLanguageService(host, documentRegistry);
141
+ const cacheEntry = { service, files };
142
+ serviceCache.set(cacheKey, cacheEntry);
143
+ return cacheEntry;
144
+ }
145
+ /**
146
+ * Resets all caches (compiler options, language services, and document registry).
147
+ * Used for test isolation to ensure a clean state between test runs.
148
+ *
149
+ * @internal
150
+ */
151
+ export function resetCache() {
152
+ compilerOptionsCache.clear();
153
+ serviceCache.clear();
154
+ }
12
155
  /**
13
156
  * Generates TypeScript declaration output from a source file.
14
157
  *
@@ -29,86 +172,39 @@ import { JsdocError } from "./errors.js";
29
172
  * @throws {JsdocError} If the file cannot be read or parsed
30
173
  */
31
174
  export function generateTypeDeclarations(filePath) {
32
- let sourceText;
175
+ // Verify the file exists and throw FILE_NOT_FOUND for any read errors
33
176
  try {
34
- sourceText = readFileSync(filePath, "utf-8");
177
+ readFileSync(filePath, "utf-8");
35
178
  } catch (_error) {
36
179
  throw new JsdocError("FILE_NOT_FOUND", `Failed to read file: ${filePath}`);
37
180
  }
38
- // Create compiler options matching the project setup
39
- const compilerOptions = {
40
- declaration: true,
41
- emitDeclarationOnly: true,
42
- target: ts.ScriptTarget.Latest,
43
- module: ts.ModuleKind.NodeNext,
44
- moduleResolution: ts.ModuleResolutionKind.NodeNext,
45
- skipLibCheck: true,
46
- removeComments: false, // Preserve JSDoc comments
47
- };
48
- // Create a custom compiler host that provides our file
49
- const host = ts.createCompilerHost(compilerOptions);
50
- const originalGetSourceFile = host.getSourceFile;
51
- host.getSourceFile = (
52
- fileName,
53
- languageVersion,
54
- onError,
55
- shouldCreateNewSourceFile,
56
- ) => {
57
- if (fileName === filePath) {
58
- return ts.createSourceFile(fileName, sourceText, languageVersion, true);
59
- }
60
- return originalGetSourceFile.call(
61
- host,
62
- fileName,
63
- languageVersion,
64
- onError,
65
- shouldCreateNewSourceFile,
66
- );
67
- };
68
- // Capture emitted output
69
- let declarationOutput = "";
70
- host.writeFile = (fileName, data) => {
71
- if (fileName.endsWith(".d.ts")) {
72
- declarationOutput = data;
73
- }
74
- };
75
- // Create program and emit declarations
76
- const program = ts.createProgram([filePath], compilerOptions, host);
77
- const sourceFile = program.getSourceFile(filePath);
78
- if (!sourceFile) {
79
- throw new JsdocError("PARSE_ERROR", `Failed to parse file: ${filePath}`);
80
- }
81
- const emitResult = program.emit(sourceFile, undefined, undefined, true);
82
- // Check for emit errors
83
- const diagnostics = ts
84
- .getPreEmitDiagnostics(program)
85
- .concat(emitResult.diagnostics);
181
+ // Resolve compiler options from nearest tsconfig.json
182
+ const { tsconfigPath, options } = resolveCompilerOptions(filePath);
183
+ // Get or create a cached language service for this project
184
+ const { service, files } = getLanguageService(tsconfigPath, options);
185
+ // Register the file with the language service
186
+ files.add(filePath);
187
+ // Check for parse errors first
188
+ const diagnostics = service.getSyntacticDiagnostics(filePath);
86
189
  if (diagnostics.length > 0) {
87
- const errors = diagnostics
88
- .map((diagnostic) => {
89
- if (diagnostic.file && diagnostic.start !== undefined) {
90
- const { line, character } =
91
- diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
92
- const message = ts.flattenDiagnosticMessageText(
93
- diagnostic.messageText,
94
- "\n",
95
- );
96
- return `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`;
97
- }
98
- return ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
99
- })
100
- .join("\n");
101
- throw new JsdocError("PARSE_ERROR", `TypeScript errors:\n${errors}`);
190
+ throw new JsdocError("PARSE_ERROR", `Failed to parse file: ${filePath}`);
102
191
  }
103
- if (!declarationOutput) {
104
- // If no output was generated, the file may have no exports
105
- // Return empty string in this case
192
+ // Get emit output using the language service
193
+ const emitOutput = service.getEmitOutput(filePath, true); // true = emitOnlyDtsFiles
194
+ // Find the .d.ts output file
195
+ const dtsFile = emitOutput.outputFiles.find((file) =>
196
+ file.name.endsWith(".d.ts"),
197
+ );
198
+ if (!dtsFile) {
199
+ // No declaration output - file has no exports
106
200
  return "";
107
201
  }
108
202
  // Clean up the output
109
- let cleaned = declarationOutput;
203
+ let cleaned = dtsFile.text;
110
204
  // Remove empty export statement if present and no other exports
111
- if (cleaned.trim() === "export {};") {
205
+ // Strip out any leading comments first to check if the only actual code is "export {};"
206
+ const withoutComments = cleaned.replace(/\/\*\*[\s\S]*?\*\//g, "").trim();
207
+ if (withoutComments === "export {};") {
112
208
  cleaned = "";
113
209
  }
114
210
  return cleaned;
package/dist/types.js CHANGED
@@ -14,3 +14,18 @@ export const VALIDATION_STATUS_PRIORITY = [
14
14
  "missing_description",
15
15
  "missing_barrel",
16
16
  ];
17
+ /** Tags whose content is treated as description (free-text). */
18
+ export const DESCRIPTION_TAGS = new Set([
19
+ "desc",
20
+ "description",
21
+ "file",
22
+ "fileoverview",
23
+ ]);
24
+ /**
25
+ * Append non-empty text to an accumulator with space separation.
26
+ */
27
+ export function appendText(existing, addition) {
28
+ if (addition.length === 0) return existing;
29
+ if (existing.length === 0) return addition;
30
+ return `${existing} ${addition}`;
31
+ }
package/dist/validate.js CHANGED
@@ -1,6 +1,6 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { dirname, join, relative } from "node:path";
3
- import { isBarrel } from "./barrel.js";
1
+ import { readFileSync } from "node:fs";
2
+ import { relative } from "node:path";
3
+ import { findMissingBarrels } from "./barrel.js";
4
4
  import { JsdocError } from "./errors.js";
5
5
  import {
6
6
  createValidationLinter,
@@ -29,31 +29,6 @@ async function classifyFile(eslint, filePath, cwd) {
29
29
  const status = mapToValidationStatus(messages);
30
30
  return { path: relativePath, status };
31
31
  }
32
- /** Minimum number of .ts/.tsx files in a directory to require a barrel. */
33
- const BARREL_THRESHOLD = 3;
34
- /**
35
- * Find directories with more than BARREL_THRESHOLD .ts/.tsx files
36
- * that lack a barrel file (index.ts or index.tsx).
37
- */
38
- function findMissingBarrels(filePaths, cwd) {
39
- const dirCounts = new Map();
40
- for (const filePath of filePaths) {
41
- if (isBarrel(filePath)) continue;
42
- const dir = dirname(filePath);
43
- dirCounts.set(dir, (dirCounts.get(dir) ?? 0) + 1);
44
- }
45
- const missing = [];
46
- for (const [dir, count] of dirCounts) {
47
- if (count <= BARREL_THRESHOLD) continue;
48
- const hasBarrel =
49
- existsSync(join(dir, "index.ts")) || existsSync(join(dir, "index.tsx"));
50
- if (!hasBarrel) {
51
- const rel = relative(cwd, dir) || ".";
52
- missing.push(rel);
53
- }
54
- }
55
- return missing.sort();
56
- }
57
32
  /**
58
33
  * Group file statuses into a ValidationResult, applying a limit
59
34
  * to the total number of invalid file paths shown.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsdoczoom",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "CLI tool for extracting JSDoc summaries at configurable depths",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -42,16 +42,16 @@
42
42
  "release:dry-run": "bash ../../scripts/release-package.sh jsdoczoom --dry-run"
43
43
  },
44
44
  "dependencies": {
45
- "@typescript-eslint/parser": "^8.21.0",
46
- "eslint": "^9.20.0",
47
- "eslint-plugin-jsdoc": "^50.6.4",
48
- "glob": "^11.0.0",
45
+ "@typescript-eslint/parser": "^8.55.0",
46
+ "eslint": "^10.0.0",
47
+ "eslint-plugin-jsdoc": "^62.5.5",
48
+ "glob": "^13.0.3",
49
49
  "ignore": "^7.0.5",
50
50
  "typescript": "^5.9.3"
51
51
  },
52
52
  "devDependencies": {
53
- "@biomejs/biome": "2.3.14",
54
- "@types/node": "^24",
55
- "vitest": "4.0.13"
53
+ "@biomejs/biome": "2.4.1",
54
+ "@types/node": "^25.2.3",
55
+ "vitest": "4.0.18"
56
56
  }
57
57
  }
package/types/barrel.d.ts CHANGED
@@ -33,3 +33,13 @@ export declare function getBarrelChildren(
33
33
  barrelPath: string,
34
34
  _cwd: string,
35
35
  ): string[];
36
+ /** Minimum number of .ts/.tsx files in a directory to require a barrel. */
37
+ export declare const BARREL_THRESHOLD = 3;
38
+ /**
39
+ * Find directories with more than BARREL_THRESHOLD .ts/.tsx files
40
+ * that lack a barrel file (index.ts or index.tsx).
41
+ */
42
+ export declare function findMissingBarrels(
43
+ filePaths: string[],
44
+ cwd: string,
45
+ ): string[];
@@ -1,4 +1,4 @@
1
- import type { ParsedFileInfo } from "./types.js";
1
+ import { type ParsedFileInfo } from "./types.js";
2
2
  /**
3
3
  * Uses the TypeScript compiler to locate the first JSDoc block before any
4
4
  * code statements, then extracts the first `@summary` tag and free-text
@@ -1,11 +1,40 @@
1
+ import ts from "typescript";
1
2
  /**
2
- * Produces .d.ts-like output from a TypeScript source file using the
3
- * TypeScript compiler's declaration emit. Preserves JSDoc comments and
4
- * source order while stripping implementation bodies and non-exported
5
- * internals.
3
+ * Resolves compiler options for a given file path by finding and parsing
4
+ * the nearest tsconfig.json file.
6
5
  *
7
- * @summary Generate TypeScript declaration output from source files
6
+ * @internal
7
+ * @param filePath - Absolute path to the TypeScript source file
8
+ * @returns Object containing the tsconfig path and resolved compiler options
9
+ */
10
+ export declare function resolveCompilerOptions(filePath: string): {
11
+ tsconfigPath: string | null;
12
+ options: ts.CompilerOptions;
13
+ };
14
+ /**
15
+ * Gets or creates a cached language service for the given tsconfig and compiler options.
16
+ * Language services are shared across files in the same project (same tsconfig path) and
17
+ * reuse parsed source files via the document registry.
18
+ *
19
+ * @internal
20
+ * @param tsconfigPath - Path to the tsconfig.json file, or null if using defaults
21
+ * @param compilerOptions - Resolved compiler options for this project
22
+ * @returns Object containing the language service and mutable set of files
23
+ */
24
+ export declare function getLanguageService(
25
+ tsconfigPath: string | null,
26
+ compilerOptions: ts.CompilerOptions,
27
+ ): {
28
+ service: ts.LanguageService;
29
+ files: Set<string>;
30
+ };
31
+ /**
32
+ * Resets all caches (compiler options, language services, and document registry).
33
+ * Used for test isolation to ensure a clean state between test runs.
34
+ *
35
+ * @internal
8
36
  */
37
+ export declare function resetCache(): void;
9
38
  /**
10
39
  * Generates TypeScript declaration output from a source file.
11
40
  *
package/types/types.d.ts CHANGED
@@ -81,6 +81,12 @@ export interface SelectorInfo {
81
81
  pattern: string;
82
82
  depth: number | undefined;
83
83
  }
84
+ /** Tags whose content is treated as description (free-text). */
85
+ export declare const DESCRIPTION_TAGS: Set<string>;
86
+ /**
87
+ * Append non-empty text to an accumulator with space separation.
88
+ */
89
+ export declare function appendText(existing: string, addition: string): string;
84
90
  /** A single lint diagnostic from ESLint */
85
91
  export interface LintDiagnostic {
86
92
  line: number;
@@ -98,6 +104,7 @@ export interface LintFileResult {
98
104
  /** Overall lint result with summary statistics */
99
105
  export interface LintResult {
100
106
  files: LintFileResult[];
107
+ missingBarrels?: string[];
101
108
  summary: {
102
109
  totalFiles: number;
103
110
  filesWithIssues: number;