jsdoc-scribe 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chintan Goswami
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # jsdoc-scribe
2
+
3
+ Pure, deterministic, **AST-based** JSDoc comment generator for JavaScript & TypeScript.
4
+ **No AI / LLM is used anywhere** — every line of every comment is derived
5
+ mechanically from the syntax tree (names, modifiers, type annotations,
6
+ parameter lists, heritage clauses, enum members, etc). Same input always
7
+ produces the same output.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ # run once without installing anything
13
+ npx jsdoc-scribe . --write
14
+
15
+ # or add it to your project
16
+ npm install --save-dev jsdoc-scribe
17
+
18
+ # or install it globally
19
+ npm install -g jsdoc-scribe
20
+ ```
21
+
22
+ Once installed, the command is **`gen-comments`**.
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ gen-comments <path> [path2 ...] [options]
28
+ ```
29
+
30
+ `<path>` can be a single file **or a directory** — directories are scanned
31
+ recursively for `.js` / `.jsx` / `.ts` / `.tsx` files. `node_modules`, `.git`,
32
+ `dist`, `build`, `out`, `coverage`, `.next`, `.turbo`, `.cache`, and any other
33
+ dotfolder are skipped automatically.
34
+
35
+ | Flag | Description |
36
+ |---|---|
37
+ | `--write`, `-w` | Edit files **in place**. Without this flag, output goes to a sibling `<name>.commented.<ext>` file next to each original, so you can review a diff before committing to it. |
38
+ | `--force`, `-f` | Re-insert comment blocks even on nodes that already have a leading `/** */`. Off by default, to stay idempotent. |
39
+ | `--help`, `-h` | Show usage. |
40
+ | `--version`, `-v` | Show the installed version. |
41
+
42
+ ```bash
43
+ gen-comments src/utils.ts # preview only -> utils.commented.ts
44
+ gen-comments . # scan whole project, preview only
45
+ gen-comments . --write # scan whole project, edit in place
46
+ gen-comments src --write --force # also re-document already-commented files
47
+ ```
48
+
49
+ If you run `--write` outside a git repo, the CLI prints a one-line warning
50
+ (not a blocker) recommending you commit first so you have something to diff
51
+ or revert against.
52
+
53
+ ## The algorithm
54
+
55
+ 1. **Parse** each file into a `ts.SourceFile` AST using the TypeScript
56
+ compiler's parser (`typescript` npm package). That parser is syntax-only —
57
+ it never type-checks — and it's a superset parser for JavaScript, which is
58
+ why this works on plain `.js`/`.jsx` just as well as `.ts`/`.tsx`.
59
+ 2. **Walk** the AST recursively. Tracked node kinds:
60
+ - `FunctionDeclaration` (top-level or nested)
61
+ - `ClassDeclaration` + its `constructor` / methods / properties / get-set
62
+ - `VariableStatement` (`const`/`let`/`var`, including arrow/function inits)
63
+ - `PropertyAssignment` with a function/arrow value inside an object literal
64
+ - `InterfaceDeclaration`, `TypeAliasDeclaration`, `EnumDeclaration`
65
+ 3. **Skip** any node that already has a leading `/** ... */` block (checked
66
+ via `ts.getLeadingCommentRanges`) — unless `--force` is passed. This makes
67
+ the tool idempotent: running it twice never duplicates comments.
68
+ 4. **Build** the comment block from pure syntax only:
69
+ - An explicit type annotation is always used as-is.
70
+ - With no annotation, it falls back to a syntactic guess: literal kind for
71
+ variables (`'x'` → `string`, `[1,2]` → `Array`, …), and for function
72
+ return types, a scan for a top-level `return <value>;` (so a function
73
+ that clearly returns something is never mislabeled `void` just because
74
+ it lacks a type annotation).
75
+ - Modifiers (`async`, `static`, `private`, `readonly`, `abstract`,
76
+ `export`, generator `*`) are read directly off the AST node.
77
+ 5. **Insert** the comment as plain text at the exact byte offset where the
78
+ node's own line indentation begins. All edits across a file are collected
79
+ first, then applied **bottom-to-top** so earlier offsets never shift.
80
+ The rest of the file is never re-printed or reformatted — only insertions
81
+ happen.
82
+ 6. **Write** the result — to `<file>.commented.<ext>` by default, or back to
83
+ the original file with `--write`.
84
+
85
+ ## Using it as a library
86
+
87
+ ```js
88
+ const { processFile, collectFiles } = require('jsdoc-scribe');
89
+
90
+ // process one file, return number of comment blocks added
91
+ processFile('src/utils.ts', { write: true });
92
+
93
+ // recursively find every matching source file under a directory
94
+ const files = collectFiles('src');
95
+ ```
96
+
97
+ ## Known scope / limitations (by design)
98
+
99
+ - Inline anonymous callbacks passed directly as call arguments
100
+ (`arr.map(x => x * 2)`) are **not** commented — inserting a multi-line block
101
+ there would mangle the call expression. Anything with its own declaration
102
+ (function decl, class member, variable, named object property) is covered.
103
+ - Type/return inference is 100% syntactic. It is never "smart" about what
104
+ your code *means* — only what it *looks like* structurally. That's the
105
+ whole point: deterministic, reproducible output, every time.
106
+ - Multi-declarator statements (`const a = 1, b = 2;`) get one combined block
107
+ rather than a per-declarator function-style doc.
108
+ - `.d.ts` files are skipped (no implementation to document).
109
+
110
+ ## Development
111
+
112
+ ```bash
113
+ git clone <your-repo-url>
114
+ cd jsdoc-scribe
115
+ npm install
116
+ npm test # runs the self-test suite (test/run.js)
117
+ npm run demo # runs the CLI against the bundled example/ files
118
+ ```
119
+
120
+ ## Publishing new versions
121
+
122
+ ```bash
123
+ npm version patch # or minor / major
124
+ npm publish
125
+ ```
126
+
127
+ `prepublishOnly` runs the test suite automatically before every publish.
128
+
129
+ ## License
130
+
131
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { processFile, collectFiles } = require("../lib/index.js");
7
+ const pkg = require("../package.json");
8
+
9
+ function printHelp() {
10
+ console.log(`
11
+ ${pkg.name} v${pkg.version}
12
+ Pure, AST-based JSDoc comment generator for JavaScript & TypeScript. No AI involved.
13
+
14
+ Usage:
15
+ gen-comments <path> [path2 ...] [options]
16
+
17
+ <path> can be a single file OR a directory (scanned recursively for
18
+ .js/.jsx/.ts/.tsx files; node_modules, .git, dist, build, etc. are skipped).
19
+
20
+ Options:
21
+ --write, -w Overwrite files in place.
22
+ (default: writes a sibling "<name>.commented.<ext>" file
23
+ next to each original, so you can review a diff first)
24
+ --force, -f Add comment blocks even on nodes that already have one.
25
+ --help, -h Show this help.
26
+ --version, -v Show the installed version.
27
+
28
+ Examples:
29
+ gen-comments src/utils.ts # preview only, writes utils.commented.ts
30
+ gen-comments . # scan whole project, preview only
31
+ gen-comments . --write # scan whole project, edit files in place
32
+ gen-comments src --write --force # re-document files that already have JSDoc
33
+ `);
34
+ }
35
+
36
+ function parseArgs(argv) {
37
+ const args = { inputs: [], write: false, force: false, help: false, version: false };
38
+ for (const a of argv) {
39
+ if (a === "--write" || a === "-w") args.write = true;
40
+ else if (a === "--force" || a === "-f") args.force = true;
41
+ else if (a === "--help" || a === "-h") args.help = true;
42
+ else if (a === "--version" || a === "-v") args.version = true;
43
+ else args.inputs.push(a);
44
+ }
45
+ return args;
46
+ }
47
+
48
+ function isInsideGitRepo(startDir) {
49
+ let dir = path.resolve(startDir);
50
+ while (true) {
51
+ if (fs.existsSync(path.join(dir, ".git"))) return true;
52
+ const parent = path.dirname(dir);
53
+ if (parent === dir) return false;
54
+ dir = parent;
55
+ }
56
+ }
57
+
58
+ function main() {
59
+ const argv = process.argv.slice(2);
60
+ const { inputs, write, force, help, version } = parseArgs(argv);
61
+
62
+ if (version) {
63
+ console.log(pkg.version);
64
+ return;
65
+ }
66
+ if (help || inputs.length === 0) {
67
+ printHelp();
68
+ process.exitCode = inputs.length === 0 && !help ? 1 : 0;
69
+ return;
70
+ }
71
+
72
+ if (write && !isInsideGitRepo(process.cwd())) {
73
+ console.warn(
74
+ "⚠ --write will edit files in place and this folder is not (or you are not inside) a git repo.\n" +
75
+ " Consider committing your work first so you have something to diff/revert against.\n",
76
+ );
77
+ }
78
+
79
+ let files = [];
80
+ for (const input of inputs) {
81
+ if (!fs.existsSync(input)) {
82
+ console.error(`skip: path not found - ${input}`);
83
+ continue;
84
+ }
85
+ files.push(...collectFiles(input));
86
+ }
87
+ files = [...new Set(files)];
88
+
89
+ if (files.length === 0) {
90
+ console.log("No matching .js/.jsx/.ts/.tsx files found.");
91
+ return;
92
+ }
93
+
94
+ console.log(`Scanning ${files.length} file(s)...`);
95
+ let totalBlocks = 0;
96
+ let touchedFiles = 0;
97
+ for (const file of files) {
98
+ try {
99
+ const count = processFile(file, { write, force });
100
+ totalBlocks += count;
101
+ if (count > 0) touchedFiles += 1;
102
+ } catch (err) {
103
+ console.error(` ${file} -> FAILED: ${err.message}`);
104
+ }
105
+ }
106
+
107
+ console.log(
108
+ `\nDone. ${totalBlocks} comment block(s) added across ${touchedFiles} file(s) ` + `(${files.length} scanned).`,
109
+ );
110
+ if (!write) {
111
+ console.log("This was a preview run — pass --write to edit the original files in place.");
112
+ }
113
+ }
114
+
115
+ main();
package/lib/index.js ADDED
@@ -0,0 +1,427 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * jsdoc-scribe — 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 mods = (node.modifiers || []).map((m) => ts.SyntaxKind[m.kind]);
78
+ return {
79
+ isExported: mods.includes("ExportKeyword"),
80
+ isAsync: mods.includes("AsyncKeyword"),
81
+ isStatic: mods.includes("StaticKeyword"),
82
+ isReadonly: mods.includes("ReadonlyKeyword"),
83
+ isPrivate: mods.includes("PrivateKeyword"),
84
+ isProtected: mods.includes("ProtectedKeyword"),
85
+ isAbstract: mods.includes("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?)$/, ".commented$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) {
396
+ const stat = fs.statSync(inputPath);
397
+ if (stat.isFile()) {
398
+ return extensions.includes(path.extname(inputPath).toLowerCase()) ? [inputPath] : [];
399
+ }
400
+ if (!stat.isDirectory()) return [];
401
+
402
+ const results = [];
403
+ function walk(dir) {
404
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
405
+ const full = path.join(dir, entry.name);
406
+ if (entry.isDirectory()) {
407
+ if (ignoreDirs.has(entry.name) || entry.name.startsWith(".")) continue;
408
+ walk(full);
409
+ } else if (entry.isFile()) {
410
+ const lower = entry.name.toLowerCase();
411
+ if (lower.endsWith(".d.ts")) continue; // type-declaration files have no implementations to document
412
+ if (/\.commented\.[jt]sx?$/.test(lower)) continue; // skip our own previous output
413
+ if (extensions.includes(path.extname(lower))) results.push(full);
414
+ }
415
+ }
416
+ }
417
+ walk(inputPath);
418
+ return results;
419
+ }
420
+
421
+ module.exports = {
422
+ processFile,
423
+ collectFiles,
424
+ collectEdits,
425
+ DEFAULT_EXTENSIONS,
426
+ DEFAULT_IGNORE_DIRS,
427
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "jsdoc-scribe",
3
+ "version": "1.0.0",
4
+ "description": "Pure AST-based JSDoc comment generator for JS/TS - no AI involved.",
5
+ "main": "lib/index.js",
6
+ "bin": {
7
+ "gen-comments": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "lib",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "demo": "node bin/cli.js example",
17
+ "test": "node test/run.js",
18
+ "prepublishOnly": "npm test"
19
+ },
20
+ "keywords": [
21
+ "jsdoc",
22
+ "comments",
23
+ "documentation",
24
+ "typescript",
25
+ "javascript",
26
+ "cli",
27
+ "ast",
28
+ "code-documentation",
29
+ "comment-generator",
30
+ "autodoc"
31
+ ],
32
+ "author": {
33
+ "name": "Chintan Goswami"
34
+ },
35
+ "license": "MIT",
36
+ "engines": {
37
+ "node": ">=14"
38
+ },
39
+ "dependencies": {
40
+ "typescript": ">=5.0.0"
41
+ }
42
+ }