@runtime-type-inspector/repl 3.2.2 → 3.2.4

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,31 @@
1
+ //import {transpile} from "ts-to-jsdoc";
2
+ import {transpile} from "./ts-to-jsdoc.js";
3
+ const ret = transpile(`
4
+
5
+ import type {
6
+ ImportDeclaration,
7
+ ClassDeclaration,
8
+ FunctionLikeDeclaration,
9
+ InterfaceDeclaration,
10
+ JSDoc,
11
+ JSDocableNode,
12
+ MethodDeclaration,
13
+ ModifierableNode,
14
+ PropertyAssignment,
15
+ PropertyDeclaration,
16
+ PropertySignature,
17
+ TypeAliasDeclaration,
18
+ TypedNode,
19
+ VariableDeclaration,
20
+ } from "ts-morph";
21
+
22
+ import type {
23
+ A,B
24
+ } from "abc";
25
+
26
+ /***/
27
+ const groups: Record<string, string[]> = {
28
+ test: ['a', 'b']
29
+ } as const;
30
+ `);
31
+ console.log(ret);
package/ts-to-jsdoc.js ADDED
@@ -0,0 +1,460 @@
1
+ import path from "path";
2
+ //import * as ts from 'typescript';
3
+ //debugger;
4
+ //const ScriptTarget = ts.ScriptTarget;
5
+ import * as tsDefault from 'typescript';
6
+ const ts = tsDefault.default;
7
+ import {
8
+ Node,
9
+ Project,
10
+ //ScriptTarget,
11
+ //SyntaxKind
12
+ } from "./ts-morph.js";
13
+ const {
14
+ //Project,
15
+ ScriptTarget,
16
+ SyntaxKind,
17
+ } = ts;
18
+ //import { Node, Project, ScriptTarget, SyntaxKind, } from "ts-morph";
19
+ Node.isObjectProperty = (node) => (Node.isPropertyDeclaration(node)
20
+ || Node.isPropertyAssignment(node)
21
+ || Node.isPropertySignature(node));
22
+ /**
23
+ * Get children for object node
24
+ * @param {Node} node
25
+ * @returns {ObjectProperty[]}
26
+ */
27
+ function getChildProperties(node) {
28
+ const properties = node?.getType()?.getProperties();
29
+ const valueDeclarations = properties.map((child) => child.getValueDeclaration())
30
+ // Hacky way to check if the child is actually a defined child in the interface
31
+ // or if it's, e.g. a built-in method of the type (such as array.length)
32
+ ?.filter((child) => node.getFullText().includes(child?.getFullText()));
33
+ return (valueDeclarations ?? []);
34
+ }
35
+ /**
36
+ * @param {JSDocableNode} node
37
+ * @returns {JSDoc | undefined}
38
+ */
39
+ function getJsDoc(node) {
40
+ return node.getJsDocs().at(-1);
41
+ }
42
+ /**
43
+ * Get JSDoc for a node or create one if there isn't any
44
+ * @param {JSDocableNode} node
45
+ * @returns {JSDoc}
46
+ */
47
+ function getJsDocOrCreate(node) {
48
+ return getJsDoc(node) || node.addJsDoc({});
49
+ }
50
+ /**
51
+ * getJsDocOrCreate, but if JSDoc is created, insert a newline at the beginning
52
+ * so that the first line of JSDoc doesn't appear on the same line as `/**`
53
+ * @param {JSDocableNode} node
54
+ * @returns {JSDoc}
55
+ */
56
+ function getJsDocOrCreateMultiline(node) {
57
+ return getJsDoc(node) || node.addJsDoc({
58
+ description: "\n",
59
+ });
60
+ }
61
+ /**
62
+ * Return the node most suitable for JSDoc for a function, adding JSDoc if there isn't any
63
+ * @param {FunctionLikeDeclaration} functionNode
64
+ * @param {JSDocableNode} [docNode]
65
+ * @returns {JSDocableNode}
66
+ */
67
+ function getOutputJsDocNodeOrCreate(functionNode, docNode) {
68
+ if (docNode) {
69
+ const funcNodeDocs = functionNode.getJsDocs();
70
+ if (funcNodeDocs.length)
71
+ return functionNode;
72
+ getJsDocOrCreateMultiline(docNode);
73
+ return docNode;
74
+ }
75
+ getJsDocOrCreateMultiline(functionNode);
76
+ return functionNode;
77
+ }
78
+ /**
79
+ * Generate `@param` documentation from function parameters for functionNode, storing it in docNode
80
+ * @param {FunctionLikeDeclaration} functionNode
81
+ * @param {JSDocableNode} docNode
82
+ * @returns {void}
83
+ */
84
+ function generateParameterDocumentation(functionNode, docNode) {
85
+ const params = functionNode.getParameters();
86
+ if (!params.length)
87
+ return;
88
+ const jsDoc = getJsDocOrCreateMultiline(docNode);
89
+ // Get existing param tags, store their comments, then remove them
90
+ const paramTags = (jsDoc.getTags() || [])
91
+ .filter((tag) => ["param", "parameter"].includes(tag.getTagName()));
92
+ const commentLookup = Object.fromEntries(paramTags.map((tag) => [
93
+ // @ts-ignore
94
+ tag.compilerNode.name?.getText().replace(/\[|\]|(=.*)/g, "").trim(),
95
+ (tag.getComment() || "").toString().trim(),
96
+ ]));
97
+ const preferredTagName = paramTags[0]?.getTagName();
98
+ paramTags.forEach((tag) => tag.remove());
99
+ for (const param of params) {
100
+ const paramType = param.getTypeNode()?.getText() || param.getType().getText();
101
+ if (!paramType)
102
+ continue;
103
+ const paramName = param.compilerNode.name?.getText();
104
+ const isOptional = param.isOptional();
105
+ const isRest = param.isRestParameter();
106
+ // Rest parameters are arrays, but the JSDoc syntax is `...number` instead of `number[]`
107
+ const paramTypeOut = isRest ? `...${paramType.replace(/\[\]\s*$/, "")}` : paramType;
108
+ let defaultValue;
109
+ if (isOptional) {
110
+ const paramInitializer = param.getInitializer();
111
+ defaultValue = paramInitializer?.getText().replaceAll(/(\s|\t)*\n(\s|\t)*/g, " ");
112
+ }
113
+ let paramNameOut = paramName;
114
+ // Skip parameter names if they are present in the type as an object literal
115
+ // e.g. destructuring; { a }: { a: string }
116
+ if (paramNameOut.match(/[{},]/))
117
+ paramNameOut = "";
118
+ if (paramNameOut && isOptional) {
119
+ // Wrap name in square brackets if the parameter is optional
120
+ const defaultValueOut = defaultValue !== undefined ? `=${defaultValue}` : "";
121
+ paramNameOut = `[${paramNameOut}${defaultValueOut}]`;
122
+ }
123
+ paramNameOut = paramNameOut ? ` ${paramNameOut}` : "";
124
+ const comment = commentLookup[paramName.trim()];
125
+ jsDoc.addTag({
126
+ tagName: preferredTagName || "param",
127
+ text: `{${paramTypeOut}}${paramNameOut}${comment ? ` ${comment}` : ""}`,
128
+ });
129
+ }
130
+ }
131
+ /**
132
+ * Generate `@returns` documentation from function return type for functionNode, storing it in docNode
133
+ * @param {FunctionLikeDeclaration} functionNode
134
+ * @param {JSDocableNode} docNode
135
+ * @returns {void}
136
+ */
137
+ function generateReturnTypeDocumentation(functionNode, docNode) {
138
+ const returnTypeNode = functionNode.getReturnTypeNode() ?? functionNode.getReturnType();
139
+ const functionReturnType = returnTypeNode.getText(functionNode.getSignature().getDeclaration());
140
+ const jsDoc = getJsDocOrCreate(docNode);
141
+ const returnsTag = (jsDoc?.getTags() || [])
142
+ .find((tag) => ["returns", "return"].includes(tag.getTagName()));
143
+ // Replace tag with one that contains type info if tag exists
144
+ const tagName = returnsTag?.getTagName() || "returns";
145
+ const comment = (returnsTag?.getComment() || "").toString().trim();
146
+ if (returnsTag) {
147
+ returnsTag.remove();
148
+ }
149
+ jsDoc.addTag({
150
+ tagName,
151
+ text: `{${functionReturnType}}${comment ? ` ${comment}` : ""}`,
152
+ });
153
+ }
154
+ /**
155
+ * Generate documentation for a function, storing it in functionNode or docNode
156
+ * @param {FunctionLikeDeclaration} functionNode
157
+ * @param {JSDocableNode} [docNode]
158
+ * @returns {void}
159
+ */
160
+ function generateFunctionDocumentation(functionNode, docNode) {
161
+ const outputDocNode = getOutputJsDocNodeOrCreate(functionNode, docNode);
162
+ generateParameterDocumentation(functionNode, outputDocNode);
163
+ generateReturnTypeDocumentation(functionNode, outputDocNode);
164
+ }
165
+ /**
166
+ * Generate modifier documentation for class member
167
+ * @param {ClassMemberNode} classMemberNode
168
+ * @returns {void}
169
+ */
170
+ function generateModifierDocumentation(classMemberNode) {
171
+ const modifiers = classMemberNode.getModifiers() || [];
172
+ for (const modifier of modifiers) {
173
+ const text = modifier?.getText();
174
+ if (["public", "private", "protected", "readonly", "static"].includes(text)) {
175
+ const jsDoc = getJsDocOrCreateMultiline(classMemberNode);
176
+ jsDoc.addTag({ tagName: text });
177
+ }
178
+ }
179
+ }
180
+ /**
181
+ * Create class property initializer in constructor if it doesn't exist
182
+ * so that documentation is preserved when transpiling
183
+ * @param {ObjectProperty} classPropertyNode
184
+ * @returns {void}
185
+ */
186
+ function generateInitializerDocumentation(classPropertyNode) {
187
+ if (!classPropertyNode.getStructure()?.initializer) {
188
+ classPropertyNode.setInitializer("undefined");
189
+ }
190
+ const initializer = classPropertyNode.getStructure()?.initializer;
191
+ if (initializer !== "undefined") {
192
+ const jsDoc = getJsDocOrCreate(classPropertyNode);
193
+ jsDoc.addTag({ tagName: "default", text: initializer });
194
+ }
195
+ }
196
+ /**
197
+ * Document the class itself; at the moment just its extends signature
198
+ * @param {ClassDeclaration} classNode
199
+ * @returns {void}
200
+ */
201
+ function generateClassBaseDocumentation(classNode) {
202
+ const extendedClass = classNode.getExtends();
203
+ if (extendedClass) {
204
+ const jsDoc = getJsDocOrCreate(classNode);
205
+ jsDoc.addTag({ tagName: "extends", text: extendedClass.getText() });
206
+ }
207
+ }
208
+ /**
209
+ * Generate documentation for class members in general; either property or method
210
+ * @param {ClassMemberNode} classMemberNode
211
+ * @returns {void}
212
+ */
213
+ function generateClassMemberDocumentation(classMemberNode) {
214
+ generateModifierDocumentation(classMemberNode);
215
+ Node.isObjectProperty(classMemberNode) && generateInitializerDocumentation(classMemberNode);
216
+ Node.isMethodDeclaration(classMemberNode) && generateFunctionDocumentation(classMemberNode);
217
+ }
218
+ /**
219
+ * Generate documentation for a class — itself and its members
220
+ * @param {ClassDeclaration} classNode
221
+ * @returns {void}
222
+ */
223
+ function generateClassDocumentation(classNode) {
224
+ generateClassBaseDocumentation(classNode);
225
+ classNode.getMembers().forEach(generateClassMemberDocumentation);
226
+ }
227
+ /**
228
+ * Generate `@typedefs` from type aliases
229
+ * @param {TypeAliasDeclaration} typeAlias
230
+ * @return {string} A JSDoc comment containing the typedef
231
+ */
232
+ function generateTypedefDocumentation(typeAlias) {
233
+ const name = typeAlias.getName();
234
+ const typeNode = typeAlias.getTypeNode();
235
+ const isObjectType = Node.isTypeLiteral(typeNode) && typeAlias.getType().isObject();
236
+ const properties = isObjectType ? typeNode.getProperties() : [];
237
+ const typeParams = typeAlias.getTypeParameters();
238
+ // If we're going to have multiple tags, we need to create a multiline JSDoc
239
+ const jsDoc = properties.length || typeParams.length
240
+ ? getJsDocOrCreateMultiline(typeAlias)
241
+ : getJsDocOrCreate(typeAlias);
242
+ if (Node.isTypeLiteral(typeNode) && typeAlias.getType().isObject()) {
243
+ jsDoc.addTag({ tagName: "typedef", text: `{Object} ${name}` });
244
+ typeNode.getProperties().forEach((prop) => {
245
+ generateObjectPropertyDocumentation(prop, jsDoc);
246
+ });
247
+ }
248
+ else {
249
+ const { type } = typeAlias.getStructure();
250
+ if (typeof type !== "string")
251
+ return jsDoc.getFullText();
252
+ jsDoc.addTag({ tagName: "typedef", text: `{${type}} ${name}` });
253
+ }
254
+ typeParams.forEach((param) => {
255
+ const constraint = param.getConstraint();
256
+ const defaultType = param.getDefault();
257
+ const paramName = param.getName();
258
+ const nameWithDefault = defaultType ? `[${paramName}=${defaultType.getText()}]` : paramName;
259
+ jsDoc.addTag({
260
+ tagName: "template",
261
+ text: `${constraint ? `{${constraint.getText()}} ` : ""}${nameWithDefault}`,
262
+ });
263
+ });
264
+ return jsDoc.getFullText();
265
+ }
266
+ /**
267
+ * Generate documentation for object properties; runs recursively for nested objects
268
+ * @param {ObjectProperty} node
269
+ * @param {JSDoc} jsDoc
270
+ * @param {string} [name=""] The name to assign child docs to;
271
+ * "obj" will generate docs for "obj.val1", "obj.val2", etc
272
+ * @param {boolean} [topLevelCall=true] recursive functions are funky
273
+ * @returns {void}
274
+ */
275
+ function generateObjectPropertyDocumentation(node, jsDoc, name = "", topLevelCall = true) {
276
+ name = name || node.getName();
277
+ if (!topLevelCall)
278
+ name = `${name}.${node.getName()}`;
279
+ let propType = node.getTypeNode()
280
+ ?.getText()
281
+ ?.replace(/\n/g, "")?.trim();
282
+ const isOptional = node.hasQuestionToken()
283
+ || node.getJsDocs()?.[0]
284
+ ?.getTags()
285
+ ?.some((tag) => tag.getTagName() === "optional");
286
+ // Copy over existing description if there is one
287
+ const existingPropDocs = node.getJsDocs()?.[0]?.getDescription()?.trim() || "";
288
+ const children = getChildProperties(node);
289
+ if (children.length)
290
+ propType = "Object";
291
+ jsDoc.addTag({
292
+ tagName: "property",
293
+ text: `{${propType}} ${isOptional ? `[${name}]` : name} ${existingPropDocs}`,
294
+ });
295
+ if (children.length) {
296
+ children.forEach((child) => generateObjectPropertyDocumentation(child, jsDoc, name, false));
297
+ }
298
+ }
299
+ /**
300
+ * Generate `@typedefs` from interfaces
301
+ * @param {InterfaceDeclaration} interfaceNode
302
+ * @returns {string}
303
+ */
304
+ function generateInterfaceDocumentation(interfaceNode) {
305
+ const name = interfaceNode.getName();
306
+ const jsDoc = getJsDocOrCreateMultiline(interfaceNode);
307
+ jsDoc.addTag({ tagName: "typedef", text: `{Object} ${name}` });
308
+ interfaceNode.getProperties().forEach((prop) => {
309
+ generateObjectPropertyDocumentation(prop, jsDoc);
310
+ });
311
+ return jsDoc.getFullText();
312
+ }
313
+ /**
314
+ * Generate documentation for top-level var, const, and let declarations
315
+ * @param {VariableDeclaration} varNode
316
+ * @returns {void}
317
+ */
318
+ function generateTopLevelVariableDocumentation(varNode) {
319
+ const paramType = (varNode.getTypeNode() || varNode.getType())?.getText();
320
+ if (!paramType) {
321
+ return;
322
+ }
323
+ const jsDoc = getJsDoc(varNode.getVariableStatement());
324
+ if (!jsDoc) {
325
+ // Only generate documentation for variables that have an existing comment in JSDoc format
326
+ return;
327
+ }
328
+ const tags = jsDoc?.getTags() || [];
329
+ if (tags.find((tag) => ["type"].includes(tag.getTagName()))) {
330
+ return;
331
+ }
332
+ const constTag = tags.find((tag) => ["const", "constant"].includes(tag.getTagName()));
333
+ if (constTag && constTag.getComment()?.length) {
334
+ return;
335
+ }
336
+ jsDoc.addTag({
337
+ tagName: "type",
338
+ text: `{${paramType}}`,
339
+ });
340
+ }
341
+ /**
342
+ * Transpile.
343
+ * @param {string} src Source code to transpile
344
+ * @param {string} [filename="input.ts"] Filename to use internally when transpiling (can be a path or a name)
345
+ * @param {object} [compilerOptions={}] Options for the compiler.
346
+ * See https://www.typescriptlang.org/tsconfig#compilerOptions
347
+ * @param {boolean} [debug=false] Whether to log errors
348
+ * @return {string} Transpiled code (or the original source code if something went wrong)
349
+ */
350
+ function transpile(src, filename = "input.ts", compilerOptions = {}, debug = false) {
351
+ // Useless variable to prevent comments from getting removed when code contains just
352
+ // typedefs/interfaces, which get transpiled to nothing but comments
353
+ const protectCommentsHeader = "const __tsToJsdoc_protectCommentsHeader = 1;\n";
354
+ src = protectCommentsHeader + src;
355
+ try {
356
+ const project = new Project({
357
+ compilerOptions: {
358
+ target: ScriptTarget.ESNext,
359
+ esModuleInterop: true,
360
+ ...compilerOptions,
361
+ },
362
+ });
363
+ // Preserve blank lines in output
364
+ const blankLineMarker = "// TS-TO-JSDOC BLANK LINE //";
365
+ const code = src.split("\n").map((line) => (line.match(/^[\s\t]*$/) ? (blankLineMarker + line) : line)).join("\n");
366
+ // ts-morph throws a fit if the path already exists
367
+ const sourceFile = project.createSourceFile(`${path.basename(filename, ".ts")}.ts-to-jsdoc.ts`, code);
368
+ sourceFile.getClasses().forEach(generateClassDocumentation);
369
+ // Convert something like:
370
+ // import type {
371
+ // ImportDeclaration,
372
+ // ClassDeclaration,
373
+ // FunctionLikeDeclaration,
374
+ // } from 'ts-morph'
375
+ // Into:
376
+ // @typedef {import('ts-morph').ImportDeclaration } ImportDeclaration
377
+ // @typedef {import('ts-morph').ClassDeclaration } ClassDeclaration
378
+ // @typedef {import('ts-morph').FunctionLikeDeclaration} FunctionLikeDeclaration
379
+ const importDeclarations = sourceFile.getImportDeclarations();
380
+ let importTypesToTypedefs = '';
381
+ for (const importDeclaration of importDeclarations) {
382
+ const {importClause, moduleSpecifier} = importDeclaration.compilerNode;
383
+ if (importClause.isTypeOnly) {
384
+ const {elements} = importClause.namedBindings;
385
+ let out = '';
386
+ for (const element of elements) {
387
+ if (element.kind === ts.SyntaxKind.ImportSpecifier) {
388
+ const moduleSpecifierName = moduleSpecifier.text;
389
+ const elementName = element.name.text;
390
+ out += `/** @typedef {import('${moduleSpecifierName}').${elementName}} ${elementName} */\n`;
391
+ }
392
+ }
393
+ importTypesToTypedefs += out;
394
+ }
395
+ }
396
+ //console.log("importDeclarations", importDeclarations);
397
+
398
+ const typedefs = sourceFile.getTypeAliases()
399
+ .map((typeAlias) => generateTypedefDocumentation(typeAlias).trim())
400
+ .join("\n");
401
+ const interfaces = sourceFile.getInterfaces()
402
+ .map((interfaceNode) => generateInterfaceDocumentation(interfaceNode).trim())
403
+ .join("\n");
404
+ const directFunctions = sourceFile.getFunctions();
405
+ directFunctions.forEach((node) => generateFunctionDocumentation(node));
406
+ const varDeclarations = sourceFile.getVariableDeclarations();
407
+ varDeclarations.forEach((varDeclaration) => {
408
+ const initializer = varDeclaration.getInitializerIfKind(SyntaxKind.ArrowFunction)
409
+ || varDeclaration.getInitializerIfKind(SyntaxKind.FunctionExpression);
410
+ if (initializer) {
411
+ generateFunctionDocumentation(initializer, varDeclaration.getVariableStatement());
412
+ }
413
+ else {
414
+ generateTopLevelVariableDocumentation(varDeclaration);
415
+ }
416
+ });
417
+ let result = project
418
+ .emitToMemory()
419
+ ?.getFiles()
420
+ ?.find((file) => file.filePath.slice(0, -3) === sourceFile.getFilePath().slice(0, -3))
421
+ ?.text;
422
+ if (result) {
423
+ //console.log("RESULT", result);
424
+ if (!result.startsWith(protectCommentsHeader)) {
425
+ throw new Error("Internal error: generated header is missing in output.\n\n"
426
+ + `Output: ${JSON.stringify(`${result.slice(protectCommentsHeader.length + 100)} ...`)}`);
427
+ }
428
+ result = result.replace(protectCommentsHeader, "");
429
+ result = importTypesToTypedefs + result;
430
+ // Restore blank lines in output
431
+ result = result.split("\n").map((_line) => {
432
+ const line = _line.trim();
433
+ return line.startsWith(blankLineMarker)
434
+ ? line.slice(blankLineMarker.length)
435
+ : _line;
436
+ }).join("\n").trim();
437
+ if (typedefs)
438
+ result += `\n\n${typedefs}`;
439
+ if (interfaces)
440
+ result += `\n\n${interfaces}`;
441
+ result = `${result.trim()}\n`;
442
+ return result;
443
+ }
444
+ throw new Error("Could not emit output to memory.");
445
+ }
446
+ catch (e) {
447
+ debug && console.error(e);
448
+ return src;
449
+ }
450
+ return src;
451
+ }
452
+ export {transpile};
453
+ /**
454
+ * @typedef {JSDocableNode & TypedNode & (
455
+ * | PropertyDeclaration
456
+ * | PropertyAssignment
457
+ * | PropertySignature
458
+ * )} ObjectProperty
459
+ */
460
+ /** @typedef {JSDocableNode & ModifierableNode & ObjectProperty & MethodDeclaration} ClassMemberNode */