jsdoczoom 1.0.0 → 1.2.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 +47 -28
- package/dist/cli.js +192 -64
- package/dist/drilldown.js +12 -7
- package/dist/file-discovery.js +15 -10
- package/dist/index.js +6 -1
- package/dist/jsdoc-parser.js +3 -3
- package/dist/lint.js +3 -3
- package/dist/search.js +226 -0
- package/dist/text-format.js +3 -3
- package/dist/type-declarations.js +164 -33
- package/dist/validate.js +3 -3
- package/package.json +1 -1
- package/types/barrel.d.ts +2 -2
- package/types/file-discovery.d.ts +2 -2
- package/types/index.d.ts +6 -1
- package/types/jsdoc-parser.d.ts +3 -1
- package/types/search.d.ts +44 -0
- package/types/type-declarations.d.ts +53 -1
package/dist/barrel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { access, readdir } from "node:fs/promises";
|
|
2
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.
|
|
@@ -34,29 +34,34 @@ export function isBarrel(filePath) {
|
|
|
34
34
|
* @param _cwd - Working directory (unused, kept for API consistency)
|
|
35
35
|
* @returns Sorted array of absolute paths to child files
|
|
36
36
|
*/
|
|
37
|
-
export function getBarrelChildren(barrelPath, _cwd) {
|
|
37
|
+
export async function getBarrelChildren(barrelPath, _cwd) {
|
|
38
38
|
const dir = dirname(barrelPath);
|
|
39
39
|
const barrelName = basename(barrelPath);
|
|
40
|
-
let
|
|
40
|
+
let rawEntries;
|
|
41
41
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
42
|
+
const dirents = await readdir(dir, { withFileTypes: true });
|
|
43
|
+
rawEntries = dirents.map((e) => ({
|
|
44
|
+
name: e.name,
|
|
45
|
+
isDirectory: e.isDirectory(),
|
|
46
|
+
}));
|
|
47
|
+
} catch {
|
|
48
|
+
// Directory unreadable (permissions, deleted, etc.) — no children discoverable
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const entries = (
|
|
52
|
+
await Promise.all(
|
|
53
|
+
rawEntries.map(async (entry) => {
|
|
45
54
|
if (entry.isDirectory) {
|
|
46
|
-
|
|
47
|
-
const childBarrel = findChildBarrel(resolve(dir, entry.name));
|
|
55
|
+
const childBarrel = await findChildBarrel(resolve(dir, entry.name));
|
|
48
56
|
return childBarrel ? [childBarrel] : [];
|
|
49
57
|
}
|
|
50
|
-
// Sibling file: must be .ts/.tsx, not .d.ts, not the barrel itself
|
|
51
58
|
if (isTsFile(entry.name) && entry.name !== barrelName) {
|
|
52
59
|
return [resolve(dir, entry.name)];
|
|
53
60
|
}
|
|
54
61
|
return [];
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return [];
|
|
59
|
-
}
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
).flat();
|
|
60
65
|
return entries.sort();
|
|
61
66
|
}
|
|
62
67
|
/**
|
|
@@ -72,16 +77,21 @@ function isTsFile(name) {
|
|
|
72
77
|
* index.ts takes priority over index.tsx.
|
|
73
78
|
* Returns the absolute path to the barrel, or null if none found.
|
|
74
79
|
*/
|
|
75
|
-
function findChildBarrel(subdirPath) {
|
|
80
|
+
async function findChildBarrel(subdirPath) {
|
|
76
81
|
const tsPath = resolve(subdirPath, "index.ts");
|
|
77
|
-
|
|
82
|
+
try {
|
|
83
|
+
await access(tsPath);
|
|
78
84
|
return tsPath;
|
|
85
|
+
} catch {
|
|
86
|
+
// no index.ts
|
|
79
87
|
}
|
|
80
88
|
const tsxPath = resolve(subdirPath, "index.tsx");
|
|
81
|
-
|
|
89
|
+
try {
|
|
90
|
+
await access(tsxPath);
|
|
82
91
|
return tsxPath;
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
83
94
|
}
|
|
84
|
-
return null;
|
|
85
95
|
}
|
|
86
96
|
/** Minimum number of .ts/.tsx files in a directory to require a barrel. */
|
|
87
97
|
export const BARREL_THRESHOLD = 3;
|
|
@@ -89,7 +99,7 @@ export const BARREL_THRESHOLD = 3;
|
|
|
89
99
|
* Find directories with more than BARREL_THRESHOLD .ts/.tsx files
|
|
90
100
|
* that lack a barrel file (index.ts or index.tsx).
|
|
91
101
|
*/
|
|
92
|
-
export function findMissingBarrels(filePaths, cwd) {
|
|
102
|
+
export async function findMissingBarrels(filePaths, cwd) {
|
|
93
103
|
const dirCounts = new Map();
|
|
94
104
|
for (const filePath of filePaths) {
|
|
95
105
|
if (isBarrel(filePath)) continue;
|
|
@@ -97,14 +107,23 @@ export function findMissingBarrels(filePaths, cwd) {
|
|
|
97
107
|
dirCounts.set(dir, (dirCounts.get(dir) ?? 0) + 1);
|
|
98
108
|
}
|
|
99
109
|
const missing = [];
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
await Promise.all(
|
|
111
|
+
[...dirCounts.entries()].map(async ([dir, count]) => {
|
|
112
|
+
if (count <= BARREL_THRESHOLD) return;
|
|
113
|
+
const [tsExists, tsxExists] = await Promise.all([
|
|
114
|
+
access(join(dir, "index.ts")).then(
|
|
115
|
+
() => true,
|
|
116
|
+
() => false,
|
|
117
|
+
),
|
|
118
|
+
access(join(dir, "index.tsx")).then(
|
|
119
|
+
() => true,
|
|
120
|
+
() => false,
|
|
121
|
+
),
|
|
122
|
+
]);
|
|
123
|
+
if (!tsExists && !tsxExists) {
|
|
124
|
+
missing.push(relative(cwd, dir) || ".");
|
|
125
|
+
}
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
109
128
|
return missing.sort();
|
|
110
129
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
3
|
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 { discoverFiles } from "./file-discovery.js";
|
|
7
8
|
import { lint, lintFiles } from "./lint.js";
|
|
9
|
+
import { search, searchFiles } from "./search.js";
|
|
8
10
|
import { parseSelector } from "./selector.js";
|
|
9
11
|
import { RULE_EXPLANATIONS, SKILL_TEXT } from "./skill-text.js";
|
|
10
12
|
import { formatTextOutput } from "./text-format.js";
|
|
@@ -28,48 +30,54 @@ Each file has four detail levels (1-indexed): @1 summary, @2 description,
|
|
|
28
30
|
Options:
|
|
29
31
|
-h, --help Show this help text
|
|
30
32
|
-v, --version Show version number
|
|
31
|
-
-c, --check
|
|
32
|
-
-l, --lint
|
|
33
|
+
-c, --check Validate file-level structure (has JSDoc block, @summary, description)
|
|
34
|
+
-l, --lint Lint comprehensive JSDoc quality (file-level + function-level tags)
|
|
33
35
|
-s, --skill Print JSDoc writing guidelines
|
|
34
36
|
--json Output as JSON (default is plain text)
|
|
35
37
|
--pretty Format JSON output with 2-space indent (use with --json)
|
|
36
38
|
--limit N Max results shown (default 500)
|
|
37
39
|
--no-gitignore Include files ignored by .gitignore
|
|
40
|
+
--search <query> Search files by regex pattern
|
|
38
41
|
--disable-cache Skip all cache operations
|
|
39
42
|
--cache-directory Override cache directory (default: system temp)
|
|
40
43
|
--explain-rule R Explain a lint rule with examples (e.g. jsdoc/informative-docs)
|
|
41
44
|
|
|
42
45
|
Selector:
|
|
43
46
|
A glob pattern or file path, optionally with @depth suffix (1-4).
|
|
47
|
+
|
|
44
48
|
Examples:
|
|
45
49
|
jsdoczoom src/**/*.ts # All .ts files at depth 1 (summary)
|
|
46
|
-
jsdoczoom src/
|
|
50
|
+
jsdoczoom src/foo.ts@2 # Single file at depth 2 (description)
|
|
47
51
|
jsdoczoom **/*.ts@3 # All .ts files at depth 3 (type decls)
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
Search (--search):
|
|
54
|
+
Searches all **/*.{ts,tsx} files (or a selector's file set) by regex.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
jsdoczoom --search "CacheConfig" # find where CacheConfig is used
|
|
58
|
+
jsdoczoom --search "auth-loader" # searches file name
|
|
59
|
+
jsdoczoom src/*.ts --search "TODO|FIXME" # restrict to a file subset
|
|
54
60
|
|
|
55
61
|
Output:
|
|
56
62
|
Plain text by default. Each item has a "# path@depth" header followed by
|
|
57
63
|
content. Use the header value as the next selector to drill deeper.
|
|
58
|
-
|
|
64
|
+
|
|
65
|
+
Use --json for machine-parseable JSON output, use "next_id" to drill deeper.
|
|
59
66
|
|
|
60
67
|
Type declarations (@3) include source line annotations (// LN or // LN-LM)
|
|
61
68
|
so you can locate the implementation in the source file.
|
|
62
69
|
|
|
70
|
+
Stdin:
|
|
71
|
+
Pipe file paths one per line (useful with -c/-l for targeted validation):
|
|
72
|
+
git diff --name-only | jsdoczoom -c # validate changed files
|
|
73
|
+
cat filelist.txt | jsdoczoom -l # lint a specific set of files
|
|
74
|
+
|
|
63
75
|
Barrel gating (glob mode):
|
|
64
76
|
A barrel's @summary and description reflect the cumulative functionality
|
|
65
77
|
of its directory's children, not the barrel file itself. Barrels with a
|
|
66
78
|
@summary gate sibling files at depths 1-2. At depth 3 the barrel
|
|
67
79
|
disappears and its children appear at depth 1.
|
|
68
80
|
|
|
69
|
-
Modes:
|
|
70
|
-
-c Validate file-level structure (has JSDoc block, @summary, description)
|
|
71
|
-
-l Lint comprehensive JSDoc quality (file-level + function-level tags)
|
|
72
|
-
|
|
73
81
|
Exit codes:
|
|
74
82
|
0 Success (all files pass)
|
|
75
83
|
1 Runtime error (invalid arguments, missing files)
|
|
@@ -79,12 +87,7 @@ Workflow:
|
|
|
79
87
|
$ jsdoczoom src/**/*.ts # list summaries
|
|
80
88
|
$ jsdoczoom src/utils@2 # drill into description
|
|
81
89
|
$ jsdoczoom src/utils@3 # see type declarations
|
|
82
|
-
|
|
83
|
-
Pipe examples:
|
|
84
|
-
$ jsdoczoom src/utils.ts@3 | grep "functionName" # find symbol + source line
|
|
85
|
-
$ jsdoczoom src/utils.ts@3 | grep "// L" # list all declarations with lines
|
|
86
|
-
$ jsdoczoom src/**/*.ts | grep "^#" # list all file headers
|
|
87
|
-
$ grep -rl "term" src/ --include="*.ts" | jsdoczoom # describe matching files
|
|
90
|
+
$ jsdoczoom src/**/*.ts | grep "^#" # list all file headers
|
|
88
91
|
`;
|
|
89
92
|
/**
|
|
90
93
|
* Parse a flag that requires a value argument.
|
|
@@ -111,6 +114,8 @@ function parseArgs(args) {
|
|
|
111
114
|
cacheDirectory: undefined,
|
|
112
115
|
explainRule: undefined,
|
|
113
116
|
selectorArg: undefined,
|
|
117
|
+
extraArgs: [],
|
|
118
|
+
searchQuery: undefined,
|
|
114
119
|
};
|
|
115
120
|
for (let i = 0; i < args.length; i++) {
|
|
116
121
|
const arg = args[i];
|
|
@@ -170,6 +175,15 @@ function parseArgs(args) {
|
|
|
170
175
|
i = nextIndex;
|
|
171
176
|
continue;
|
|
172
177
|
}
|
|
178
|
+
if (arg === "--search") {
|
|
179
|
+
const { value, nextIndex } = parseValueFlag(args, i);
|
|
180
|
+
if (value === undefined) {
|
|
181
|
+
throw new JsdocError("INVALID_SELECTOR", "--search requires a value");
|
|
182
|
+
}
|
|
183
|
+
parsed.searchQuery = value;
|
|
184
|
+
i = nextIndex;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
173
187
|
// Unknown flag or positional arg
|
|
174
188
|
if (arg.startsWith("-")) {
|
|
175
189
|
throw new JsdocError("INVALID_SELECTOR", `Unrecognized option: ${arg}`);
|
|
@@ -177,6 +191,8 @@ function parseArgs(args) {
|
|
|
177
191
|
// Positional selector arg
|
|
178
192
|
if (parsed.selectorArg === undefined) {
|
|
179
193
|
parsed.selectorArg = arg;
|
|
194
|
+
} else {
|
|
195
|
+
parsed.extraArgs.push(arg);
|
|
180
196
|
}
|
|
181
197
|
}
|
|
182
198
|
return parsed;
|
|
@@ -200,10 +216,10 @@ function extractDepthFromArg(selectorArg) {
|
|
|
200
216
|
return parsed.depth;
|
|
201
217
|
}
|
|
202
218
|
/**
|
|
203
|
-
* Process
|
|
219
|
+
* Process an explicit list of resolved file paths.
|
|
204
220
|
*/
|
|
205
|
-
async function
|
|
206
|
-
|
|
221
|
+
async function processFileList(
|
|
222
|
+
filePaths,
|
|
207
223
|
selectorArg,
|
|
208
224
|
checkMode,
|
|
209
225
|
lintMode,
|
|
@@ -212,19 +228,30 @@ async function processStdin(
|
|
|
212
228
|
limit,
|
|
213
229
|
cwd,
|
|
214
230
|
cacheConfig,
|
|
231
|
+
searchQuery,
|
|
215
232
|
) {
|
|
216
|
-
const stdinPaths = parseStdinPaths(stdin, cwd);
|
|
217
233
|
const depth =
|
|
218
234
|
selectorArg !== undefined ? extractDepthFromArg(selectorArg) : undefined;
|
|
235
|
+
if (searchQuery !== undefined) {
|
|
236
|
+
const result = await searchFiles(
|
|
237
|
+
filePaths,
|
|
238
|
+
searchQuery,
|
|
239
|
+
cwd,
|
|
240
|
+
limit,
|
|
241
|
+
cacheConfig,
|
|
242
|
+
);
|
|
243
|
+
writeDrilldownResult(result, json, pretty);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
219
246
|
if (lintMode) {
|
|
220
|
-
const result = await lintFiles(
|
|
247
|
+
const result = await lintFiles(filePaths, cwd, limit, cacheConfig);
|
|
221
248
|
writeLintResult(result, pretty);
|
|
222
249
|
} else if (checkMode) {
|
|
223
|
-
const result = await validateFiles(
|
|
250
|
+
const result = await validateFiles(filePaths, cwd, limit, cacheConfig);
|
|
224
251
|
writeValidationResult(result, pretty);
|
|
225
252
|
} else {
|
|
226
253
|
const result = await drilldownFiles(
|
|
227
|
-
|
|
254
|
+
filePaths,
|
|
228
255
|
depth,
|
|
229
256
|
cwd,
|
|
230
257
|
limit,
|
|
@@ -233,6 +260,35 @@ async function processStdin(
|
|
|
233
260
|
writeDrilldownResult(result, json, pretty);
|
|
234
261
|
}
|
|
235
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Process stdin mode: file paths piped in.
|
|
265
|
+
*/
|
|
266
|
+
async function processStdin(
|
|
267
|
+
stdin,
|
|
268
|
+
selectorArg,
|
|
269
|
+
checkMode,
|
|
270
|
+
lintMode,
|
|
271
|
+
json,
|
|
272
|
+
pretty,
|
|
273
|
+
limit,
|
|
274
|
+
cwd,
|
|
275
|
+
cacheConfig,
|
|
276
|
+
searchQuery,
|
|
277
|
+
) {
|
|
278
|
+
const stdinPaths = parseStdinPaths(stdin, cwd);
|
|
279
|
+
await processFileList(
|
|
280
|
+
stdinPaths,
|
|
281
|
+
selectorArg,
|
|
282
|
+
checkMode,
|
|
283
|
+
lintMode,
|
|
284
|
+
json,
|
|
285
|
+
pretty,
|
|
286
|
+
limit,
|
|
287
|
+
cwd,
|
|
288
|
+
cacheConfig,
|
|
289
|
+
searchQuery,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
236
292
|
/**
|
|
237
293
|
* Process selector mode: glob or path argument.
|
|
238
294
|
*/
|
|
@@ -246,10 +302,23 @@ async function processSelector(
|
|
|
246
302
|
gitignore,
|
|
247
303
|
cwd,
|
|
248
304
|
cacheConfig,
|
|
305
|
+
searchQuery,
|
|
249
306
|
) {
|
|
250
307
|
const selector = selectorArg
|
|
251
308
|
? parseSelector(selectorArg)
|
|
252
309
|
: { type: "glob", pattern: "**/*.{ts,tsx}", depth: undefined };
|
|
310
|
+
if (searchQuery !== undefined) {
|
|
311
|
+
const result = await search(
|
|
312
|
+
{ type: selector.type, pattern: selector.pattern, depth: undefined },
|
|
313
|
+
searchQuery,
|
|
314
|
+
cwd,
|
|
315
|
+
gitignore,
|
|
316
|
+
limit,
|
|
317
|
+
cacheConfig,
|
|
318
|
+
);
|
|
319
|
+
writeDrilldownResult(result, json, pretty);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
253
322
|
if (lintMode) {
|
|
254
323
|
const result = await lint(selector, cwd, limit, gitignore, cacheConfig);
|
|
255
324
|
writeLintResult(result, pretty);
|
|
@@ -268,19 +337,27 @@ async function processSelector(
|
|
|
268
337
|
}
|
|
269
338
|
}
|
|
270
339
|
/**
|
|
271
|
-
* Write an error to stderr as JSON
|
|
340
|
+
* Write an error to stderr as JSON or plain text depending on the json flag.
|
|
272
341
|
*/
|
|
273
|
-
function writeError(error) {
|
|
342
|
+
function writeError(error, json) {
|
|
343
|
+
process.exitCode = 1;
|
|
344
|
+
if (json) {
|
|
345
|
+
if (error instanceof JsdocError) {
|
|
346
|
+
void process.stderr.write(`${JSON.stringify(error.toJSON())}\n`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
350
|
+
void process.stderr.write(
|
|
351
|
+
`${JSON.stringify({ error: { code: "INTERNAL_ERROR", message } })}\n`,
|
|
352
|
+
);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
274
355
|
if (error instanceof JsdocError) {
|
|
275
|
-
void process.stderr.write(
|
|
276
|
-
process.exitCode = 1;
|
|
356
|
+
void process.stderr.write(`Error [${error.code}]: ${error.message}\n`);
|
|
277
357
|
return;
|
|
278
358
|
}
|
|
279
359
|
const message = error instanceof Error ? error.message : String(error);
|
|
280
|
-
void process.stderr.write(
|
|
281
|
-
`${JSON.stringify({ error: { code: "INTERNAL_ERROR", message } })}\n`,
|
|
282
|
-
);
|
|
283
|
-
process.exitCode = 1;
|
|
360
|
+
void process.stderr.write(`Error: ${message}\n`);
|
|
284
361
|
}
|
|
285
362
|
/**
|
|
286
363
|
* Handle --help flag by printing help text.
|
|
@@ -291,13 +368,13 @@ function handleHelp() {
|
|
|
291
368
|
/**
|
|
292
369
|
* Handle --version flag by reading and printing version from package.json.
|
|
293
370
|
*/
|
|
294
|
-
function handleVersion() {
|
|
371
|
+
async function handleVersion() {
|
|
295
372
|
const pkgPath = resolve(
|
|
296
373
|
dirname(fileURLToPath(import.meta.url)),
|
|
297
374
|
"..",
|
|
298
375
|
"package.json",
|
|
299
376
|
);
|
|
300
|
-
const pkg = JSON.parse(
|
|
377
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
301
378
|
void process.stdout.write(`${pkg.version}\n`);
|
|
302
379
|
}
|
|
303
380
|
/**
|
|
@@ -309,7 +386,7 @@ function handleSkill() {
|
|
|
309
386
|
/**
|
|
310
387
|
* Handle --explain-rule flag by printing rule explanation.
|
|
311
388
|
*/
|
|
312
|
-
function handleExplainRule(ruleName) {
|
|
389
|
+
function handleExplainRule(ruleName, json) {
|
|
313
390
|
const explanation = RULE_EXPLANATIONS[ruleName];
|
|
314
391
|
if (explanation) {
|
|
315
392
|
void process.stdout.write(explanation);
|
|
@@ -321,39 +398,65 @@ function handleExplainRule(ruleName) {
|
|
|
321
398
|
"INVALID_SELECTOR",
|
|
322
399
|
`Unknown rule: ${ruleName}. Available rules: ${available}`,
|
|
323
400
|
),
|
|
401
|
+
json,
|
|
324
402
|
);
|
|
325
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Handle early-exit flags that print output and return without processing files.
|
|
406
|
+
* Returns true if an early-exit flag was handled.
|
|
407
|
+
*/
|
|
408
|
+
async function handleEarlyExitFlags(parsed, json) {
|
|
409
|
+
if (parsed.help) {
|
|
410
|
+
handleHelp();
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
if (parsed.version) {
|
|
414
|
+
await handleVersion();
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
if (parsed.skillMode) {
|
|
418
|
+
handleSkill();
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
if (parsed.explainRule !== undefined) {
|
|
422
|
+
handleExplainRule(parsed.explainRule, json);
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Validate that mode flags are not used in incompatible combinations.
|
|
429
|
+
* Returns true if validation passed (no conflicts), false if an error was written.
|
|
430
|
+
*/
|
|
431
|
+
function validateModeCombinations(parsed, json) {
|
|
432
|
+
if (parsed.checkMode && parsed.lintMode) {
|
|
433
|
+
writeError(
|
|
434
|
+
new JsdocError("INVALID_SELECTOR", "Cannot use -c and -l together"),
|
|
435
|
+
json,
|
|
436
|
+
);
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
if (
|
|
440
|
+
parsed.searchQuery !== undefined &&
|
|
441
|
+
(parsed.checkMode || parsed.lintMode)
|
|
442
|
+
) {
|
|
443
|
+
writeError(
|
|
444
|
+
new JsdocError("INVALID_SELECTOR", "Cannot use --search with -c or -l"),
|
|
445
|
+
json,
|
|
446
|
+
);
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
326
451
|
/**
|
|
327
452
|
* Main CLI entry point. Exported for testability.
|
|
328
453
|
*/
|
|
329
454
|
export async function main(args, stdin) {
|
|
455
|
+
const json = args.includes("--json");
|
|
330
456
|
try {
|
|
331
457
|
const parsed = parseArgs(args);
|
|
332
|
-
|
|
333
|
-
if (parsed
|
|
334
|
-
handleHelp();
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
if (parsed.version) {
|
|
338
|
-
handleVersion();
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
if (parsed.skillMode) {
|
|
342
|
-
handleSkill();
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
if (parsed.explainRule !== undefined) {
|
|
346
|
-
handleExplainRule(parsed.explainRule);
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
// Validate mode combinations
|
|
350
|
-
if (parsed.checkMode && parsed.lintMode) {
|
|
351
|
-
writeError(
|
|
352
|
-
new JsdocError("INVALID_SELECTOR", "Cannot use -c and -l together"),
|
|
353
|
-
);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
// Build cache config and process files
|
|
458
|
+
if (await handleEarlyExitFlags(parsed, json)) return;
|
|
459
|
+
if (!validateModeCombinations(parsed, json)) return;
|
|
357
460
|
const cacheConfig = {
|
|
358
461
|
enabled: !parsed.disableCache,
|
|
359
462
|
directory: parsed.cacheDirectory ?? DEFAULT_CACHE_DIR,
|
|
@@ -370,6 +473,30 @@ export async function main(args, stdin) {
|
|
|
370
473
|
parsed.limit,
|
|
371
474
|
cwd,
|
|
372
475
|
cacheConfig,
|
|
476
|
+
parsed.searchQuery,
|
|
477
|
+
);
|
|
478
|
+
} else if (parsed.extraArgs.length > 0) {
|
|
479
|
+
// Multiple positional args (e.g. shell-expanded glob): expand each to
|
|
480
|
+
// .ts/.tsx files via discoverFiles (handles directories recursively)
|
|
481
|
+
const allArgPaths = [
|
|
482
|
+
...(parsed.selectorArg ? [parsed.selectorArg] : []),
|
|
483
|
+
...parsed.extraArgs,
|
|
484
|
+
];
|
|
485
|
+
const fileLists = await Promise.all(
|
|
486
|
+
allArgPaths.map((p) => discoverFiles(p, cwd, parsed.gitignore)),
|
|
487
|
+
);
|
|
488
|
+
const filePaths = [...new Set(fileLists.flat())];
|
|
489
|
+
await processFileList(
|
|
490
|
+
filePaths,
|
|
491
|
+
parsed.selectorArg,
|
|
492
|
+
parsed.checkMode,
|
|
493
|
+
parsed.lintMode,
|
|
494
|
+
parsed.json,
|
|
495
|
+
parsed.pretty,
|
|
496
|
+
parsed.limit,
|
|
497
|
+
cwd,
|
|
498
|
+
cacheConfig,
|
|
499
|
+
parsed.searchQuery,
|
|
373
500
|
);
|
|
374
501
|
} else {
|
|
375
502
|
await processSelector(
|
|
@@ -382,10 +509,11 @@ export async function main(args, stdin) {
|
|
|
382
509
|
parsed.gitignore,
|
|
383
510
|
cwd,
|
|
384
511
|
cacheConfig,
|
|
512
|
+
parsed.searchQuery,
|
|
385
513
|
);
|
|
386
514
|
}
|
|
387
515
|
} catch (error) {
|
|
388
|
-
writeError(error);
|
|
516
|
+
writeError(error, json);
|
|
389
517
|
}
|
|
390
518
|
}
|
|
391
519
|
/**
|
package/dist/drilldown.js
CHANGED
|
@@ -32,8 +32,13 @@ function buildLevels(info, typeDeclarations, fileContent) {
|
|
|
32
32
|
return [
|
|
33
33
|
summary !== null ? { text: summary } : null,
|
|
34
34
|
description !== null ? { text: description } : null,
|
|
35
|
-
{
|
|
36
|
-
|
|
35
|
+
{
|
|
36
|
+
text:
|
|
37
|
+
typeDeclarations.length > 0
|
|
38
|
+
? `\`\`\`typescript\n${typeDeclarations}\n\`\`\``
|
|
39
|
+
: typeDeclarations,
|
|
40
|
+
},
|
|
41
|
+
{ text: `\`\`\`typescript\n${fileContent}\n\`\`\`` },
|
|
37
42
|
];
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
@@ -177,7 +182,7 @@ async function gatherBarrelInfos(barrelPaths, cwd, config) {
|
|
|
177
182
|
const info = await processWithCache(config, "drilldown", content, () =>
|
|
178
183
|
parseFileSummaries(barrelPath),
|
|
179
184
|
);
|
|
180
|
-
const children = getBarrelChildren(barrelPath, cwd);
|
|
185
|
+
const children = await getBarrelChildren(barrelPath, cwd);
|
|
181
186
|
return {
|
|
182
187
|
type: "info",
|
|
183
188
|
data: {
|
|
@@ -328,7 +333,7 @@ export async function drilldown(
|
|
|
328
333
|
) {
|
|
329
334
|
const depth = selector.depth ?? 1;
|
|
330
335
|
if (selector.type === "path") {
|
|
331
|
-
const files = discoverFiles(selector.pattern, cwd, gitignore);
|
|
336
|
+
const files = await discoverFiles(selector.pattern, cwd, gitignore);
|
|
332
337
|
if (files.length > 1) {
|
|
333
338
|
// Directory expanded to multiple files — route through glob pipeline
|
|
334
339
|
const results = await processGlobWithBarrels(files, depth, cwd, config);
|
|
@@ -358,7 +363,7 @@ export async function drilldown(
|
|
|
358
363
|
barrelEffectiveDepth = 3;
|
|
359
364
|
}
|
|
360
365
|
if (barrelEffectiveDepth >= 3) {
|
|
361
|
-
const children = getBarrelChildren(filePath, cwd);
|
|
366
|
+
const children = await getBarrelChildren(filePath, cwd);
|
|
362
367
|
const childDepth = barrelEffectiveDepth - 2;
|
|
363
368
|
const results = await collectSafeResults(
|
|
364
369
|
children,
|
|
@@ -385,7 +390,7 @@ export async function drilldown(
|
|
|
385
390
|
return { items, truncated: false };
|
|
386
391
|
}
|
|
387
392
|
// Glob selector — apply barrel gating
|
|
388
|
-
const files = discoverFiles(selector.pattern, cwd, gitignore);
|
|
393
|
+
const files = await discoverFiles(selector.pattern, cwd, gitignore);
|
|
389
394
|
if (files.length === 0) {
|
|
390
395
|
throw new JsdocError(
|
|
391
396
|
"NO_FILES_MATCHED",
|
|
@@ -422,7 +427,7 @@ export async function drilldownFiles(
|
|
|
422
427
|
config = DEFAULT_CACHE_CONFIG,
|
|
423
428
|
) {
|
|
424
429
|
const d = depth ?? 1;
|
|
425
|
-
const ig = loadGitignore(cwd);
|
|
430
|
+
const ig = await loadGitignore(cwd);
|
|
426
431
|
const tsFiles = filePaths.filter((f) => {
|
|
427
432
|
if (!(f.endsWith(".ts") || f.endsWith(".tsx")) || f.endsWith(".d.ts")) {
|
|
428
433
|
return false;
|
package/dist/file-discovery.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { glob } from "glob";
|
|
4
4
|
import ignore from "ignore";
|
|
5
5
|
import { JsdocError } from "./errors.js";
|
|
6
6
|
/**
|
|
@@ -15,13 +15,13 @@ import { JsdocError } from "./errors.js";
|
|
|
15
15
|
* Walk from `cwd` up to the filesystem root, collecting .gitignore entries.
|
|
16
16
|
* Returns an Ignore instance loaded with all discovered rules.
|
|
17
17
|
*/
|
|
18
|
-
export function loadGitignore(cwd) {
|
|
18
|
+
export async function loadGitignore(cwd) {
|
|
19
19
|
const ig = ignore();
|
|
20
20
|
let dir = resolve(cwd);
|
|
21
21
|
while (true) {
|
|
22
22
|
const gitignorePath = join(dir, ".gitignore");
|
|
23
|
-
|
|
24
|
-
const content =
|
|
23
|
+
try {
|
|
24
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
25
25
|
const prefix = relative(cwd, dir);
|
|
26
26
|
const lines = content
|
|
27
27
|
.split("\n")
|
|
@@ -32,6 +32,8 @@ export function loadGitignore(cwd) {
|
|
|
32
32
|
// relative to `cwd`, which is where glob results are anchored.
|
|
33
33
|
ig.add(prefix ? `${prefix}/${line}` : line);
|
|
34
34
|
}
|
|
35
|
+
} catch {
|
|
36
|
+
// No .gitignore at this level, continue walking up
|
|
35
37
|
}
|
|
36
38
|
const parent = dirname(dir);
|
|
37
39
|
if (parent === dir) break;
|
|
@@ -52,25 +54,28 @@ export function loadGitignore(cwd) {
|
|
|
52
54
|
* @returns Array of absolute file paths
|
|
53
55
|
* @throws {JsdocError} FILE_NOT_FOUND when a direct path does not exist
|
|
54
56
|
*/
|
|
55
|
-
export function discoverFiles(pattern, cwd, gitignore = true) {
|
|
57
|
+
export async function discoverFiles(pattern, cwd, gitignore = true) {
|
|
56
58
|
const hasGlobChars = /[*?[\]{]/.test(pattern);
|
|
57
59
|
if (hasGlobChars) {
|
|
58
|
-
const matches =
|
|
60
|
+
const matches = await glob(pattern, { cwd, absolute: true });
|
|
59
61
|
let filtered = matches.filter(
|
|
60
62
|
(f) => (f.endsWith(".ts") || f.endsWith(".tsx")) && !f.endsWith(".d.ts"),
|
|
61
63
|
);
|
|
62
64
|
if (gitignore) {
|
|
63
|
-
const ig = loadGitignore(cwd);
|
|
65
|
+
const ig = await loadGitignore(cwd);
|
|
64
66
|
filtered = filtered.filter((abs) => !ig.ignores(relative(cwd, abs)));
|
|
65
67
|
}
|
|
66
68
|
return filtered.sort();
|
|
67
69
|
}
|
|
68
70
|
// Direct path
|
|
69
71
|
const resolved = resolve(cwd, pattern);
|
|
70
|
-
|
|
72
|
+
let statResult;
|
|
73
|
+
try {
|
|
74
|
+
statResult = await stat(resolved);
|
|
75
|
+
} catch {
|
|
71
76
|
throw new JsdocError("FILE_NOT_FOUND", `File not found: ${pattern}`);
|
|
72
77
|
}
|
|
73
|
-
if (
|
|
78
|
+
if (statResult.isDirectory()) {
|
|
74
79
|
return discoverFiles(`${resolved}/**`, cwd, gitignore);
|
|
75
80
|
}
|
|
76
81
|
return [resolved];
|
package/dist/index.js
CHANGED
|
@@ -13,8 +13,13 @@ export { JsdocError } from "./errors.js";
|
|
|
13
13
|
export { discoverFiles } from "./file-discovery.js";
|
|
14
14
|
export { extractFileJsdoc, parseFileSummaries } from "./jsdoc-parser.js";
|
|
15
15
|
export { lint, lintFiles } from "./lint.js";
|
|
16
|
+
export { search, searchFiles } from "./search.js";
|
|
16
17
|
export { parseSelector } from "./selector.js";
|
|
17
18
|
export { formatTextOutput } from "./text-format.js";
|
|
18
|
-
export {
|
|
19
|
+
export {
|
|
20
|
+
extractSourceBlocks,
|
|
21
|
+
generateTypeDeclarations,
|
|
22
|
+
splitDeclarations,
|
|
23
|
+
} from "./type-declarations.js";
|
|
19
24
|
export { DEFAULT_CACHE_DIR, VALIDATION_STATUS_PRIORITY } from "./types.js";
|
|
20
25
|
export { validate, validateFiles } from "./validate.js";
|