1 | const path = require('path')
|
2 | const ts = require('typescript')
|
3 |
|
4 | const appendComment = (commentBlock, toAppend) => {
|
5 | return commentBlock.replace(/[\n,\s]*\*\//, toAppend.split('\n').map(line => `\n * ${line}`) + '\n */')
|
6 | }
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | const 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 |
|
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 |
|
42 |
|
43 | const getName = (node, src) => {
|
44 | let name = node.name && node.name.escapedText
|
45 | || node.parameters && src.substring(node.parameters.pos, node.parameters.end)
|
46 |
|
47 |
|
48 | if (name === 'key: string') { return '{...}' }
|
49 | return name
|
50 | }
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 | const 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 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 | let convertMembers = (jsDoc = '', type, src, parentName = null) => {
|
84 |
|
85 |
|
86 | const typesToCheck = [type]
|
87 | if (type.types && type.types.length) {
|
88 | typesToCheck.push(...type.types)
|
89 | }
|
90 | typesToCheck.forEach(type => {
|
91 |
|
92 | if(ts.isArrayTypeNode(type) && type.elementType) {
|
93 | jsDoc = convertMembers(jsDoc, type.elementType, src, parentName ? parentName + '[]' : '[]')
|
94 | }
|
95 |
|
96 |
|
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 | : ''
|
104 | )
|
105 | })
|
106 | }
|
107 | }
|
108 |
|
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 |
|
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 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 | module.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 |
|
143 |
|
144 | return ast.statements.map(statement => {
|
145 | let jsDocNode = statement.jsDoc && statement.jsDoc[0]
|
146 |
|
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 | }
|