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 +26 -1
- package/dist/cli.js +39 -21
- package/dist/eslint-plugin.js +2 -14
- package/dist/jsdoc-parser.js +1 -15
- package/dist/lint.js +8 -3
- package/dist/type-declarations.js +166 -70
- package/dist/types.js +15 -0
- package/dist/validate.js +3 -28
- package/package.json +8 -8
- package/types/barrel.d.ts +10 -0
- package/types/jsdoc-parser.d.ts +1 -1
- package/types/type-declarations.d.ts +34 -5
- package/types/types.d.ts +7 -0
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 {
|
|
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, --
|
|
13
|
-
* --no-gitignore), dispatches to drilldown, validation, or lint mode,
|
|
14
|
-
* handles stdin piping. Errors are written to stderr as JSON; validation
|
|
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, --
|
|
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 -
|
|
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
|
-
-
|
|
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
|
|
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 === "--
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
271
|
+
if (checkMode && lintMode) {
|
|
254
272
|
writeError(
|
|
255
|
-
new JsdocError("INVALID_SELECTOR", "Cannot use -
|
|
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
|
-
|
|
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
|
-
|
|
291
|
+
checkMode,
|
|
274
292
|
lintMode,
|
|
275
293
|
pretty,
|
|
276
294
|
limit,
|
package/dist/eslint-plugin.js
CHANGED
|
@@ -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
|
*
|
package/dist/jsdoc-parser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
// Verify the file exists and throw FILE_NOT_FOUND for any read errors
|
|
33
176
|
try {
|
|
34
|
-
|
|
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
|
-
//
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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 =
|
|
203
|
+
let cleaned = dtsFile.text;
|
|
110
204
|
// Remove empty export statement if present and no other exports
|
|
111
|
-
if
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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.
|
|
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.
|
|
46
|
-
"eslint": "^
|
|
47
|
-
"eslint-plugin-jsdoc": "^
|
|
48
|
-
"glob": "^
|
|
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.
|
|
54
|
-
"@types/node": "^
|
|
55
|
-
"vitest": "4.0.
|
|
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[];
|
package/types/jsdoc-parser.d.ts
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
* @
|
|
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;
|