jsdoc-scribe 1.0.0 → 1.7.0
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/CHANGELOG.md +98 -0
- package/README.md +500 -131
- package/bin/cli.js +115 -115
- package/bin/gen-docs.js +299 -0
- package/lib/config.js +47 -0
- package/lib/docs.js +71 -0
- package/lib/extractor.js +456 -0
- package/lib/index.js +443 -427
- package/lib/renderer.js +575 -0
- package/package.json +21 -5
package/lib/index.js
CHANGED
|
@@ -1,427 +1,443 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
* ----------------------------------------
|
|
6
|
-
* A PURE, deterministic, AST-based JSDoc comment generator.
|
|
7
|
-
* No AI / LLM / external API is used anywhere — every comment line is
|
|
8
|
-
* derived mechanically from syntax: names, modifiers, type annotations,
|
|
9
|
-
* parameter lists, heritage clauses, enum members, etc.
|
|
10
|
-
*
|
|
11
|
-
* Works on plain JavaScript (.js/.jsx) AND TypeScript (.ts/.tsx) because
|
|
12
|
-
* both are parsed with the TypeScript compiler's parser, which is a
|
|
13
|
-
* superset parser for JS (it never type-checks, only parses syntax).
|
|
14
|
-
*
|
|
15
|
-
* This file exports plain functions so it can be:
|
|
16
|
-
* 1. Driven by bin/cli.js as a command-line tool, or
|
|
17
|
-
* 2. require()'d directly in another Node script / build pipeline:
|
|
18
|
-
* const { processFile } = require('jsdoc-scribe');
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const fs = require("fs");
|
|
22
|
-
const path = require("path");
|
|
23
|
-
const ts = require("typescript");
|
|
24
|
-
|
|
25
|
-
const DEFAULT_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx"];
|
|
26
|
-
const DEFAULT_IGNORE_DIRS = new Set([
|
|
27
|
-
"node_modules",
|
|
28
|
-
".git",
|
|
29
|
-
"dist",
|
|
30
|
-
"build",
|
|
31
|
-
"out",
|
|
32
|
-
"coverage",
|
|
33
|
-
".next",
|
|
34
|
-
".turbo",
|
|
35
|
-
".cache",
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Low-level helpers
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
function getScriptKind(file) {
|
|
43
|
-
switch (path.extname(file).toLowerCase()) {
|
|
44
|
-
case ".tsx":
|
|
45
|
-
return ts.ScriptKind.TSX;
|
|
46
|
-
case ".ts":
|
|
47
|
-
return ts.ScriptKind.TS;
|
|
48
|
-
case ".jsx":
|
|
49
|
-
return ts.ScriptKind.JSX;
|
|
50
|
-
default:
|
|
51
|
-
return ts.ScriptKind.JS;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** True if `node` already has a leading /** ... *\/ block right above it. */
|
|
56
|
-
function hasLeadingJSDoc(sourceFile, node) {
|
|
57
|
-
const ranges = ts.getLeadingCommentRanges(sourceFile.text, node.getFullStart()) || [];
|
|
58
|
-
return ranges.some((r) => sourceFile.text.slice(r.pos, r.pos + 3) === "/**");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Walk backwards from the node's first real token to find where its own
|
|
63
|
-
* line-indentation begins. Returns the exact insertion offset + the
|
|
64
|
-
* whitespace string to reuse for every comment line, so the comment lines
|
|
65
|
-
* up visually with the code it documents — without re-printing/reformatting
|
|
66
|
-
* the rest of the file.
|
|
67
|
-
*/
|
|
68
|
-
function getIndentAndInsertionPos(sourceFile, node) {
|
|
69
|
-
const start = node.getStart(sourceFile);
|
|
70
|
-
const text = sourceFile.text;
|
|
71
|
-
let i = start;
|
|
72
|
-
while (i > 0 && (text[i - 1] === " " || text[i - 1] === "\t")) i--;
|
|
73
|
-
return { pos: i, indent: text.slice(i, start) };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function modifiersOf(node) {
|
|
77
|
-
const
|
|
78
|
-
return {
|
|
79
|
-
isExported:
|
|
80
|
-
isAsync:
|
|
81
|
-
isStatic:
|
|
82
|
-
isReadonly:
|
|
83
|
-
isPrivate:
|
|
84
|
-
isProtected:
|
|
85
|
-
isAbstract:
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function typeText(node) {
|
|
90
|
-
return node && node.type ? node.type.getText() : null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function isFunctionLikeInitializer(init) {
|
|
94
|
-
return !!init && (init.kind === ts.SyntaxKind.ArrowFunction || init.kind === ts.SyntaxKind.FunctionExpression);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Purely syntactic: does this block contain a `return <value>;` at its own level (not inside a nested function)? */
|
|
98
|
-
function hasReturnWithValue(block) {
|
|
99
|
-
let found = false;
|
|
100
|
-
function walk(n) {
|
|
101
|
-
if (found) return;
|
|
102
|
-
if (ts.isReturnStatement(n)) {
|
|
103
|
-
if (n.expression) found = true;
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (ts.isFunctionLike(n)) return; // don't attribute a nested closure's return to the outer function
|
|
107
|
-
ts.forEachChild(n, walk);
|
|
108
|
-
}
|
|
109
|
-
walk(block);
|
|
110
|
-
return found;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Decide the @returns type with no semantics, no AI — just syntax:
|
|
115
|
-
* 1. An explicit type annotation always wins.
|
|
116
|
-
* 2. A concise arrow body (`x => x + 1`, no braces) always produces a value.
|
|
117
|
-
* 3. A block body is scanned for a top-level `return <value>;`.
|
|
118
|
-
* 4. Otherwise it's void.
|
|
119
|
-
*/
|
|
120
|
-
function inferReturnType(fnNode) {
|
|
121
|
-
const explicit = typeText(fnNode);
|
|
122
|
-
if (explicit) return explicit;
|
|
123
|
-
const body = fnNode.body;
|
|
124
|
-
if (!body) return "void";
|
|
125
|
-
if (body.kind !== ts.SyntaxKind.Block) return "any"; // concise arrow body
|
|
126
|
-
return hasReturnWithValue(body) ? "any" : "void";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Best-effort, purely syntactic type guess from an initializer expression. No semantics, no AI. */
|
|
130
|
-
function inferTypeFromInitializer(init) {
|
|
131
|
-
if (!init) return "any";
|
|
132
|
-
switch (init.kind) {
|
|
133
|
-
case ts.SyntaxKind.StringLiteral:
|
|
134
|
-
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
135
|
-
return "string";
|
|
136
|
-
case ts.SyntaxKind.NumericLiteral:
|
|
137
|
-
return "number";
|
|
138
|
-
case ts.SyntaxKind.TrueKeyword:
|
|
139
|
-
case ts.SyntaxKind.FalseKeyword:
|
|
140
|
-
return "boolean";
|
|
141
|
-
case ts.SyntaxKind.ArrayLiteralExpression:
|
|
142
|
-
return "Array";
|
|
143
|
-
case ts.SyntaxKind.ObjectLiteralExpression:
|
|
144
|
-
return "Object";
|
|
145
|
-
case ts.SyntaxKind.ArrowFunction:
|
|
146
|
-
case ts.SyntaxKind.FunctionExpression:
|
|
147
|
-
return "Function";
|
|
148
|
-
case ts.SyntaxKind.NewExpression:
|
|
149
|
-
return init.expression ? init.expression.getText() : "Object";
|
|
150
|
-
default:
|
|
151
|
-
return "any";
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ---------------------------------------------------------------------------
|
|
156
|
-
// Comment builders — every line below comes ONLY from AST shape, never from
|
|
157
|
-
// guessing "what the code means".
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
|
|
160
|
-
function buildParamLines(params) {
|
|
161
|
-
return (params || []).map((p) => {
|
|
162
|
-
const name = p.name.getText();
|
|
163
|
-
const optional = !!p.questionToken || !!p.initializer;
|
|
164
|
-
const type = typeText(p) || (p.initializer ? inferTypeFromInitializer(p.initializer) : "any");
|
|
165
|
-
const label = optional ? `[${name}]` : name;
|
|
166
|
-
return ` * @param {${type}} ${label}`;
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function buildFunctionDoc({ name, params, returnType, mods, isGenerator }) {
|
|
171
|
-
const lines = ["/**", ` * @function ${name || "anonymous"}`];
|
|
172
|
-
if (mods.isExported) lines.push(" * @exported");
|
|
173
|
-
if (mods.isAsync) lines.push(" * @async");
|
|
174
|
-
if (isGenerator) lines.push(" * @generator");
|
|
175
|
-
lines.push(...buildParamLines(params));
|
|
176
|
-
lines.push(` * @returns {${returnType || "void"}}`);
|
|
177
|
-
lines.push(" */");
|
|
178
|
-
return lines;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function buildClassDoc(node) {
|
|
182
|
-
const name = node.name ? node.name.getText() : "AnonymousClass";
|
|
183
|
-
const mods = modifiersOf(node);
|
|
184
|
-
const heritage = node.heritageClauses || [];
|
|
185
|
-
const lines = ["/**", ` * @class ${name}`];
|
|
186
|
-
if (mods.isExported) lines.push(" * @exported");
|
|
187
|
-
if (mods.isAbstract) lines.push(" * @abstract");
|
|
188
|
-
for (const h of heritage) {
|
|
189
|
-
const kw = h.token === ts.SyntaxKind.ExtendsKeyword ? "@extends" : "@implements";
|
|
190
|
-
for (const t of h.types) lines.push(` * ${kw} ${t.getText()}`);
|
|
191
|
-
}
|
|
192
|
-
lines.push(" */");
|
|
193
|
-
return lines;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function buildMethodDoc(node) {
|
|
197
|
-
const mods = modifiersOf(node);
|
|
198
|
-
const visibility = mods.isPrivate ? "private" : mods.isProtected ? "protected" : "public";
|
|
199
|
-
const name = node.name.getText();
|
|
200
|
-
const kind = ts.isGetAccessorDeclaration(node) ? "getter" : ts.isSetAccessorDeclaration(node) ? "setter" : "method";
|
|
201
|
-
const lines = ["/**", ` * @${kind} ${name}`, ` * @${visibility}`];
|
|
202
|
-
if (mods.isStatic) lines.push(" * @static");
|
|
203
|
-
if (mods.isAbstract) lines.push(" * @abstract");
|
|
204
|
-
if (mods.isAsync) lines.push(" * @async");
|
|
205
|
-
if (node.asteriskToken) lines.push(" * @generator");
|
|
206
|
-
lines.push(...buildParamLines(node.parameters));
|
|
207
|
-
lines.push(` * @returns {${inferReturnType(node)}}`);
|
|
208
|
-
lines.push(" */");
|
|
209
|
-
return lines;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function buildMemberDoc(node) {
|
|
213
|
-
if (ts.isConstructorDeclaration(node)) {
|
|
214
|
-
const lines = ["/**", " * @constructor"];
|
|
215
|
-
lines.push(...buildParamLines(node.parameters));
|
|
216
|
-
lines.push(" */");
|
|
217
|
-
return lines;
|
|
218
|
-
}
|
|
219
|
-
if (ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) {
|
|
220
|
-
return buildMethodDoc(node);
|
|
221
|
-
}
|
|
222
|
-
if (ts.isPropertyDeclaration(node)) {
|
|
223
|
-
const mods = modifiersOf(node);
|
|
224
|
-
const visibility = mods.isPrivate ? "private" : mods.isProtected ? "protected" : "public";
|
|
225
|
-
const name = node.name.getText();
|
|
226
|
-
const type = typeText(node) || inferTypeFromInitializer(node.initializer);
|
|
227
|
-
const lines = ["/**", ` * @member ${name}`, ` * @type {${type}}`, ` * @${visibility}`];
|
|
228
|
-
if (mods.isStatic) lines.push(" * @static");
|
|
229
|
-
if (mods.isReadonly) lines.push(" * @readonly");
|
|
230
|
-
lines.push(" */");
|
|
231
|
-
return lines;
|
|
232
|
-
}
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function buildSingleVariableDoc(decl, isConst) {
|
|
237
|
-
const name = decl.name.getText();
|
|
238
|
-
const init = decl.initializer;
|
|
239
|
-
if (isFunctionLikeInitializer(init)) {
|
|
240
|
-
return buildFunctionDoc({
|
|
241
|
-
name,
|
|
242
|
-
params: init.parameters,
|
|
243
|
-
returnType: inferReturnType(init),
|
|
244
|
-
mods: { isAsync: (init.modifiers || []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) },
|
|
245
|
-
isGenerator: !!init.asteriskToken,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
const type = typeText(decl) || inferTypeFromInitializer(init);
|
|
249
|
-
return ["/**", ` * @${isConst ? "constant" : "variable"} ${name}`, ` * @type {${type}}`, " */"];
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function buildVariableStatementDoc(node, isConst) {
|
|
253
|
-
const decls = node.declarationList.declarations;
|
|
254
|
-
if (decls.length === 1) return buildSingleVariableDoc(decls[0], isConst);
|
|
255
|
-
const lines = ["/**"];
|
|
256
|
-
for (const decl of decls) {
|
|
257
|
-
const type = typeText(decl) || inferTypeFromInitializer(decl.initializer);
|
|
258
|
-
lines.push(` * @${isConst ? "constant" : "variable"} ${decl.name.getText()} {${type}}`);
|
|
259
|
-
}
|
|
260
|
-
lines.push(" */");
|
|
261
|
-
return lines;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function buildInterfaceDoc(node) {
|
|
265
|
-
const name = node.name.getText();
|
|
266
|
-
const lines = ["/**", ` * @interface ${name}`];
|
|
267
|
-
for (const m of node.members) {
|
|
268
|
-
if (ts.isPropertySignature(m) && m.name) {
|
|
269
|
-
lines.push(` * @property {${typeText(m) || "any"}} ${m.name.getText()}`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
lines.push(" */");
|
|
273
|
-
return lines;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function buildTypeAliasDoc(node) {
|
|
277
|
-
return ["/**", ` * @typedef {${node.type.getText()}} ${node.name.getText()}`, " */"];
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function buildEnumDoc(node) {
|
|
281
|
-
const members = node.members.map((m) => m.name.getText()).join(" | ");
|
|
282
|
-
return ["/**", ` * @enum ${node.name.getText()}`, ` * @values ${members}`, " */"];
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// ---------------------------------------------------------------------------
|
|
286
|
-
// AST walk: collect {pos, text} edits, never mutating the source directly.
|
|
287
|
-
// ---------------------------------------------------------------------------
|
|
288
|
-
|
|
289
|
-
function collectEdits(sourceFile, force) {
|
|
290
|
-
const edits = [];
|
|
291
|
-
|
|
292
|
-
function addEdit(node, linesFn) {
|
|
293
|
-
if (!force && hasLeadingJSDoc(sourceFile, node)) return;
|
|
294
|
-
const lines = linesFn();
|
|
295
|
-
if (!lines) return;
|
|
296
|
-
const { pos, indent } = getIndentAndInsertionPos(sourceFile, node);
|
|
297
|
-
const text = lines.map((l) => indent + l).join("\n") + "\n";
|
|
298
|
-
edits.push({ pos, text });
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function visit(node) {
|
|
302
|
-
if (ts.isFunctionDeclaration(node) && node.body) {
|
|
303
|
-
addEdit(node, () =>
|
|
304
|
-
buildFunctionDoc({
|
|
305
|
-
name: node.name ? node.name.getText() : null,
|
|
306
|
-
params: node.parameters,
|
|
307
|
-
returnType: inferReturnType(node),
|
|
308
|
-
mods: modifiersOf(node),
|
|
309
|
-
isGenerator: !!node.asteriskToken,
|
|
310
|
-
}),
|
|
311
|
-
);
|
|
312
|
-
} else if (ts.isClassDeclaration(node)) {
|
|
313
|
-
addEdit(node, () => buildClassDoc(node));
|
|
314
|
-
} else if (
|
|
315
|
-
ts.isMethodDeclaration(node) ||
|
|
316
|
-
ts.isConstructorDeclaration(node) ||
|
|
317
|
-
ts.isPropertyDeclaration(node) ||
|
|
318
|
-
ts.isGetAccessorDeclaration(node) ||
|
|
319
|
-
ts.isSetAccessorDeclaration(node)
|
|
320
|
-
) {
|
|
321
|
-
addEdit(node, () => buildMemberDoc(node));
|
|
322
|
-
} else if (ts.isPropertyAssignment(node) && isFunctionLikeInitializer(node.initializer)) {
|
|
323
|
-
const init = node.initializer;
|
|
324
|
-
addEdit(node, () =>
|
|
325
|
-
buildFunctionDoc({
|
|
326
|
-
name: node.name.getText(),
|
|
327
|
-
params: init.parameters,
|
|
328
|
-
returnType: inferReturnType(init),
|
|
329
|
-
mods: { isAsync: (init.modifiers || []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) },
|
|
330
|
-
isGenerator: !!init.asteriskToken,
|
|
331
|
-
}),
|
|
332
|
-
);
|
|
333
|
-
} else if (ts.isVariableStatement(node)) {
|
|
334
|
-
const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
|
|
335
|
-
addEdit(node, () => buildVariableStatementDoc(node, isConst));
|
|
336
|
-
} else if (ts.isInterfaceDeclaration(node)) {
|
|
337
|
-
addEdit(node, () => buildInterfaceDoc(node));
|
|
338
|
-
} else if (ts.isTypeAliasDeclaration(node)) {
|
|
339
|
-
addEdit(node, () => buildTypeAliasDoc(node));
|
|
340
|
-
} else if (ts.isEnumDeclaration(node)) {
|
|
341
|
-
addEdit(node, () => buildEnumDoc(node));
|
|
342
|
-
}
|
|
343
|
-
ts.forEachChild(node, visit);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
visit(sourceFile);
|
|
347
|
-
return edits;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ---------------------------------------------------------------------------
|
|
351
|
-
// File processing
|
|
352
|
-
// ---------------------------------------------------------------------------
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Process a single file. Returns the number of comment blocks added.
|
|
356
|
-
* @param {string} filePath
|
|
357
|
-
* @param {{ write?: boolean, force?: boolean, silent?: boolean }} [options]
|
|
358
|
-
* @returns {number}
|
|
359
|
-
*/
|
|
360
|
-
function processFile(filePath, options = {}) {
|
|
361
|
-
const { write = false, force = false, silent = false } = options;
|
|
362
|
-
const sourceText = fs.readFileSync(filePath, "utf8");
|
|
363
|
-
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
|
|
364
|
-
|
|
365
|
-
const edits = collectEdits(sourceFile, force);
|
|
366
|
-
edits.sort((a, b) => b.pos - a.pos); // bottom-to-top so earlier offsets stay valid
|
|
367
|
-
|
|
368
|
-
let output = sourceText;
|
|
369
|
-
for (const edit of edits) {
|
|
370
|
-
output = output.slice(0, edit.pos) + edit.text + output.slice(edit.pos);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const outPath = write ? filePath : filePath.replace(/(\.[jt]sx?)$/, "
|
|
374
|
-
if (edits.length > 0 || write) {
|
|
375
|
-
fs.writeFileSync(outPath, output, "utf8");
|
|
376
|
-
}
|
|
377
|
-
if (!silent) {
|
|
378
|
-
console.log(` ${filePath} -> ${outPath} (${edits.length} block${edits.length === 1 ? "" : "s"})`);
|
|
379
|
-
}
|
|
380
|
-
return edits.length;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// ---------------------------------------------------------------------------
|
|
384
|
-
// Directory / project-wide scanning
|
|
385
|
-
// ---------------------------------------------------------------------------
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Recursively collect all matching source files under a path.
|
|
389
|
-
* If `inputPath` is itself a file, returns it (if its extension matches).
|
|
390
|
-
* @param {string} inputPath
|
|
391
|
-
* @param {string[]} [extensions]
|
|
392
|
-
* @param {Set<string>} [ignoreDirs]
|
|
393
|
-
* @returns {string[]}
|
|
394
|
-
*/
|
|
395
|
-
function collectFiles(inputPath, extensions = DEFAULT_EXTENSIONS, ignoreDirs = DEFAULT_IGNORE_DIRS) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* comment-block-generator — lib/index.js
|
|
5
|
+
* ----------------------------------------
|
|
6
|
+
* A PURE, deterministic, AST-based JSDoc comment generator.
|
|
7
|
+
* No AI / LLM / external API is used anywhere — every comment line is
|
|
8
|
+
* derived mechanically from syntax: names, modifiers, type annotations,
|
|
9
|
+
* parameter lists, heritage clauses, enum members, etc.
|
|
10
|
+
*
|
|
11
|
+
* Works on plain JavaScript (.js/.jsx) AND TypeScript (.ts/.tsx) because
|
|
12
|
+
* both are parsed with the TypeScript compiler's parser, which is a
|
|
13
|
+
* superset parser for JS (it never type-checks, only parses syntax).
|
|
14
|
+
*
|
|
15
|
+
* This file exports plain functions so it can be:
|
|
16
|
+
* 1. Driven by bin/cli.js as a command-line tool, or
|
|
17
|
+
* 2. require()'d directly in another Node script / build pipeline:
|
|
18
|
+
* const { processFile } = require('jsdoc-scribe');
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require("fs");
|
|
22
|
+
const path = require("path");
|
|
23
|
+
const ts = require("typescript");
|
|
24
|
+
|
|
25
|
+
const DEFAULT_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx"];
|
|
26
|
+
const DEFAULT_IGNORE_DIRS = new Set([
|
|
27
|
+
"node_modules",
|
|
28
|
+
".git",
|
|
29
|
+
"dist",
|
|
30
|
+
"build",
|
|
31
|
+
"out",
|
|
32
|
+
"coverage",
|
|
33
|
+
".next",
|
|
34
|
+
".turbo",
|
|
35
|
+
".cache",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Low-level helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
function getScriptKind(file) {
|
|
43
|
+
switch (path.extname(file).toLowerCase()) {
|
|
44
|
+
case ".tsx":
|
|
45
|
+
return ts.ScriptKind.TSX;
|
|
46
|
+
case ".ts":
|
|
47
|
+
return ts.ScriptKind.TS;
|
|
48
|
+
case ".jsx":
|
|
49
|
+
return ts.ScriptKind.JSX;
|
|
50
|
+
default:
|
|
51
|
+
return ts.ScriptKind.JS;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** True if `node` already has a leading /** ... *\/ block right above it. */
|
|
56
|
+
function hasLeadingJSDoc(sourceFile, node) {
|
|
57
|
+
const ranges = ts.getLeadingCommentRanges(sourceFile.text, node.getFullStart()) || [];
|
|
58
|
+
return ranges.some((r) => sourceFile.text.slice(r.pos, r.pos + 3) === "/**");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Walk backwards from the node's first real token to find where its own
|
|
63
|
+
* line-indentation begins. Returns the exact insertion offset + the
|
|
64
|
+
* whitespace string to reuse for every comment line, so the comment lines
|
|
65
|
+
* up visually with the code it documents — without re-printing/reformatting
|
|
66
|
+
* the rest of the file.
|
|
67
|
+
*/
|
|
68
|
+
function getIndentAndInsertionPos(sourceFile, node) {
|
|
69
|
+
const start = node.getStart(sourceFile);
|
|
70
|
+
const text = sourceFile.text;
|
|
71
|
+
let i = start;
|
|
72
|
+
while (i > 0 && (text[i - 1] === " " || text[i - 1] === "\t")) i--;
|
|
73
|
+
return { pos: i, indent: text.slice(i, start) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function modifiersOf(node) {
|
|
77
|
+
const kinds = new Set((node.modifiers || []).map((m) => m.kind));
|
|
78
|
+
return {
|
|
79
|
+
isExported: kinds.has(ts.SyntaxKind.ExportKeyword),
|
|
80
|
+
isAsync: kinds.has(ts.SyntaxKind.AsyncKeyword),
|
|
81
|
+
isStatic: kinds.has(ts.SyntaxKind.StaticKeyword),
|
|
82
|
+
isReadonly: kinds.has(ts.SyntaxKind.ReadonlyKeyword),
|
|
83
|
+
isPrivate: kinds.has(ts.SyntaxKind.PrivateKeyword),
|
|
84
|
+
isProtected: kinds.has(ts.SyntaxKind.ProtectedKeyword),
|
|
85
|
+
isAbstract: kinds.has(ts.SyntaxKind.AbstractKeyword),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function typeText(node) {
|
|
90
|
+
return node && node.type ? node.type.getText() : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isFunctionLikeInitializer(init) {
|
|
94
|
+
return !!init && (init.kind === ts.SyntaxKind.ArrowFunction || init.kind === ts.SyntaxKind.FunctionExpression);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Purely syntactic: does this block contain a `return <value>;` at its own level (not inside a nested function)? */
|
|
98
|
+
function hasReturnWithValue(block) {
|
|
99
|
+
let found = false;
|
|
100
|
+
function walk(n) {
|
|
101
|
+
if (found) return;
|
|
102
|
+
if (ts.isReturnStatement(n)) {
|
|
103
|
+
if (n.expression) found = true;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (ts.isFunctionLike(n)) return; // don't attribute a nested closure's return to the outer function
|
|
107
|
+
ts.forEachChild(n, walk);
|
|
108
|
+
}
|
|
109
|
+
walk(block);
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Decide the @returns type with no semantics, no AI — just syntax:
|
|
115
|
+
* 1. An explicit type annotation always wins.
|
|
116
|
+
* 2. A concise arrow body (`x => x + 1`, no braces) always produces a value.
|
|
117
|
+
* 3. A block body is scanned for a top-level `return <value>;`.
|
|
118
|
+
* 4. Otherwise it's void.
|
|
119
|
+
*/
|
|
120
|
+
function inferReturnType(fnNode) {
|
|
121
|
+
const explicit = typeText(fnNode);
|
|
122
|
+
if (explicit) return explicit;
|
|
123
|
+
const body = fnNode.body;
|
|
124
|
+
if (!body) return "void";
|
|
125
|
+
if (body.kind !== ts.SyntaxKind.Block) return "any"; // concise arrow body
|
|
126
|
+
return hasReturnWithValue(body) ? "any" : "void";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Best-effort, purely syntactic type guess from an initializer expression. No semantics, no AI. */
|
|
130
|
+
function inferTypeFromInitializer(init) {
|
|
131
|
+
if (!init) return "any";
|
|
132
|
+
switch (init.kind) {
|
|
133
|
+
case ts.SyntaxKind.StringLiteral:
|
|
134
|
+
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
135
|
+
return "string";
|
|
136
|
+
case ts.SyntaxKind.NumericLiteral:
|
|
137
|
+
return "number";
|
|
138
|
+
case ts.SyntaxKind.TrueKeyword:
|
|
139
|
+
case ts.SyntaxKind.FalseKeyword:
|
|
140
|
+
return "boolean";
|
|
141
|
+
case ts.SyntaxKind.ArrayLiteralExpression:
|
|
142
|
+
return "Array";
|
|
143
|
+
case ts.SyntaxKind.ObjectLiteralExpression:
|
|
144
|
+
return "Object";
|
|
145
|
+
case ts.SyntaxKind.ArrowFunction:
|
|
146
|
+
case ts.SyntaxKind.FunctionExpression:
|
|
147
|
+
return "Function";
|
|
148
|
+
case ts.SyntaxKind.NewExpression:
|
|
149
|
+
return init.expression ? init.expression.getText() : "Object";
|
|
150
|
+
default:
|
|
151
|
+
return "any";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Comment builders — every line below comes ONLY from AST shape, never from
|
|
157
|
+
// guessing "what the code means".
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
function buildParamLines(params) {
|
|
161
|
+
return (params || []).map((p) => {
|
|
162
|
+
const name = p.name.getText();
|
|
163
|
+
const optional = !!p.questionToken || !!p.initializer;
|
|
164
|
+
const type = typeText(p) || (p.initializer ? inferTypeFromInitializer(p.initializer) : "any");
|
|
165
|
+
const label = optional ? `[${name}]` : name;
|
|
166
|
+
return ` * @param {${type}} ${label}`;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildFunctionDoc({ name, params, returnType, mods, isGenerator }) {
|
|
171
|
+
const lines = ["/**", ` * @function ${name || "anonymous"}`];
|
|
172
|
+
if (mods.isExported) lines.push(" * @exported");
|
|
173
|
+
if (mods.isAsync) lines.push(" * @async");
|
|
174
|
+
if (isGenerator) lines.push(" * @generator");
|
|
175
|
+
lines.push(...buildParamLines(params));
|
|
176
|
+
lines.push(` * @returns {${returnType || "void"}}`);
|
|
177
|
+
lines.push(" */");
|
|
178
|
+
return lines;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildClassDoc(node) {
|
|
182
|
+
const name = node.name ? node.name.getText() : "AnonymousClass";
|
|
183
|
+
const mods = modifiersOf(node);
|
|
184
|
+
const heritage = node.heritageClauses || [];
|
|
185
|
+
const lines = ["/**", ` * @class ${name}`];
|
|
186
|
+
if (mods.isExported) lines.push(" * @exported");
|
|
187
|
+
if (mods.isAbstract) lines.push(" * @abstract");
|
|
188
|
+
for (const h of heritage) {
|
|
189
|
+
const kw = h.token === ts.SyntaxKind.ExtendsKeyword ? "@extends" : "@implements";
|
|
190
|
+
for (const t of h.types) lines.push(` * ${kw} ${t.getText()}`);
|
|
191
|
+
}
|
|
192
|
+
lines.push(" */");
|
|
193
|
+
return lines;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildMethodDoc(node) {
|
|
197
|
+
const mods = modifiersOf(node);
|
|
198
|
+
const visibility = mods.isPrivate ? "private" : mods.isProtected ? "protected" : "public";
|
|
199
|
+
const name = node.name.getText();
|
|
200
|
+
const kind = ts.isGetAccessorDeclaration(node) ? "getter" : ts.isSetAccessorDeclaration(node) ? "setter" : "method";
|
|
201
|
+
const lines = ["/**", ` * @${kind} ${name}`, ` * @${visibility}`];
|
|
202
|
+
if (mods.isStatic) lines.push(" * @static");
|
|
203
|
+
if (mods.isAbstract) lines.push(" * @abstract");
|
|
204
|
+
if (mods.isAsync) lines.push(" * @async");
|
|
205
|
+
if (node.asteriskToken) lines.push(" * @generator");
|
|
206
|
+
lines.push(...buildParamLines(node.parameters));
|
|
207
|
+
lines.push(` * @returns {${inferReturnType(node)}}`);
|
|
208
|
+
lines.push(" */");
|
|
209
|
+
return lines;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildMemberDoc(node) {
|
|
213
|
+
if (ts.isConstructorDeclaration(node)) {
|
|
214
|
+
const lines = ["/**", " * @constructor"];
|
|
215
|
+
lines.push(...buildParamLines(node.parameters));
|
|
216
|
+
lines.push(" */");
|
|
217
|
+
return lines;
|
|
218
|
+
}
|
|
219
|
+
if (ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) {
|
|
220
|
+
return buildMethodDoc(node);
|
|
221
|
+
}
|
|
222
|
+
if (ts.isPropertyDeclaration(node)) {
|
|
223
|
+
const mods = modifiersOf(node);
|
|
224
|
+
const visibility = mods.isPrivate ? "private" : mods.isProtected ? "protected" : "public";
|
|
225
|
+
const name = node.name.getText();
|
|
226
|
+
const type = typeText(node) || inferTypeFromInitializer(node.initializer);
|
|
227
|
+
const lines = ["/**", ` * @member ${name}`, ` * @type {${type}}`, ` * @${visibility}`];
|
|
228
|
+
if (mods.isStatic) lines.push(" * @static");
|
|
229
|
+
if (mods.isReadonly) lines.push(" * @readonly");
|
|
230
|
+
lines.push(" */");
|
|
231
|
+
return lines;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function buildSingleVariableDoc(decl, isConst) {
|
|
237
|
+
const name = decl.name.getText();
|
|
238
|
+
const init = decl.initializer;
|
|
239
|
+
if (isFunctionLikeInitializer(init)) {
|
|
240
|
+
return buildFunctionDoc({
|
|
241
|
+
name,
|
|
242
|
+
params: init.parameters,
|
|
243
|
+
returnType: inferReturnType(init),
|
|
244
|
+
mods: { isAsync: (init.modifiers || []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) },
|
|
245
|
+
isGenerator: !!init.asteriskToken,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const type = typeText(decl) || inferTypeFromInitializer(init);
|
|
249
|
+
return ["/**", ` * @${isConst ? "constant" : "variable"} ${name}`, ` * @type {${type}}`, " */"];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function buildVariableStatementDoc(node, isConst) {
|
|
253
|
+
const decls = node.declarationList.declarations;
|
|
254
|
+
if (decls.length === 1) return buildSingleVariableDoc(decls[0], isConst);
|
|
255
|
+
const lines = ["/**"];
|
|
256
|
+
for (const decl of decls) {
|
|
257
|
+
const type = typeText(decl) || inferTypeFromInitializer(decl.initializer);
|
|
258
|
+
lines.push(` * @${isConst ? "constant" : "variable"} ${decl.name.getText()} {${type}}`);
|
|
259
|
+
}
|
|
260
|
+
lines.push(" */");
|
|
261
|
+
return lines;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildInterfaceDoc(node) {
|
|
265
|
+
const name = node.name.getText();
|
|
266
|
+
const lines = ["/**", ` * @interface ${name}`];
|
|
267
|
+
for (const m of node.members) {
|
|
268
|
+
if (ts.isPropertySignature(m) && m.name) {
|
|
269
|
+
lines.push(` * @property {${typeText(m) || "any"}} ${m.name.getText()}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
lines.push(" */");
|
|
273
|
+
return lines;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function buildTypeAliasDoc(node) {
|
|
277
|
+
return ["/**", ` * @typedef {${node.type.getText()}} ${node.name.getText()}`, " */"];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function buildEnumDoc(node) {
|
|
281
|
+
const members = node.members.map((m) => m.name.getText()).join(" | ");
|
|
282
|
+
return ["/**", ` * @enum ${node.name.getText()}`, ` * @values ${members}`, " */"];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// AST walk: collect {pos, text} edits, never mutating the source directly.
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
function collectEdits(sourceFile, force) {
|
|
290
|
+
const edits = [];
|
|
291
|
+
|
|
292
|
+
function addEdit(node, linesFn) {
|
|
293
|
+
if (!force && hasLeadingJSDoc(sourceFile, node)) return;
|
|
294
|
+
const lines = linesFn();
|
|
295
|
+
if (!lines) return;
|
|
296
|
+
const { pos, indent } = getIndentAndInsertionPos(sourceFile, node);
|
|
297
|
+
const text = lines.map((l) => indent + l).join("\n") + "\n";
|
|
298
|
+
edits.push({ pos, text });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function visit(node) {
|
|
302
|
+
if (ts.isFunctionDeclaration(node) && node.body) {
|
|
303
|
+
addEdit(node, () =>
|
|
304
|
+
buildFunctionDoc({
|
|
305
|
+
name: node.name ? node.name.getText() : null,
|
|
306
|
+
params: node.parameters,
|
|
307
|
+
returnType: inferReturnType(node),
|
|
308
|
+
mods: modifiersOf(node),
|
|
309
|
+
isGenerator: !!node.asteriskToken,
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
} else if (ts.isClassDeclaration(node)) {
|
|
313
|
+
addEdit(node, () => buildClassDoc(node));
|
|
314
|
+
} else if (
|
|
315
|
+
ts.isMethodDeclaration(node) ||
|
|
316
|
+
ts.isConstructorDeclaration(node) ||
|
|
317
|
+
ts.isPropertyDeclaration(node) ||
|
|
318
|
+
ts.isGetAccessorDeclaration(node) ||
|
|
319
|
+
ts.isSetAccessorDeclaration(node)
|
|
320
|
+
) {
|
|
321
|
+
addEdit(node, () => buildMemberDoc(node));
|
|
322
|
+
} else if (ts.isPropertyAssignment(node) && isFunctionLikeInitializer(node.initializer)) {
|
|
323
|
+
const init = node.initializer;
|
|
324
|
+
addEdit(node, () =>
|
|
325
|
+
buildFunctionDoc({
|
|
326
|
+
name: node.name.getText(),
|
|
327
|
+
params: init.parameters,
|
|
328
|
+
returnType: inferReturnType(init),
|
|
329
|
+
mods: { isAsync: (init.modifiers || []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) },
|
|
330
|
+
isGenerator: !!init.asteriskToken,
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
} else if (ts.isVariableStatement(node)) {
|
|
334
|
+
const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
|
|
335
|
+
addEdit(node, () => buildVariableStatementDoc(node, isConst));
|
|
336
|
+
} else if (ts.isInterfaceDeclaration(node)) {
|
|
337
|
+
addEdit(node, () => buildInterfaceDoc(node));
|
|
338
|
+
} else if (ts.isTypeAliasDeclaration(node)) {
|
|
339
|
+
addEdit(node, () => buildTypeAliasDoc(node));
|
|
340
|
+
} else if (ts.isEnumDeclaration(node)) {
|
|
341
|
+
addEdit(node, () => buildEnumDoc(node));
|
|
342
|
+
}
|
|
343
|
+
ts.forEachChild(node, visit);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
visit(sourceFile);
|
|
347
|
+
return edits;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// File processing
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Process a single file. Returns the number of comment blocks added.
|
|
356
|
+
* @param {string} filePath
|
|
357
|
+
* @param {{ write?: boolean, force?: boolean, silent?: boolean }} [options]
|
|
358
|
+
* @returns {number}
|
|
359
|
+
*/
|
|
360
|
+
function processFile(filePath, options = {}) {
|
|
361
|
+
const { write = false, force = false, silent = false } = options;
|
|
362
|
+
const sourceText = fs.readFileSync(filePath, "utf8");
|
|
363
|
+
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
|
|
364
|
+
|
|
365
|
+
const edits = collectEdits(sourceFile, force);
|
|
366
|
+
edits.sort((a, b) => b.pos - a.pos); // bottom-to-top so earlier offsets stay valid
|
|
367
|
+
|
|
368
|
+
let output = sourceText;
|
|
369
|
+
for (const edit of edits) {
|
|
370
|
+
output = output.slice(0, edit.pos) + edit.text + output.slice(edit.pos);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const outPath = write ? filePath : filePath.replace(/(\.[jt]sx?)$/, "$1");
|
|
374
|
+
if (edits.length > 0 || write) {
|
|
375
|
+
fs.writeFileSync(outPath, output, "utf8");
|
|
376
|
+
}
|
|
377
|
+
if (!silent) {
|
|
378
|
+
console.log(` ${filePath} -> ${outPath} (${edits.length} block${edits.length === 1 ? "" : "s"})`);
|
|
379
|
+
}
|
|
380
|
+
return edits.length;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Directory / project-wide scanning
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Recursively collect all matching source files under a path.
|
|
389
|
+
* If `inputPath` is itself a file, returns it (if its extension matches).
|
|
390
|
+
* @param {string} inputPath
|
|
391
|
+
* @param {string[]} [extensions]
|
|
392
|
+
* @param {Set<string>} [ignoreDirs]
|
|
393
|
+
* @returns {string[]}
|
|
394
|
+
*/
|
|
395
|
+
function collectFiles(inputPath, extensions = DEFAULT_EXTENSIONS, ignoreDirs = DEFAULT_IGNORE_DIRS, ignorePatterns = []) {
|
|
396
|
+
function matchesIgnore(fullPath) {
|
|
397
|
+
const normalised = fullPath.replace(/\\/g, "/");
|
|
398
|
+
return ignorePatterns.some(pat => {
|
|
399
|
+
const norm = pat.replace(/\\/g, "/").replace(/^\.?\/?/, "");
|
|
400
|
+
if (norm.startsWith("**/")) {
|
|
401
|
+
const suffix = norm.slice(3);
|
|
402
|
+
const re = new RegExp(suffix.replace(/\./g, "\\.").replace(/\*/g, "[^/]*") + "$");
|
|
403
|
+
return re.test(normalised);
|
|
404
|
+
}
|
|
405
|
+
const re = new RegExp(norm.replace(/\./g, "\\.").replace(/\*/g, "[^/]*") + "$");
|
|
406
|
+
return re.test(normalised);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const stat = fs.statSync(inputPath);
|
|
411
|
+
if (stat.isFile()) {
|
|
412
|
+
if (matchesIgnore(inputPath)) return [];
|
|
413
|
+
return extensions.includes(path.extname(inputPath).toLowerCase()) ? [inputPath] : [];
|
|
414
|
+
}
|
|
415
|
+
if (!stat.isDirectory()) return [];
|
|
416
|
+
|
|
417
|
+
const results = [];
|
|
418
|
+
function walk(dir) {
|
|
419
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
420
|
+
const full = path.join(dir, entry.name);
|
|
421
|
+
if (entry.isDirectory()) {
|
|
422
|
+
if (ignoreDirs.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
423
|
+
if (matchesIgnore(full)) continue;
|
|
424
|
+
walk(full);
|
|
425
|
+
} else if (entry.isFile()) {
|
|
426
|
+
const lower = entry.name.toLowerCase();
|
|
427
|
+
if (lower.endsWith(".d.ts")) continue;
|
|
428
|
+
if (matchesIgnore(full)) continue;
|
|
429
|
+
if (extensions.includes(path.extname(lower))) results.push(full);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
walk(inputPath);
|
|
434
|
+
return results;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
module.exports = {
|
|
438
|
+
processFile,
|
|
439
|
+
collectFiles,
|
|
440
|
+
collectEdits,
|
|
441
|
+
DEFAULT_EXTENSIONS,
|
|
442
|
+
DEFAULT_IGNORE_DIRS,
|
|
443
|
+
};
|