bio-dts 0.3.0 → 0.4.1

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/README.md CHANGED
@@ -15,6 +15,17 @@ This module provides `pre` and `post` processing helpers to a type definition pi
15
15
  npx bio-dts --outDir dist/types -r lib
16
16
  ```
17
17
 
18
+
19
+ ## Features
20
+
21
+ * Generates clean type definitions from ES5 code bases
22
+ * Supports ES5 prototypical classes + inheritance
23
+ * Supports optional parameters before required ones
24
+ * Converts JSDoc to [TSDoc](https://github.com/microsoft/tsdoc)
25
+
26
+ Checkout the [test fixtures](./test/fixtures) for full coverage.
27
+
28
+
18
29
  ## API
19
30
 
20
31
  ```javascript
@@ -38,4 +49,4 @@ const transformedCode = postTransform(tsCode);
38
49
  generateTypes(files, {
39
50
  outDir: 'dist'
40
51
  });
41
- ```
52
+ ```
package/bin/cmd.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import ts from 'typescript';
3
+ import Module from 'node:module';
4
4
 
5
5
  import glob from 'tiny-glob';
6
6
 
@@ -10,6 +10,9 @@ import fs from 'node:fs';
10
10
  import generateTypes from '../lib/generate-types.js';
11
11
 
12
12
  async function run() {
13
+
14
+ const ts = getTypescript();
15
+
13
16
  const args = process.argv.slice(2);
14
17
 
15
18
  const {
@@ -44,18 +47,12 @@ async function run() {
44
47
 
45
48
  for (const fileName of fileNames) {
46
49
 
47
- try {
48
- if (fs.statSync(fileName).isDirectory) {
49
-
50
- const globbedFiles = await glob(globPattern, { cwd: fileName, filesOnly: true });
51
-
52
- files.push(...globbedFiles.map(f => path.join(fileName, f)));
53
- } else {
54
- files.push(fileName);
55
- }
56
- } catch (err) {
50
+ if (isDir(fileName)) {
51
+ const globbedFiles = await glob(globPattern, { cwd: fileName, filesOnly: true });
57
52
 
58
- // ignore
53
+ files.push(...globbedFiles.map(f => path.join(fileName, f)));
54
+ } else {
55
+ files.push(fileName);
59
56
  }
60
57
  }
61
58
 
@@ -72,6 +69,34 @@ async function run() {
72
69
  console.log('Done.');
73
70
  }
74
71
 
72
+ function isDir(fileName) {
73
+
74
+ try {
75
+ return fs.statSync(fileName).isDirectory();
76
+ } catch (err) {
77
+
78
+ // ignore
79
+ return false;
80
+ }
81
+ }
82
+
83
+ function getTypescript() {
84
+
85
+ const cwd = process.cwd();
86
+
87
+ const createRequire = Module.createRequire || Module.createRequireFromPath;
88
+
89
+ const requireLocal = createRequire(
90
+ path.join(cwd, '__placeholder__.js')
91
+ );
92
+
93
+ try {
94
+ return requireLocal('typescript');
95
+ } catch (err) {
96
+ throw new Error(`failed to load <typescript> from <${ cwd }>`);
97
+ }
98
+ }
99
+
75
100
  run().catch(err => {
76
101
  console.error(err);
77
102
 
@@ -45,73 +45,232 @@ export default function transform(src) {
45
45
  return false;
46
46
  }
47
47
 
48
- traverse(body, function(path) {
48
+ function keepCommentParams(commentPath, params) {
49
49
 
50
- const {
51
- value
52
- } = path;
50
+ const replacements = [];
53
51
 
54
- // filter private members
55
- if (!isPublic(value)) {
56
- return replace(path);
52
+ const comment = commentPath.value;
53
+
54
+ const doc = comment.value.replace(/\n\s+/g, '\n ');
55
+ const tags = parseJSDoc(doc);
56
+
57
+ for (const tag of tags) {
58
+
59
+ // remove param not mentioned anymore
60
+ if (tag.name === 'param' && params.every(p => p.name !== tag.param.name)) {
61
+ replacements.push([ { start: tag.start - 4, end: tag.end } ]);
62
+
63
+ continue;
64
+ }
57
65
  }
58
66
 
59
- // TODO(nikku): transform illegal typescript methods into override calls
67
+ const newDoc = replacements.slice().reverse().reduce((newDoc, r) => {
60
68
 
61
- // TODO(nikku): remove annotations
62
- //
63
- // * @typedef
64
- // * @class
65
- // * @constructor
66
- // * @template
67
- // * @method
69
+ const [ { start, end }, replacement = '' ] = r;
68
70
 
69
- const comments = path.get('comments') || { value: [] };
71
+ return replaceJSDoc(newDoc, { start, end }, replacement);
72
+ }, doc);
70
73
 
71
- for (const key of keys(comments.value || []).reverse()) {
74
+ const builder = {
75
+ 'CommentBlock': b.commentBlock,
76
+ 'CommentLine': b.commentLine
77
+ }[comment.type];
72
78
 
73
- const comment = comments.get(key);
79
+ return cleanComment(builder.from({
80
+ ...comment,
81
+ value: newDoc
82
+ }));
83
+ }
74
84
 
75
- const replacements = [];
85
+ function fixOptionalArgsMethods(nodePath) {
76
86
 
77
- const doc = comment.value.value.replace(/\n\s+/g, '\n ');
78
- const tags = parseJSDoc(doc);
87
+ const {
88
+ value: node
89
+ } = nodePath;
79
90
 
80
- for (const tag of tags) {
81
- if (/class|constructor|template|method|typedef/.test(tag.name)) {
82
- replacements.push([ { start: tag.start - 4, end: tag.end } ]);
91
+ if (node.type !== 'TSDeclareMethod' && node.type !== 'TSDeclareFunction') {
92
+ return;
93
+ }
83
94
 
84
- continue;
85
- }
95
+ const hostPath = nodePath.parentPath.value.type === 'ExportNamedDeclaration'
96
+ ? nodePath.parentPath
97
+ : nodePath;
86
98
 
87
- if (tag.type) {
88
- replacements.push([ { start: tag.type.start - 1, end: tag.type.end }, '' ]);
89
- }
99
+ const {
100
+ variations
101
+ } = node.params.slice().reverse().reduce((res, param) => {
102
+
103
+ let {
104
+ required,
105
+ variations
106
+ } = res;
107
+
108
+ // required before optional
109
+ if (param.optional && required) {
110
+ variations = [
111
+ ...variations.map(v => v.slice()),
112
+ ...variations.map(v => v.slice())
113
+ ];
114
+
115
+ variations.forEach((variation, idx) => {
116
+
117
+ if (idx < variations.length / 2) {
118
+ variation.push(b.identifier.from({
119
+ ...param,
120
+ optional: false
121
+ }));
122
+ }
123
+ });
124
+ } else {
90
125
 
91
- if (tag.param) {
92
- replacements.push([ tag.param, tag.param.name ]);
126
+ if (!param.optional) {
127
+ required = true;
93
128
  }
129
+
130
+ variations.forEach((variation) => {
131
+ const builder = {
132
+ 'RestElement': b.restElement,
133
+ 'Identifier': b.identifier
134
+ }[param.type];
135
+
136
+ variation.push(builder.from(param));
137
+ });
94
138
  }
95
139
 
96
- const newDoc = replacements.slice().reverse().reduce((newDoc, r) => {
140
+ return {
141
+ required,
142
+ variations
143
+ };
144
+ }, {
145
+ variations: [
146
+ []
147
+ ],
148
+ required: false
149
+ });
150
+
151
+ if (variations.length === 1) {
152
+ return;
153
+ }
97
154
 
98
- const [ { start, end }, replacement = '' ] = r;
155
+ const commentPaths = hostPath.get('comments');
99
156
 
100
- return replaceJSDoc(newDoc, { start, end }, replacement);
101
- }, doc);
157
+ // last comment is significant
158
+ const commentPath = commentPaths?.value && commentPaths.get(commentPaths.value.length - 1) || { value: null };
102
159
 
103
- // remove entirely, if empty
104
- if (!newDoc.replace(/[/*\s]/g, '')) {
105
- replace(comment);
160
+ const replacements = variations.slice().reverse().map(variation => {
161
+
162
+ const builder = {
163
+ 'TSDeclareFunction': b.tsDeclareFunction,
164
+ 'TSDeclareMethod': b.tsDeclareMethod
165
+ }[node.type];
166
+
167
+ const variationComments = commentPath.value ? [ keepCommentParams(commentPath, variation) ] : [ ];
168
+
169
+ const newNode = builder.from({
170
+ ...node,
171
+ params: variation.slice().reverse(),
172
+ comments: hostPath === nodePath ? variationComments : []
173
+ });
174
+
175
+ return (
176
+ hostPath === nodePath
177
+ ? newNode
178
+ : b.exportNamedDeclaration.from({
179
+ ...hostPath.value,
180
+ declaration: newNode,
181
+ comments: variationComments
182
+ })
183
+ );
184
+ });
185
+
186
+ replace(hostPath, ...replacements);
187
+
188
+ return replacements;
189
+ }
190
+
191
+
192
+ function cleanComment(comment) {
193
+
194
+ const replacements = [];
195
+
196
+ const doc = comment.value.replace(/\n\s+/g, '\n ');
197
+ const tags = parseJSDoc(doc);
198
+
199
+ for (const tag of tags) {
200
+
201
+ // remove full line including the non-TS tag
202
+ if (/class|constructor|template|method|typedef|property/.test(tag.name)) {
203
+ replacements.push([ { start: tag.start - 4, end: tag.end } ]);
204
+
205
+ continue;
206
+ }
207
+
208
+ if (tag.type) {
209
+ replacements.push([ { start: tag.type.start - 1, end: tag.type.end }, '' ]);
210
+ }
211
+
212
+ if (tag.param) {
213
+
214
+ // [foo=10] => foo
215
+ replacements.push([ tag.param, tag.param.name ]);
216
+ }
217
+ }
218
+
219
+ const newDoc = replacements.slice().reverse().reduce((newDoc, r) => {
220
+
221
+ const [ { start, end }, replacement = '' ] = r;
222
+
223
+ return replaceJSDoc(newDoc, { start, end }, replacement);
224
+ }, doc);
225
+
226
+ // remove entirely, if empty
227
+ if (!newDoc.replace(/[/*\s]/g, '')) {
228
+ return null;
229
+ } else {
230
+ return b.commentBlock.from({
231
+ type: 'CommentBlock',
232
+ leading: true,
233
+ value: newDoc
234
+ });
235
+ }
236
+ }
237
+
238
+ /**
239
+ * @param {import('./util.js').Path} nodePath
240
+ */
241
+ function cleanComments(nodePath) {
242
+
243
+ // clean JSDoc comments: remove annotation, fixup type parameters
244
+ const comments = nodePath.get('comments') || { value: [] };
245
+
246
+ for (const key of keys(comments.value || []).reverse()) {
247
+ const comment = comments.get(key);
248
+
249
+ const newComment = cleanComment(comment.value);
250
+ if (newComment) {
251
+ replace(comment, newComment);
106
252
  } else {
107
- replace(comment, b.commentBlock.from({
108
- type: 'CommentBlock',
109
- leading: true,
110
- value: newDoc
111
- }));
253
+ replace(comment);
112
254
  }
113
255
  }
256
+ }
257
+
258
+ traverse(body, function(path) {
259
+
260
+ const {
261
+ value
262
+ } = path;
263
+
264
+ // filter private members
265
+ if (!isPublic(value)) {
266
+ return replace(path);
267
+ }
268
+
269
+ if (fixOptionalArgsMethods(path)) {
270
+ return false;
271
+ }
114
272
 
273
+ cleanComments(path);
115
274
  });
116
275
 
117
276
  for (const [ path, args ] of replacements) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bio-dts",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Generate sane and clean types from JavaScript sources",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -16,14 +16,14 @@
16
16
  "dependencies": {
17
17
  "@babel/parser": "^7.21.2",
18
18
  "recast": "^0.22.0",
19
- "tiny-glob": "^0.2.9",
20
- "typescript": "^4.9.5"
19
+ "tiny-glob": "^0.2.9"
21
20
  },
22
21
  "devDependencies": {
23
22
  "chai": "^4.3.7",
24
23
  "eslint": "^8.36.0",
25
24
  "eslint-plugin-bpmn-io": "^1.0.0",
26
- "mocha": "^10.2.0"
25
+ "mocha": "^10.2.0",
26
+ "typescript": "^5.0.2"
27
27
  },
28
28
  "files": [
29
29
  "bin",