bio-dts 0.6.1 → 0.8.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.
package/README.md CHANGED
@@ -6,7 +6,9 @@ Utilities to generate sane and clean type definitions from JavaScript files.
6
6
 
7
7
  ## About
8
8
 
9
- This module provides `pre` and `post` processing helpers to a type definition pipeline, as well as a simple [generator cli](#usage).
9
+ This module provides `pre` and `post` processing helpers to a type definition pipeline.
10
+
11
+ You can use it [via API](#api) or through a simple [generator cli](#usage).
10
12
 
11
13
 
12
14
  ## Usage
@@ -86,7 +86,7 @@ export function parse(str) {
86
86
  i = skipWhitespace(str, j);
87
87
 
88
88
  // parse type
89
- if ([ 'param', 'return', 'returns', 'typedef' ].includes(tag.name)) {
89
+ if ([ 'param', 'return', 'returns', 'typedef', 'template' ].includes(tag.name)) {
90
90
 
91
91
  let match = findClose(str, i, '{', '}');
92
92
 
@@ -98,7 +98,7 @@ export function parse(str) {
98
98
  }
99
99
 
100
100
  // parse param
101
- if ([ 'param', 'typedef' ].includes(tag.name)) {
101
+ if ([ 'param', 'typedef', 'template' ].includes(tag.name)) {
102
102
 
103
103
  let match = tag.name === 'param' && findClose(str, i, '[', ']');
104
104
 
@@ -82,6 +82,56 @@ export default function transform(src) {
82
82
  }));
83
83
  }
84
84
 
85
+ function getDocumentedParams(node, params = [], comment = null) {
86
+
87
+ if (!comment) {
88
+ return params;
89
+ }
90
+
91
+ const doc = comment.value.replace(/\n\s+/g, '\n ');
92
+
93
+ // parse known parameters
94
+ // ignore hierarchical (sub-type) params
95
+ const knownParams =
96
+ parseJSDoc(doc)
97
+ .filter(tag => tag.name === 'param' && !tag.param.name.includes('.'))
98
+ .map(tag => tag.param.name);
99
+
100
+ if (!knownParams.length) {
101
+ return params;
102
+ }
103
+
104
+ let i = 0;
105
+ let j = 0;
106
+
107
+ const filteredParams = [];
108
+
109
+ while (j < knownParams.length) {
110
+
111
+ let expectedName = knownParams[j];
112
+ let param = params[i++];
113
+
114
+ if (!param) {
115
+ throw error(node, `documented parameter <${ expectedName }> not found`);
116
+ }
117
+
118
+ if (param.name === 'this') {
119
+ filteredParams.push(param);
120
+ continue;
121
+ }
122
+
123
+ const actualName = param.argument?.name || param.name;
124
+
125
+ if (actualName !== expectedName) {
126
+ throw error(node, `documented parameter <${ expectedName }> differs from actual parameter <${ actualName }>`);
127
+ }
128
+
129
+ filteredParams.push(param);
130
+ j++;
131
+ }
132
+
133
+ return filteredParams;
134
+ }
85
135
 
86
136
  /**
87
137
  * Ensure optional args methods are properly escaped
@@ -96,36 +146,34 @@ export default function transform(src) {
96
146
  value: node
97
147
  } = nodePath;
98
148
 
99
- const fnDeclaration = (
100
- node.type === 'TSDeclareMethod' ||
101
- node.type === 'TSDeclareFunction'
102
- );
149
+ const functionKind = getFunctionKind(node);
103
150
 
104
- const fnDefinition = (
105
- node?.typeAnnotation?.typeAnnotation?.type === 'TSFunctionType'
106
- );
107
-
108
- if (!fnDeclaration && !fnDefinition) {
151
+ if (!functionKind) {
109
152
  return;
110
153
  }
111
154
 
112
- const params = fnDeclaration
113
- ? node.params
114
- : node?.typeAnnotation?.typeAnnotation?.parameters;
155
+ const params = functionKind === 'TSFunctionType'
156
+ ? node.typeAnnotation?.typeAnnotation?.parameters
157
+ : node.params;
158
+
159
+ const typeParameters = functionKind === 'TSFunctionType'
160
+ ? node.typeAnnotation?.typeAnnotation?.typeParameters
161
+ : node.typeParameters;
162
+
163
+ const returnType = functionKind === 'TSFunctionType'
164
+ ? node.typeAnnotation?.typeAnnotation?.typeAnnotation
165
+ : node.returnType;
115
166
 
116
- const returnType = fnDeclaration
117
- ? node.returnType
118
- : node?.typeAnnotation?.typeAnnotation?.typeAnnotation;
167
+ const hostPath = getHostPath(nodePath);
119
168
 
120
- const hostPath = (
121
- [ 'ExportDefaultDeclaration', 'ExportNamedDeclaration' ].includes(nodePath.parentPath.value.type)
122
- ? nodePath.parentPath
123
- : nodePath
124
- );
169
+ const commentPaths = hostPath.get('comments');
170
+
171
+ // last comment is significant
172
+ const commentPath = commentPaths?.value && commentPaths.get(commentPaths.value.length - 1) || { value: null };
125
173
 
126
174
  const {
127
175
  variations
128
- } = params.slice().reverse().reduce((res, param) => {
176
+ } = getDocumentedParams(hostPath.value, params, commentPath.value).slice().reverse().reduce((res, param) => {
129
177
 
130
178
  let {
131
179
  required,
@@ -179,11 +227,6 @@ export default function transform(src) {
179
227
  return;
180
228
  }
181
229
 
182
- const commentPaths = hostPath.get('comments');
183
-
184
- // last comment is significant
185
- const commentPath = commentPaths?.value && commentPaths.get(commentPaths.value.length - 1) || { value: null };
186
-
187
230
  const replacements = variations.slice().reverse().map(variation => {
188
231
 
189
232
  const builder = {
@@ -207,7 +250,10 @@ export default function transform(src) {
207
250
  returnType: returnType ? b.tsTypeAnnotation.from({
208
251
  ...returnType
209
252
  }) : null,
210
- comments: hostPath === nodePath ? variationComments : []
253
+ comments: hostPath === nodePath ? variationComments : [],
254
+ typeParameters: typeParameters ? b.tsTypeParameterDeclaration.from({
255
+ ...typeParameters
256
+ }) : null
211
257
  });
212
258
 
213
259
  return (
@@ -227,6 +273,216 @@ export default function transform(src) {
227
273
  }
228
274
 
229
275
 
276
+ /**
277
+ * Ensures we don't re-export external declarations
278
+ *
279
+ * @example
280
+ *
281
+ * ```javascript
282
+ * export type Woop = import('./Woop').default;
283
+ *
284
+ * // ===>
285
+ *
286
+ * type Woop = import('./Woop').default;
287
+ * ```
288
+ *
289
+ * @param {import('./util.js').Path} nodePath
290
+ *
291
+ * @return {any[]} replacements
292
+ */
293
+ function fixTypeExport(nodePath) {
294
+
295
+ const {
296
+ value: node
297
+ } = nodePath;
298
+
299
+ if (
300
+ node.type !== 'ExportNamedDeclaration' ||
301
+ node.declaration?.typeAnnotation?.type !== 'TSImportType'
302
+ ) {
303
+ return;
304
+ }
305
+
306
+ replace(nodePath, nodePath.get('declaration').value);
307
+ }
308
+
309
+ /**
310
+ * Ensure that only documented method parameters are used.
311
+ *
312
+ * @param {Path} nodePath
313
+ *
314
+ * @return {boolean} true if modified
315
+ */
316
+ function removeUnknownParams(nodePath) {
317
+
318
+ const {
319
+ value: node
320
+ } = nodePath;
321
+
322
+ const functionKind = getFunctionKind(node);
323
+
324
+ if (!functionKind) {
325
+ return;
326
+ }
327
+
328
+ const params = functionKind === 'TSFunctionType'
329
+ ? nodePath.get('typeAnnotation', 'typeAnnotation', 'parameters')
330
+ : nodePath.get('params');
331
+
332
+ const hostPath = getHostPath(nodePath);
333
+
334
+ const commentPaths = hostPath.get('comments');
335
+
336
+ // last comment is significant
337
+ const commentPath = commentPaths?.value && commentPaths.get(commentPaths.value.length - 1) || { value: null };
338
+
339
+ const knownParams = getDocumentedParams(hostPath.value, params.value, commentPath.value);
340
+
341
+ let replaced = false;
342
+
343
+ for (const idx in params.value || []) {
344
+ if (!knownParams[idx]) {
345
+ replace(params.get(idx));
346
+ replaced = true;
347
+ }
348
+ }
349
+
350
+ return replaced;
351
+ }
352
+
353
+ /**
354
+ * Generate method overloads based on `@overlord` annotated JSDoc comments.
355
+ *
356
+ * Our strategy is to parse for separate `@overlord` annotated tags,
357
+ * use the meta-data, and generate a completely new method from it.
358
+ *
359
+ * @param {Path} nodePath
360
+ *
361
+ * @return {boolean} true if modified
362
+ */
363
+ function generateOverloads(nodePath) {
364
+
365
+ const {
366
+ value: node
367
+ } = nodePath;
368
+
369
+ const functionKind = getFunctionKind(node);
370
+
371
+ if (!functionKind) {
372
+ return;
373
+ }
374
+
375
+ const hostPath = getHostPath(nodePath);
376
+
377
+ const commentPaths = hostPath.get('comments');
378
+
379
+ const comments = (commentPaths?.value || []);
380
+
381
+ // scan for potential overloads
382
+ const overloads = comments.reduce((overloads, comment, idx) => {
383
+ const commentText = comment.value;
384
+
385
+ if (commentText.includes('@overlord') || idx === comments.length - 1) {
386
+ const tags = parseJSDoc(comment.value);
387
+
388
+ const paramTags = tags.filter(t => t.name === 'param');
389
+ const templateTags = tags.filter(t => t.name === 'template');
390
+ const returnTags = tags.filter(t => t.name === 'return' || t.name === 'returns');
391
+
392
+ if (returnTags.length > 1) {
393
+ throw error(hostPath.value, 'must specify zero or one @return(s) type in @overload fn');
394
+ }
395
+
396
+ return [
397
+ ...overloads,
398
+ {
399
+ comment,
400
+ templateTags,
401
+ paramTags,
402
+ returnTag: returnTags[0]
403
+ }
404
+ ];
405
+ }
406
+
407
+ return overloads;
408
+ }, []);
409
+
410
+ if (overloads.length < 2) {
411
+ return;
412
+ }
413
+
414
+ const replacements = overloads.slice().reverse().map(overload => {
415
+
416
+ const {
417
+ comment,
418
+ templateTags,
419
+ paramTags,
420
+ returnTag
421
+ } = overload;
422
+
423
+ const methodCode = `/*${cleanComment(comment).value}*/
424
+ declare function p${
425
+ templateTags.length ? '<' + templateTags.map(
426
+ t => [
427
+ t.param.name,
428
+ t.type ? 'extends ' + t.type.value.slice(1, -1) : null
429
+ ].filter(f => f).join(' ')
430
+ ).join(',') + '>' : ''
431
+ }(${
432
+ paramTags.map(p => {
433
+
434
+ return [
435
+ p.param.name,
436
+ p.param.value.startsWith('[') ? '?' : null,
437
+ ': ',
438
+ p.type.value.slice(1, -1)
439
+ ].filter(f => f).join('');
440
+ }).join(',')
441
+ }) : ${ returnTag ? returnTag.type.value.slice(1, -1) : 'void' };`;
442
+
443
+ const func = parseDts(methodCode).program.body[0];
444
+
445
+ const builder = {
446
+ 'ClassProperty': b.tsDeclareMethod,
447
+ 'TSDeclareFunction': b.tsDeclareFunction,
448
+ 'TSDeclareMethod': b.tsDeclareMethod
449
+ }[node.type];
450
+
451
+ const hostBuilder = {
452
+ 'ExportNamedDeclaration': b.exportNamedDeclaration,
453
+ 'ExportDefaultDeclaration': b.exportDefaultDeclaration
454
+ }[hostPath.value.type];
455
+
456
+ const newNode = builder.from({
457
+ key: node.key,
458
+ params: func.params,
459
+ id: node.id,
460
+ declare: node.declare || false,
461
+ returnType: func.returnType ? b.tsTypeAnnotation.from({
462
+ ...func.returnType
463
+ }) : null,
464
+ comments: hostPath === nodePath ? func.comments : [],
465
+ typeParameters: func.typeParameters ? b.tsTypeParameterDeclaration.from({
466
+ ...func.typeParameters
467
+ }) : null
468
+ });
469
+
470
+ return (
471
+ hostBuilder
472
+ ? hostBuilder.from({
473
+ ...hostPath.value,
474
+ declaration: newNode,
475
+ comments: func.comments
476
+ })
477
+ : newNode
478
+ );
479
+ });
480
+
481
+ replace(hostPath, ...replacements);
482
+
483
+ return replacements;
484
+ }
485
+
230
486
  function cleanComment(comment) {
231
487
 
232
488
  const replacements = [];
@@ -237,7 +493,10 @@ export default function transform(src) {
237
493
  for (const tag of tags) {
238
494
 
239
495
  // remove full line including the non-TS tag
240
- if (/class|constructor|template|method|typedef|property/.test(tag.name)) {
496
+ if (
497
+ /class|constructor|template|method|typedef|property|this|overlord/.test(tag.name) ||
498
+ tag.param?.name?.includes('.')
499
+ ) {
241
500
  replacements.push([ { start: tag.start - 4, end: tag.end } ]);
242
501
 
243
502
  continue;
@@ -304,20 +563,27 @@ export default function transform(src) {
304
563
  return replace(path);
305
564
  }
306
565
 
566
+ if (generateOverloads(path)) {
567
+ return false;
568
+ }
569
+
307
570
  if (fixOptionalArgsMethods(path)) {
308
571
  return false;
309
572
  }
310
573
 
574
+ removeUnknownParams(path);
311
575
  cleanComments(path);
576
+
577
+ fixTypeExport(path);
312
578
  });
313
579
 
314
580
  for (const [ path, args ] of replacements) {
315
581
  try {
316
582
  path.replace(...args);
317
583
  } catch (err) {
318
- console.error('Failed to replace path', path, ...args);
584
+ console.error('Failed to replace path', path, ...args, err);
319
585
 
320
- throw err;
586
+ throw error(path.value, 'failed to replace path: ' + err.message);
321
587
  }
322
588
  }
323
589
 
@@ -342,4 +608,61 @@ function traverse(path, cb) {
342
608
 
343
609
  traverse(path.get(key), cb);
344
610
  }
611
+ }
612
+
613
+
614
+ /**
615
+ * @param {any} node
616
+ * @return {'TSDeclareMethod' | 'TSDeclareFunction' | 'TSFunctionType' | null}
617
+ */
618
+ function getFunctionKind(node) {
619
+
620
+ if (
621
+ node.type === 'TSDeclareMethod'
622
+ ) {
623
+ return 'TSDeclareMethod';
624
+ }
625
+
626
+ if (node.type === 'TSDeclareFunction') {
627
+ return 'TSDeclareFunction';
628
+ }
629
+
630
+ if (
631
+ node.typeAnnotation?.typeAnnotation?.type === 'TSFunctionType'
632
+ ) {
633
+ return 'TSFunctionType';
634
+ }
635
+
636
+ return null;
637
+ }
638
+
639
+ /**
640
+ * Return host path for node (with attached comments).
641
+ *
642
+ * @param {Path} nodePath
643
+ *
644
+ * @return {Path}
645
+ */
646
+ function getHostPath(nodePath) {
647
+ return [ 'ExportDefaultDeclaration', 'ExportNamedDeclaration' ].includes(nodePath.parentPath.value.type)
648
+ ? nodePath.parentPath
649
+ : nodePath;
650
+ }
651
+
652
+ /**
653
+ * @param {Node} node
654
+ *
655
+ * @return {string}
656
+ */
657
+ function error(node, message) {
658
+
659
+ const {
660
+ loc: {
661
+ start
662
+ }
663
+ } = node;
664
+
665
+ const loc = `[line ${start.line + 1}, column ${start.column + 1}]`;
666
+
667
+ return new Error(`${message} ${loc}`);
345
668
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bio-dts",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Generate sane and clean types from JavaScript sources",
5
5
  "type": "module",
6
6
  "main": "index.js",