UNPKG

8.5 kBJavaScriptView Raw
1const path = require('path')
2const ts = require('typescript')
3
4const appendComment = (commentBlock, toAppend) => {
5 return commentBlock.replace(/[\n,\s]*\*\//, toAppend.split('\n').map(line => `\n * ${line}`) + '\n */')
6}
7
8/**
9 * Get type from a node
10 * @param {ts.TypeNode} type which should be parsed to string
11 * @param {string} src source for an entire parsed file
12 * @returns {string} node type
13 */
14const getTypeName = (type, src) => {
15 if (type.typeName && type.typeName.escapedText) {
16 const typeName = type.typeName.escapedText
17 if(type.typeArguments && type.typeArguments.length) {
18 const args = type.typeArguments.map(subType => getTypeName(subType, src)).join(', ')
19 return `${typeName}<${args}>`
20 } else {
21 return typeName
22 }
23 }
24 if(ts.isFunctionTypeNode(type) || ts.isFunctionLike(type)) {
25 // it replaces ():void => {} (and other) to simple function
26 return 'function'
27 }
28 if (ts.isArrayTypeNode(type)) {
29 return 'Array'
30 }
31 if (type.types) {
32 return type.types.map(subType => getTypeName(subType, src)).join(' | ')
33 }
34 if (type.members && type.members.length) {
35 return 'object'
36 }
37 return src.substring(type.pos, type.end).trim()
38}
39
40/**
41 * Fetches name from a node.
42 */
43const getName = (node, src) => {
44 let name = node.name && node.name.escapedText
45 || node.parameters && src.substring(node.parameters.pos, node.parameters.end)
46
47 // changing type [key: string] to {...} - otherwise it wont be parsed by @jsdoc
48 if (name === 'key: string') { return '{...}' }
49 return name
50}
51
52/**
53 * converts function parameters to @params
54 *
55 * @param {string} [jsDoc] existing jsdoc text where all @param comments should be appended
56 * @param {ts.FunctionDeclaration} wrapper ts node which has to be parsed
57 * @param {string} src source for an entire parsed file (we are fetching substrings from it)
58 * @param {string} parentName name of a parent element - NOT IMPLEMENTED YET
59 * @returns {string} modified jsDoc comment with appended @param tags
60 *
61 */
62const convertParams = (jsDoc = '', node, src, parentName = null) => {
63 node.type.parameters.forEach(parameter => {
64 let name = getName(parameter, src)
65 let comment = parameter.jsDoc && parameter.jsDoc[0] && parameter.jsDoc[0].comment || ''
66 if (parameter.questionToken) {
67 name = ['[', name, ']'].join('')
68 }
69 let type = getTypeName(parameter.type, src)
70 jsDoc = appendComment(jsDoc, `@param {${type}} ${name} ${comment}`)
71 })
72 return jsDoc
73}
74
75/**
76 * Convert type properties to @property
77 * @param {string} [jsDoc] existing jsdoc text where all @param comments should be appended
78 * @param {ts.TypeNode} wrapper ts node which has to be parsed
79 * @param {string} src source for an entire parsed file (we are fetching substrings from it)
80 * @param {string} parentName name of a parent element
81 * @returns {string} modified jsDoc comment with appended @param tags
82 */
83let convertMembers = (jsDoc = '', type, src, parentName = null) => {
84 // type could be an array of types like: `{sth: 1} | string` - so we parse
85 // each type separately
86 const typesToCheck = [type]
87 if (type.types && type.types.length) {
88 typesToCheck.push(...type.types)
89 }
90 typesToCheck.forEach(type => {
91 // Handling array defined like this: {alement1: 'something'}[]
92 if(ts.isArrayTypeNode(type) && type.elementType) {
93 jsDoc = convertMembers(jsDoc, type.elementType, src, parentName ? parentName + '[]' : '[]')
94 }
95
96 // Handling Array<{element1: 'somethin'}>
97 if (type.typeName && type.typeName.escapedText === 'Array') {
98 if(type.typeArguments && type.typeArguments.length) {
99 type.typeArguments.forEach(subType => {
100
101 jsDoc = convertMembers(jsDoc, subType, src, parentName
102 ? parentName + '[]'
103 : '' // when there is no parent - jsdoc cannot parse [].name
104 )
105 })
106 }
107 }
108 // Handling {property1: "value"}
109 (type.members || []).filter(m => ts.isTypeElement(m)).forEach(member => {
110 let name = getName(member, src)
111 let comment = member.jsDoc && member.jsDoc[0] && member.jsDoc[0].comment || ''
112 const members = member.type.members || []
113 let typeName = members.length ? 'object' : getTypeName(member.type, src)
114 if (parentName) {
115 name = [parentName, name].join('.')
116 }
117 // optional
118 const nameToPlace = member.questionToken ? `[${name}]` : name
119 jsDoc = appendComment(jsDoc, `@property {${typeName}} ${nameToPlace} ${comment}`)
120 jsDoc = convertMembers(jsDoc, member.type, src, name)
121 })
122 })
123 return jsDoc
124}
125
126/**
127 * Main function which converts types
128 *
129 * @param {string} src typescript code to convert to jsdoc comments
130 * @param {string} [filename] filename which is required by typescript parser
131 * @return {string} @jsdoc comments generated from given typescript code
132 */
133module.exports = function typeConverter(src, filename = 'test.ts') {
134 let ast = ts.createSourceFile(
135 path.basename(filename),
136 src,
137 ts.ScriptTarget.Latest,
138 false,
139 ts.ScriptKind.TS
140 )
141
142 // iterate through all the statements in global scope
143 // we are looking for `interface xxxx` and `type zzz`
144 return ast.statements.map(statement => {
145 let jsDocNode = statement.jsDoc && statement.jsDoc[0]
146 // Parse only statements with jsdoc comments.
147 if (jsDocNode) {
148 let comment = src.substring(jsDocNode.pos, jsDocNode.end)
149 const name = getName(statement, src)
150
151 if (ts.isTypeAliasDeclaration(statement)) {
152 if (ts.isFunctionTypeNode(statement.type)) {
153 comment = appendComment(comment, `@typedef {function} ${name}`)
154 return convertParams(comment, statement, src)
155 }
156 if (ts.isTypeLiteralNode(statement.type)) {
157 comment = appendComment(comment, `@typedef {object} ${name}`)
158 return convertMembers(comment, statement.type, src)
159 }
160 if (ts.isIntersectionTypeNode(statement.type)) {
161 comment = appendComment(comment, `@typedef {object} ${name}`)
162 return convertMembers(comment, statement.type, src)
163 }
164 }
165 if (ts.isInterfaceDeclaration(statement)) {
166 comment = appendComment(comment, `@interface ${name}`)
167
168 statement.members.forEach(member => {
169 if (!member.jsDoc) { return }
170 let memberComment = src.substring(member.jsDoc[0].pos, member.jsDoc[0].end)
171 let memberName = getName(member, src)
172 memberComment = appendComment(memberComment, [
173 `@name ${name}#${memberName}`
174 ].join('\n'))
175 if (member.questionToken) {
176 memberComment = appendComment(memberComment, '@optional')
177 }
178 if (!member.type && ts.isFunctionLike(member)) {
179 let type = getTypeName(member, src)
180 memberComment = appendComment(memberComment, `@type {${type}}`)
181 memberComment = appendComment(memberComment, `@method`)
182 } else {
183 memberComment = convertMembers(memberComment, member.type, src, parentName = null)
184 let type = getTypeName(member.type, src)
185 memberComment = appendComment(memberComment, `@type {${type}}`)
186 }
187 comment += '\n' + memberComment
188 })
189 return comment
190 }
191 if (ts.isClassDeclaration(statement)) {
192 comment = ''
193 const className = getName(statement, src)
194 statement.members.forEach(member => {
195 if (!member.jsDoc) { return }
196 if (!ts.isPropertyDeclaration(member)) { return }
197 let memberComment = src.substring(member.jsDoc[0].pos, member.jsDoc[0].end)
198 const modifiers = (member.modifiers || []).map(m => m.getText({text: src}))
199 modifiers.forEach(m => {
200 if (['private', 'public', 'protected'].includes(m)) {
201 memberComment = appendComment(memberComment, `@${m}`)
202 }
203 })
204 if (member.type) {
205 memberComment = appendComment(memberComment, `@type {${getTypeName(member.type, src)}}`)
206 }
207 getTypeName(member, src)
208 if (modifiers.find((m => m === 'static'))) {
209 memberComment += '\n' + `${className}.${getName(member, src)}`
210 } else {
211 memberComment += '\n' + `${className}.prototype.${getName(member, src)}`
212 }
213 comment += '\n' + memberComment
214 })
215 return comment
216 }
217 }
218 return ''
219 }).join('\n')
220}