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.
@@ -0,0 +1,456 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const ts = require("typescript");
5
+
6
+ function getScriptKind(file) {
7
+ switch (path.extname(file).toLowerCase()) {
8
+ case ".tsx": return ts.ScriptKind.TSX;
9
+ case ".ts": return ts.ScriptKind.TS;
10
+ case ".jsx": return ts.ScriptKind.JSX;
11
+ default: return ts.ScriptKind.JS;
12
+ }
13
+ }
14
+
15
+ function modifiersOf(node) {
16
+ const kinds = new Set((node.modifiers || []).map(m => m.kind));
17
+ return {
18
+ isExported: kinds.has(ts.SyntaxKind.ExportKeyword),
19
+ isAsync: kinds.has(ts.SyntaxKind.AsyncKeyword),
20
+ isStatic: kinds.has(ts.SyntaxKind.StaticKeyword),
21
+ isReadonly: kinds.has(ts.SyntaxKind.ReadonlyKeyword),
22
+ isPrivate: kinds.has(ts.SyntaxKind.PrivateKeyword),
23
+ isProtected: kinds.has(ts.SyntaxKind.ProtectedKeyword),
24
+ isAbstract: kinds.has(ts.SyntaxKind.AbstractKeyword),
25
+ };
26
+ }
27
+
28
+ function typeText(node) { return node && node.type ? node.type.getText() : null; }
29
+
30
+ function isFunctionLike(init) {
31
+ return !!init && (init.kind === ts.SyntaxKind.ArrowFunction || init.kind === ts.SyntaxKind.FunctionExpression);
32
+ }
33
+
34
+ function hasReturnWithValue(block) {
35
+ let found = false;
36
+ function walk(n) {
37
+ if (found) return;
38
+ if (ts.isReturnStatement(n)) { if (n.expression) found = true; return; }
39
+ if (ts.isFunctionLike(n)) return;
40
+ ts.forEachChild(n, walk);
41
+ }
42
+ walk(block);
43
+ return found;
44
+ }
45
+
46
+ function inferReturnType(fnNode) {
47
+ const explicit = typeText(fnNode);
48
+ if (explicit) return explicit;
49
+ const body = fnNode.body;
50
+ if (!body) return "void";
51
+ if (body.kind !== ts.SyntaxKind.Block) return "any";
52
+ return hasReturnWithValue(body) ? "any" : "void";
53
+ }
54
+
55
+ function inferTypeFromInitializer(init) {
56
+ if (!init) return "any";
57
+ switch (init.kind) {
58
+ case ts.SyntaxKind.StringLiteral:
59
+ case ts.SyntaxKind.NoSubstitutionTemplateLiteral: return "string";
60
+ case ts.SyntaxKind.NumericLiteral: return "number";
61
+ case ts.SyntaxKind.TrueKeyword:
62
+ case ts.SyntaxKind.FalseKeyword: return "boolean";
63
+ case ts.SyntaxKind.ArrayLiteralExpression: return "Array";
64
+ case ts.SyntaxKind.ObjectLiteralExpression: return "Object";
65
+ case ts.SyntaxKind.ArrowFunction:
66
+ case ts.SyntaxKind.FunctionExpression: return "Function";
67
+ case ts.SyntaxKind.NewExpression:
68
+ return init.expression ? init.expression.getText() : "Object";
69
+ default: return "any";
70
+ }
71
+ }
72
+
73
+ function extractParams(params) {
74
+ return (params || []).map(p => ({
75
+ name: p.name.getText(),
76
+ type: typeText(p) || (p.initializer ? inferTypeFromInitializer(p.initializer) : "any"),
77
+ optional: !!p.questionToken || !!p.initializer,
78
+ }));
79
+ }
80
+
81
+ function getLineNumber(sourceFile, node) {
82
+ try { return sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1; }
83
+ catch (_) { return null; }
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // JSDoc comment extraction
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /** Empty jsdoc result used as default */
91
+ function emptyJSDoc() {
92
+ return { description: null, example: null, since: null, deprecated: null, params: [], returns: null, throws: [] };
93
+ }
94
+
95
+ /**
96
+ * Parse a raw /** ... *\/ comment block.
97
+ * Returns: { description, example, since, deprecated, params, returns, throws }
98
+ * - description: text before the first @tag line
99
+ * - example: text after @example
100
+ * - since: string after @since
101
+ * - deprecated: message after @deprecated (or empty string if bare tag)
102
+ * - params: [{ name, type, description }] from @param {type} name desc
103
+ * - returns: { type, description } from @returns {type} desc
104
+ * - throws: [{ type, description }] from @throws {type} desc
105
+ */
106
+ function parseJSDocBlock(raw) {
107
+ const lines = raw
108
+ .replace(/^\/\*\*/, "")
109
+ .replace(/\*\/$/, "")
110
+ .split("\n")
111
+ .map(l => l.replace(/^\s*\*\s?/, "").trimEnd());
112
+
113
+ const descLines = [];
114
+ let exampleLines = null;
115
+ let inExample = false;
116
+ let since = null;
117
+ let deprecated = null;
118
+ const params = [];
119
+ let returns = null;
120
+ const throws = [];
121
+
122
+ // Helper: extract {type} from start of rest string; returns { type, rest }
123
+ function extractBraced(str) {
124
+ const m = str.match(/^\{([^}]*)\}\s*(.*)/);
125
+ if (m) return { type: m[1].trim(), rest: m[2] };
126
+ return { type: "any", rest: str };
127
+ }
128
+
129
+ for (const line of lines) {
130
+ if (/^@example\b/.test(line)) {
131
+ inExample = true;
132
+ exampleLines = exampleLines || [];
133
+ const rest = line.slice("@example".length).trim();
134
+ if (rest) exampleLines.push(rest);
135
+ } else if (/^@param\b/.test(line)) {
136
+ inExample = false;
137
+ const rest = line.slice("@param".length).trim();
138
+ const { type, rest: nameAndDesc } = extractBraced(rest);
139
+ const m2 = nameAndDesc.match(/^(\[?[\w.]+\]?)\s*(.*)/);
140
+ if (m2) {
141
+ const rawName = m2[1];
142
+ const optional = rawName.startsWith("[") && rawName.endsWith("]");
143
+ const name = rawName.replace(/^\[|\]$/g, "");
144
+ params.push({ name, type, description: m2[2].trim() || null, optional });
145
+ }
146
+ } else if (/^@returns?\b/.test(line)) {
147
+ inExample = false;
148
+ const rest = line.replace(/^@returns?\s*/, "");
149
+ const { type, rest: desc } = extractBraced(rest);
150
+ returns = { type, description: desc.trim() || null };
151
+ } else if (/^@throws?\b/.test(line)) {
152
+ inExample = false;
153
+ const rest = line.replace(/^@throws?\s*/, "");
154
+ const { type, rest: desc } = extractBraced(rest);
155
+ throws.push({ type, description: desc.trim() || null });
156
+ } else if (/^@since\b/.test(line)) {
157
+ inExample = false;
158
+ since = line.slice("@since".length).trim() || null;
159
+ } else if (/^@deprecated\b/.test(line)) {
160
+ inExample = false;
161
+ deprecated = line.slice("@deprecated".length).trim() || "";
162
+ } else if (/^@\w/.test(line)) {
163
+ inExample = false;
164
+ } else if (inExample) {
165
+ exampleLines.push(line);
166
+ } else if (descLines.length === 0 && line.trim() === "") {
167
+ // skip leading blanks
168
+ } else {
169
+ descLines.push(line);
170
+ }
171
+ }
172
+
173
+ while (descLines.length && descLines[descLines.length - 1].trim() === "") descLines.pop();
174
+ const description = descLines.join("\n").trim() || null;
175
+
176
+ let example = null;
177
+ if (exampleLines) {
178
+ while (exampleLines.length && exampleLines[0].trim() === "") exampleLines.shift();
179
+ while (exampleLines.length && exampleLines[exampleLines.length - 1].trim() === "") exampleLines.pop();
180
+ example = exampleLines.join("\n").trim() || null;
181
+ }
182
+
183
+ return { description, example, since, deprecated, params, returns, throws };
184
+ }
185
+
186
+ /**
187
+ * Read the nearest leading /** ... *\/ block for a node.
188
+ */
189
+ function readJSDoc(sourceFile, node) {
190
+ const ranges = ts.getLeadingCommentRanges(sourceFile.text, node.getFullStart()) || [];
191
+ for (let i = ranges.length - 1; i >= 0; i--) {
192
+ const r = ranges[i];
193
+ const text = sourceFile.text.slice(r.pos, r.end);
194
+ if (text.startsWith("/**")) return parseJSDocBlock(text);
195
+ }
196
+ return emptyJSDoc();
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Module-level JSDoc
201
+ // ---------------------------------------------------------------------------
202
+
203
+ /**
204
+ * Extract top-of-file /** @module ... *\/ block.
205
+ * Returns { moduleName, description, since } or all-null.
206
+ */
207
+ function extractModuleDoc(sourceFile) {
208
+ // Walk the first few statements looking for a /** block
209
+ const text = sourceFile.text;
210
+ const ranges = ts.getLeadingCommentRanges(text, 0) || [];
211
+ for (const r of ranges) {
212
+ const block = text.slice(r.pos, r.end);
213
+ if (!block.startsWith("/**")) continue;
214
+ const parsed = parseJSDocBlock(block);
215
+ // Extract @module tag from raw block
216
+ const modMatch = block.match(/@module\s+([^\s*]+)/);
217
+ return {
218
+ moduleName: modMatch ? modMatch[1].trim() : null,
219
+ description: parsed.description,
220
+ since: parsed.since,
221
+ };
222
+ }
223
+ return { moduleName: null, description: null, since: null };
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // Item extractors
228
+ // ---------------------------------------------------------------------------
229
+
230
+ function extractFunction(node, sourceFile, nameOverride) {
231
+ const mods = modifiersOf(node);
232
+ const jsdoc = sourceFile ? readJSDoc(sourceFile, node) : emptyJSDoc();
233
+ return {
234
+ name: nameOverride || (node.name ? node.name.getText() : "anonymous"),
235
+ line: sourceFile ? getLineNumber(sourceFile, node) : null,
236
+ description: jsdoc.description,
237
+ example: jsdoc.example,
238
+ since: jsdoc.since,
239
+ deprecated: jsdoc.deprecated,
240
+ jsdocParams: jsdoc.params,
241
+ returns: jsdoc.returns,
242
+ throws: jsdoc.throws,
243
+ params: extractParams(node.parameters),
244
+ returnType: inferReturnType(node),
245
+ isExported: mods.isExported,
246
+ isAsync: mods.isAsync,
247
+ isGenerator: !!node.asteriskToken,
248
+ };
249
+ }
250
+
251
+ function extractClass(node, sourceFile) {
252
+ const mods = modifiersOf(node);
253
+ const name = node.name ? node.name.getText() : "AnonymousClass";
254
+ const jsdoc = sourceFile ? readJSDoc(sourceFile, node) : emptyJSDoc();
255
+ const result = {
256
+ name,
257
+ line: sourceFile ? getLineNumber(sourceFile, node) : null,
258
+ description: jsdoc.description,
259
+ example: jsdoc.example,
260
+ since: jsdoc.since,
261
+ deprecated: jsdoc.deprecated,
262
+ isExported: mods.isExported,
263
+ isAbstract: mods.isAbstract,
264
+ extends: [],
265
+ implements: [],
266
+ constructor: null,
267
+ methods: [],
268
+ getters: [],
269
+ setters: [],
270
+ properties: [],
271
+ };
272
+
273
+ for (const h of (node.heritageClauses || [])) {
274
+ const target = h.token === ts.SyntaxKind.ExtendsKeyword ? result.extends : result.implements;
275
+ for (const t of h.types) target.push(t.getText());
276
+ }
277
+
278
+ for (const m of node.members) {
279
+ const mjsdoc = sourceFile ? readJSDoc(sourceFile, m) : emptyJSDoc();
280
+ if (ts.isConstructorDeclaration(m)) {
281
+ result.constructor = {
282
+ params: extractParams(m.parameters),
283
+ description: mjsdoc.description,
284
+ jsdocParams: mjsdoc.params,
285
+ throws: mjsdoc.throws,
286
+ };
287
+ } else if (ts.isMethodDeclaration(m)) {
288
+ const mm = modifiersOf(m);
289
+ const visibility = mm.isPrivate ? "private" : mm.isProtected ? "protected" : "public";
290
+ result.methods.push({
291
+ name: m.name.getText(),
292
+ description: mjsdoc.description,
293
+ since: mjsdoc.since,
294
+ deprecated: mjsdoc.deprecated,
295
+ returns: mjsdoc.returns,
296
+ throws: mjsdoc.throws,
297
+ jsdocParams: mjsdoc.params,
298
+ visibility,
299
+ isStatic: mm.isStatic,
300
+ isAbstract: mm.isAbstract,
301
+ isAsync: mm.isAsync,
302
+ isGenerator: !!m.asteriskToken,
303
+ params: extractParams(m.parameters),
304
+ returnType: inferReturnType(m),
305
+ });
306
+ } else if (ts.isGetAccessorDeclaration(m)) {
307
+ const mm = modifiersOf(m);
308
+ result.getters.push({ name: m.name.getText(), returnType: inferReturnType(m), isStatic: mm.isStatic, description: mjsdoc.description, deprecated: mjsdoc.deprecated, since: mjsdoc.since });
309
+ } else if (ts.isSetAccessorDeclaration(m)) {
310
+ const mm = modifiersOf(m);
311
+ result.setters.push({ name: m.name.getText(), params: extractParams(m.parameters), isStatic: mm.isStatic, description: mjsdoc.description, deprecated: mjsdoc.deprecated });
312
+ } else if (ts.isPropertyDeclaration(m)) {
313
+ const mm = modifiersOf(m);
314
+ const visibility = mm.isPrivate ? "private" : mm.isProtected ? "protected" : "public";
315
+ result.properties.push({
316
+ name: m.name.getText(),
317
+ type: typeText(m) || inferTypeFromInitializer(m.initializer),
318
+ visibility,
319
+ isStatic: mm.isStatic,
320
+ isReadonly: mm.isReadonly,
321
+ isAbstract: mm.isAbstract,
322
+ description: mjsdoc.description,
323
+ deprecated: mjsdoc.deprecated,
324
+ since: mjsdoc.since,
325
+ });
326
+ }
327
+ }
328
+
329
+ return result;
330
+ }
331
+
332
+ function extractInterface(node, sourceFile) {
333
+ const jsdoc = sourceFile ? readJSDoc(sourceFile, node) : emptyJSDoc();
334
+ return {
335
+ name: node.name.getText(),
336
+ line: sourceFile ? getLineNumber(sourceFile, node) : null,
337
+ description: jsdoc.description,
338
+ example: jsdoc.example,
339
+ since: jsdoc.since,
340
+ deprecated: jsdoc.deprecated,
341
+ isExported: modifiersOf(node).isExported,
342
+ properties: node.members
343
+ .filter(m => ts.isPropertySignature(m) && m.name)
344
+ .map(m => ({
345
+ name: m.name.getText(),
346
+ type: typeText(m) || "any",
347
+ optional: !!m.questionToken,
348
+ })),
349
+ methods: node.members
350
+ .filter(m => ts.isMethodSignature(m) && m.name)
351
+ .map(m => ({
352
+ name: m.name.getText(),
353
+ params: extractParams(m.parameters),
354
+ returnType: typeText(m) || "void",
355
+ optional: !!m.questionToken,
356
+ })),
357
+ };
358
+ }
359
+
360
+ function extractTypeAlias(node, sourceFile) {
361
+ const jsdoc = sourceFile ? readJSDoc(sourceFile, node) : emptyJSDoc();
362
+ return {
363
+ name: node.name.getText(),
364
+ line: sourceFile ? getLineNumber(sourceFile, node) : null,
365
+ description: jsdoc.description,
366
+ since: jsdoc.since,
367
+ deprecated: jsdoc.deprecated,
368
+ type: node.type.getText(),
369
+ isExported: modifiersOf(node).isExported,
370
+ };
371
+ }
372
+
373
+ function extractEnum(node, sourceFile) {
374
+ const jsdoc = sourceFile ? readJSDoc(sourceFile, node) : emptyJSDoc();
375
+ return {
376
+ name: node.name.getText(),
377
+ line: sourceFile ? getLineNumber(sourceFile, node) : null,
378
+ description: jsdoc.description,
379
+ since: jsdoc.since,
380
+ deprecated: jsdoc.deprecated,
381
+ isExported: modifiersOf(node).isExported,
382
+ members: node.members.map(m => ({
383
+ name: m.name.getText(),
384
+ value: m.initializer ? m.initializer.getText() : null,
385
+ })),
386
+ };
387
+ }
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // Main entry
391
+ // ---------------------------------------------------------------------------
392
+
393
+ function extractModule(filePath) {
394
+ const fs = require("fs");
395
+ const sourceText = fs.readFileSync(filePath, "utf8");
396
+ const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
397
+
398
+ const moduleDoc = extractModuleDoc(sourceFile);
399
+ const result = { filePath, moduleName: moduleDoc.moduleName, description: moduleDoc.description, since: moduleDoc.since, functions: [], classes: [], interfaces: [], typeAliases: [], enums: [], variables: [] };
400
+
401
+ function visit(node) {
402
+ if (ts.isFunctionDeclaration(node) && node.body && node.name) {
403
+ result.functions.push(extractFunction(node, sourceFile));
404
+ } else if (ts.isClassDeclaration(node)) {
405
+ result.classes.push(extractClass(node, sourceFile));
406
+ } else if (ts.isInterfaceDeclaration(node)) {
407
+ result.interfaces.push(extractInterface(node, sourceFile));
408
+ } else if (ts.isTypeAliasDeclaration(node)) {
409
+ result.typeAliases.push(extractTypeAlias(node, sourceFile));
410
+ } else if (ts.isEnumDeclaration(node)) {
411
+ result.enums.push(extractEnum(node, sourceFile));
412
+ } else if (ts.isVariableStatement(node)) {
413
+ const mods = modifiersOf(node);
414
+ const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
415
+ const stmtJsdoc = sourceFile ? readJSDoc(sourceFile, node) : emptyJSDoc();
416
+ for (const decl of node.declarationList.declarations) {
417
+ const init = decl.initializer;
418
+ if (isFunctionLike(init)) {
419
+ result.functions.push({
420
+ name: decl.name.getText(),
421
+ line: sourceFile ? getLineNumber(sourceFile, decl) : null,
422
+ description: stmtJsdoc.description,
423
+ example: stmtJsdoc.example,
424
+ since: stmtJsdoc.since,
425
+ deprecated: stmtJsdoc.deprecated,
426
+ jsdocParams: stmtJsdoc.params,
427
+ returns: stmtJsdoc.returns,
428
+ throws: stmtJsdoc.throws,
429
+ params: extractParams(init.parameters),
430
+ returnType: inferReturnType(init),
431
+ isExported: mods.isExported,
432
+ isAsync: (init.modifiers || []).some(m => m.kind === ts.SyntaxKind.AsyncKeyword),
433
+ isGenerator: !!init.asteriskToken,
434
+ });
435
+ } else {
436
+ result.variables.push({
437
+ name: decl.name.getText(),
438
+ line: sourceFile ? getLineNumber(sourceFile, decl) : null,
439
+ description: stmtJsdoc.description,
440
+ since: stmtJsdoc.since,
441
+ deprecated: stmtJsdoc.deprecated,
442
+ type: typeText(decl) || inferTypeFromInitializer(init),
443
+ isConst,
444
+ isExported: mods.isExported,
445
+ });
446
+ }
447
+ }
448
+ }
449
+ ts.forEachChild(node, visit);
450
+ }
451
+
452
+ visit(sourceFile);
453
+ return result;
454
+ }
455
+
456
+ module.exports = { extractModule };