bio-dts 0.14.1 → 0.15.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
@@ -28,6 +28,7 @@ Generates clean type definitions from ES5 code bases:
28
28
  * Converts JSDoc to [TSDoc](https://github.com/microsoft/tsdoc)
29
29
  * Only exposed documented parameters
30
30
  * Validates, where needed declared parameters
31
+ * Supports [declaration maps](https://www.typescriptlang.org/tsconfig/#declarationMap)
31
32
 
32
33
  Checkout the [test fixtures](./test/fixtures) for full coverage.
33
34
 
@@ -46,11 +47,11 @@ import * as typescript from 'typescript';
46
47
  // transform JS so it keeps the shape,
47
48
  // but is properly digestable by the typescript
48
49
  // compiler
49
- const transformedCode = preTransform(jsCode);
50
+ const { code } = preTransform(jsCode);
50
51
 
51
52
  // post process typescript compiler type code
52
53
  // removing internals, and fixing up the definitions
53
- const transformedCode = postTransform(tsCode);
54
+ const { code } = postTransform(tsCode);
54
55
 
55
56
  // execute the full pipeline, including invoking the
56
57
  // typescript compiler
package/bin/cmd.js CHANGED
@@ -9,6 +9,7 @@ import os from 'node:os';
9
9
  import fs from 'node:fs';
10
10
 
11
11
  import generateTypes from '../lib/generate-types.js';
12
+ import { readTsConfigOptions } from '../lib/tsconfig.js';
12
13
 
13
14
  async function run() {
14
15
 
@@ -63,14 +64,18 @@ async function run() {
63
64
 
64
65
  console.log(`Generating types for ${files.length} files...`);
65
66
 
67
+ const tsConfigOptions = readTsConfigOptions(ts);
68
+
66
69
  const generateOptions = {
67
70
  allowJs: true,
68
71
  declaration: true,
69
72
  emitDeclarationOnly: true,
73
+ ...tsConfigOptions,
70
74
  ...options,
71
75
  verbose
72
76
  };
73
77
 
78
+ verbose && Object.keys(tsConfigOptions).length && console.log('Using tsconfig options', tsConfigOptions);
74
79
  verbose && console.log('Using options ', generateOptions);
75
80
 
76
81
  const {
@@ -4,10 +4,23 @@ import preTransform from './pre-transform.js';
4
4
 
5
5
  import postTransform from './post-transform.js';
6
6
 
7
+ import remapping from '@jridgewell/remapping';
8
+
9
+
7
10
  /**
11
+ * @typedef { import('typescript').CompilerHost } CompilerHost
12
+ * @typedef { import('typescript').WriteFileCallback } WriteFileCallback
13
+ *
14
+ * @typedef { {
15
+ * _readFile: (fileName: string) => string,
16
+ * _writeFile: WriteFileCallback,
17
+ * ___cache: Record<string, string>
18
+ * } } CompilerHostExtension
19
+ *
8
20
  * @typedef { import('typescript').CompilerOptions } CompilerOptions
9
21
  * @typedef { import('typescript').createCompilerHost } createCompilerHost
10
22
  * @typedef { import('typescript').createProgram } createProgram
23
+ *
11
24
  * @typedef { {
12
25
  * createCompilerHost: createCompilerHost,
13
26
  * createProgram: createProgram
@@ -18,7 +31,7 @@ import postTransform from './post-transform.js';
18
31
  * @param { string[] } fileNames
19
32
  * @param { CompilerOptions } compilerOptions
20
33
  * @param { TypeScript } ts
21
- * @param { { lax: boolean } } generateOptions
34
+ * @param { { lax?: boolean } } [generateOptions]
22
35
  */
23
36
  export default function generateTypes(fileNames, compilerOptions, ts, generateOptions) {
24
37
 
@@ -29,13 +42,19 @@ export default function generateTypes(fileNames, compilerOptions, ts, generateOp
29
42
  // CompilerOptions.JsxEmit.None = 0
30
43
  const jsx = compilerOptions.jsx !== 0;
31
44
 
45
+ const declarationMap = compilerOptions.declarationMap;
46
+ const sourceRoot = compilerOptions.sourceRoot;
47
+
32
48
  const names = new Set(fileNames.map((p) => path.resolve(p)));
33
49
 
34
- const host = ts.createCompilerHost(compilerOptions);
50
+ const host = /** @type { CompilerHost & CompilerHostExtension } */ (
51
+ ts.createCompilerHost(compilerOptions)
52
+ );
35
53
 
36
- // @ts-expect-error
37
54
  host._readFile = host.readFile;
38
55
 
56
+ host.___cache = {};
57
+
39
58
  /**
40
59
  * @param {string} fileName
41
60
  *
@@ -43,44 +62,121 @@ export default function generateTypes(fileNames, compilerOptions, ts, generateOp
43
62
  */
44
63
  host.readFile = (fileName) => {
45
64
 
46
- // @ts-expect-error
47
65
  let src = host._readFile(fileName);
48
66
 
49
67
  if (names.has(path.resolve(fileName))) {
50
68
  isVerbose(compilerOptions, fileName) && console.debug('[generate-types] [pre]', fileName, src);
51
69
 
52
70
  try {
53
- src = preTransform(src, { jsx });
71
+
72
+ const sourceMapOptions = declarationMap ? {
73
+ sourceFileName: fileName,
74
+ sourceMapName: fileName + '___pre',
75
+ sourceRoot
76
+ } : {};
77
+
78
+ const transformed = preTransform(src, {
79
+ ...sourceMapOptions,
80
+ jsx
81
+ });
82
+
83
+ if (transformed.map) {
84
+ host.___cache[fileName + '___pre'] = JSON.stringify(transformed.map);
85
+ }
86
+
87
+ src = transformed.code;
88
+
89
+ isVerbose(compilerOptions, fileName) && console.debug('[generate-types] [pre] [generated]', fileName, src);
54
90
  } catch (err) {
55
91
  console.error(`[generate-types] [pre] failed to parse ${fileName} with contents ${src}`, err);
56
92
  throw err;
57
93
  }
58
94
 
59
- isVerbose(compilerOptions, fileName) && console.debug('[generate-types] [pre] [generated]', fileName, src);
60
95
  }
61
96
 
62
97
  return src;
63
98
  };
64
99
 
65
- // @ts-expect-error
66
100
  host._writeFile = host.writeFile;
67
101
 
68
- host.writeFile = (fileName, text, ...args) => {
69
- isVerbose(compilerOptions, fileName) && console.debug('[generate-types] [post]', fileName, text);
102
+ host.writeFile = (fileName, text, writeByteOrderMark, onError, sourceFiles, ...args) => {
103
+
104
+ const [ sourceFile ] = sourceFiles;
105
+
106
+ const originalFileName = /** @type {string|null} */ (
107
+ 'originalFileName' in sourceFile && sourceFile.originalFileName || null
108
+ );
109
+
110
+ if (fileName.toLowerCase().endsWith('.d.ts.map')) {
111
+
112
+ if (originalFileName) {
113
+ host.___cache[originalFileName + '___ts'] = text;
114
+ }
115
+
116
+ return;
117
+ }
70
118
 
71
119
  if (fileName.toLowerCase().endsWith('.d.ts')) {
120
+
121
+ isVerbose(compilerOptions, fileName) && console.debug('[generate-types] [post]', fileName, text);
122
+
72
123
  try {
73
- text = postTransform(text, generateOptions);
124
+ const sourceMap_pre_json = host.___cache[originalFileName + '___pre'];
125
+ const sourceMap_ts_json = host.___cache[originalFileName + '___ts'];
126
+
127
+ const sourceMapOptions = sourceMap_pre_json && sourceMap_ts_json && {
128
+ sourceFileName: fileName + '___ts',
129
+ sourceMapName: fileName,
130
+ sourceRoot
131
+ } || {};
132
+
133
+ const transformed = postTransform(text, {
134
+ ...sourceMapOptions,
135
+ ...generateOptions
136
+ });
137
+
138
+ if (transformed.map) {
139
+ const sourceMap_pre = JSON.parse(sourceMap_pre_json);
140
+ const sourceMap_ts = JSON.parse(sourceMap_ts_json);
141
+ const sourceMap_post = transformed.map;
142
+
143
+ const combined = remapping(
144
+ [
145
+ sourceMap_post,
146
+ sourceMap_ts,
147
+ sourceMap_pre
148
+ ],
149
+ () => null
150
+ );
151
+
152
+ const combinedJSON = JSON.parse(JSON.stringify(combined));
153
+
154
+ // produce source maps with relative paths to sources,
155
+ // following typescript conventions
156
+ if (combinedJSON.sources) {
157
+ const mapDir = path.dirname(path.resolve(fileName));
158
+ combinedJSON.sources = combinedJSON.sources.map(src =>
159
+ path.relative(mapDir, path.resolve(src))
160
+ );
161
+ }
162
+
163
+ // keep maps small, do not inline source text in declaration maps
164
+ delete combinedJSON.sourcesContent;
165
+
166
+ host._writeFile(fileName + '.map', JSON.stringify(combinedJSON), writeByteOrderMark, onError, sourceFiles, ...args);
167
+ }
168
+
169
+ text = transformed.code;
170
+
171
+ isVerbose(compilerOptions, fileName) && console.debug('[generate-types] [post] [generated]', fileName, text);
74
172
  } catch (err) {
75
173
  console.error(`[generate-types] [post] failed to parse ${fileName} with contents ${text}`, err);
76
174
  throw err;
77
175
  }
78
176
 
79
- isVerbose(compilerOptions, fileName) && console.debug('[generate-types] [post] [generated]', fileName, text);
80
177
  }
81
178
 
82
- // @ts-expect-error
83
- host._writeFile(fileName, text, ...args);
179
+ host._writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, ...args);
84
180
  };
85
181
 
86
182
  const program = ts.createProgram(fileNames, compilerOptions, host);
@@ -15,6 +15,12 @@ import {
15
15
  * @typedef { import('./util.js').Path<T> } Path
16
16
  */
17
17
 
18
+ /**
19
+ * @typedef { import('./util.js').ParseOptions } ParseOptions
20
+ *
21
+ * @typedef { { lax?: boolean } & ParseOptions } TransformOptions
22
+ */
23
+
18
24
  /**
19
25
  * @param { { name: string, key?: { name?: string }, comments?: any[] } } m
20
26
  *
@@ -35,17 +41,18 @@ function isPublic(m) {
35
41
 
36
42
  /**
37
43
  * @param { string } src
38
- * @param { { lax: boolean } } [options]
44
+ * @param { TransformOptions } [transformOptions]
39
45
  *
40
- * @return { string }
46
+ * @return { import('./util.js').PrintResult }
41
47
  */
42
- export default function transform(src, options) {
48
+ export default function transform(src, transformOptions = {}) {
43
49
 
44
50
  const {
45
- lax = false
46
- } = options || {};
51
+ lax = false,
52
+ ...parseOptions
53
+ } = transformOptions;
47
54
 
48
- const ast = parseDts(src);
55
+ const ast = parseDts(src, parseOptions);
49
56
  const body = path(ast).get('program', 'body');
50
57
 
51
58
  const replacements = [];
@@ -627,7 +634,7 @@ declare function p${
627
634
  }
628
635
  }
629
636
 
630
- return print(ast).code;
637
+ return print(ast, parseOptions);
631
638
  }
632
639
 
633
640
  function traverse(path, cb) {
@@ -1,10 +1,32 @@
1
1
  import { matcher, parse, path, print } from './util.js';
2
2
 
3
+ import { SourceMapConsumer, SourceMapGenerator } from '@jridgewell/source-map';
4
+
3
5
  /**
4
6
  * @template T
5
7
  * @typedef { import('./util.js').Path<T> } Path
6
8
  */
7
9
 
10
+ /**
11
+ * @typedef { import('ast-types/lib/gen/kinds').PrintableKind } WithLocation
12
+ */
13
+
14
+ /**
15
+ * @typedef { import('./util.js').ParseOptions } ParseOptions
16
+ *
17
+ * @typedef { { jsx?: boolean } & ParseOptions } TransformOptions
18
+ */
19
+
20
+
21
+ /**
22
+ * @typedef { import('recast').types.namedTypes.ClassMethod } ClassMethod
23
+ * @typedef { import('recast').types.namedTypes.ClassDeclaration } ClassDeclaration
24
+ */
25
+
26
+ /**
27
+ * @typedef { import('ast-types').ASTNode } ASTNode
28
+ */
29
+
8
30
  import {
9
31
  parse as parseJSDoc
10
32
  } from './parsers/jsdoc.js';
@@ -15,32 +37,39 @@ import {
15
37
 
16
38
  function superCall() {
17
39
  return b.expressionStatement.from({
18
- 'expression': {
19
- 'type': 'CallExpression',
20
- 'callee': {
21
- 'type': 'Super'
22
- },
23
- 'arguments': []
24
- }
40
+ expression: b.callExpression.from({
41
+ callee: b.super(),
42
+ arguments: []
43
+ })
25
44
  });
26
45
  }
27
46
 
47
+ /**
48
+ * @return { ClassMethod }
49
+ */
28
50
  function clazzConstructor(cls, comments) {
29
51
  const constructor = cls.ctor.value;
30
52
 
31
53
  return b.classMethod.from({
32
- 'static': false,
33
- 'key': {
34
- 'type': 'Identifier',
35
- 'name': 'constructor'
36
- },
37
- 'kind': 'constructor',
38
- 'params': constructor.params,
39
- 'body': constructor.body,
40
- 'comments': comments
54
+ static: false,
55
+ key: b.identifier.from({
56
+ name: 'constructor',
57
+ loc: cls.identifier.loc
58
+ }),
59
+ kind: 'constructor',
60
+ params: constructor.params,
61
+ body: constructor.body,
62
+ comments,
63
+ loc: constructor.loc
41
64
  });
42
65
  }
43
66
 
67
+ /**
68
+ * @param { any } cls
69
+ * @param { ClassMethod } ctor
70
+ *
71
+ * @return { ClassDeclaration }
72
+ */
44
73
  function clazzDefinition(cls, ctor) {
45
74
 
46
75
  const members = [
@@ -49,79 +78,63 @@ function clazzDefinition(cls, ctor) {
49
78
 
50
79
  // class method
51
80
  if (m.member.type === 'FunctionExpression') {
52
- return {
53
- 'type': 'ClassMethod',
54
- 'static': false,
55
- 'key': {
56
- 'type': 'Identifier',
57
- 'name': m.name
58
- },
59
- 'kind': 'method',
60
- 'params': m.member.params,
61
- 'body': m.member.body,
62
- 'comments': m.comments
63
- };
81
+ return b.classMethod.from({
82
+ static: false,
83
+ key: m.identifier,
84
+ kind: 'method',
85
+ async: m.member.async || false,
86
+ params: m.member.params,
87
+ body: m.member.body,
88
+ comments: m.comments || null,
89
+ loc: m.member.loc
90
+ });
64
91
  }
65
92
 
66
93
  // save to transform into class property
67
- return {
68
- 'type': 'ClassProperty',
69
- 'static': false,
70
- 'key': {
71
- 'type': 'Identifier',
72
- 'name': m.name
73
- },
74
- 'value': m.member,
75
- 'comments': m.comments
76
- };
94
+ return b.classProperty.from({
95
+ static: false,
96
+ key: m.identifier,
97
+ value: m.member,
98
+ comments: m.comments || null,
99
+ loc: m.member.loc
100
+ });
77
101
  })
78
102
  ];
79
103
 
80
104
  const staticMembers = cls.staticMembers.map(m => {
81
105
 
82
106
  if (m.member.type === 'FunctionExpression') {
83
- return {
84
- 'type': 'ClassMethod',
85
- 'static': true,
86
- 'key': {
87
- 'type': 'Identifier',
88
- 'name': m.name
89
- },
90
- 'kind': 'method',
91
- 'params': m.member.params,
92
- 'body': m.member.body,
93
- 'comments': m.comments
94
- };
107
+ return b.classMethod.from({
108
+ static: true,
109
+ key: m.identifier,
110
+ kind: 'method',
111
+ async: m.member.async || false,
112
+ params: m.member.params,
113
+ body: m.member.body,
114
+ comments: m.comments || null,
115
+ loc: m.member.loc
116
+ });
95
117
  }
96
118
 
97
- return {
98
- 'type': 'ClassProperty',
99
- 'static': true,
100
- 'key': {
101
- 'type': 'Identifier',
102
- 'name': m.name
103
- },
104
- 'value': m.member,
105
- 'comments': m.comments
106
- };
119
+ return b.classProperty.from({
120
+ static: true,
121
+ key: m.identifier,
122
+ value: m.member,
123
+ comments: m.comments || null,
124
+ loc: m.member.loc
125
+ });
107
126
  });
108
127
 
109
128
  return b.classDeclaration.from({
110
- 'id': {
111
- 'type': 'Identifier',
112
- 'name': cls.name
113
- },
114
- 'superClass': cls.superDeclaration ? {
115
- 'type': 'Identifier',
116
- 'name': cls.superDeclaration.clazz
117
- } : null,
118
- 'body': {
119
- 'type': 'ClassBody',
120
- 'body': [
129
+ id: cls.identifier,
130
+ superClass: cls.superDeclaration ? cls.superDeclaration.clazz : null,
131
+ body: b.classBody.from({
132
+ body: [
121
133
  ...members,
122
134
  ...staticMembers
123
135
  ]
124
- }
136
+ }),
137
+ loc: cls.node.value.loc
125
138
  });
126
139
  }
127
140
 
@@ -136,21 +149,23 @@ function splitComment(cls) {
136
149
  const [ intro, ...rest ] = comment?.value?.split(/\* @param/) || [];
137
150
 
138
151
  return {
139
- ctorComments: rest.length ? [ {
140
- leading: true,
141
- trailing: false,
142
- type: 'CommentBlock',
143
- value: '*\n * @param' + rest.join('* @param'),
144
- } ] : [],
152
+ ctorComments: rest.length ? [
153
+ b.commentBlock.from({
154
+ leading: true,
155
+ trailing: false,
156
+ value: '*\n * @param' + rest.join('* @param'),
157
+ loc: comment.loc
158
+ })
159
+ ] : [],
145
160
  additionalComments: [
146
161
  ...additionalComments,
147
162
  ...(
148
163
  intro ? [
149
- {
164
+ b.commentBlock.from({
150
165
  leading: true,
151
- type: 'CommentBlock',
152
- value: intro
153
- }
166
+ value: intro,
167
+ loc: comment.loc
168
+ })
154
169
  ] : []
155
170
  )
156
171
  ]
@@ -159,13 +174,25 @@ function splitComment(cls) {
159
174
 
160
175
  /**
161
176
  * @param { string } src
162
- * @param { { jsx?: boolean } } [parseOptions]
177
+ * @param { TransformOptions } [transformOptions]
163
178
  *
164
- * @return {string}
179
+ * @return { import('./util.js').PrintResult }
165
180
  */
166
- export default function transform(src, parseOptions = {}) {
181
+ export default function transform(src, transformOptions = {}) {
182
+
183
+ const {
184
+ jsx,
185
+ ...parseOptions
186
+ } = transformOptions;
187
+
188
+ /**
189
+ * @type { ASTNode }
190
+ */
191
+ const ast = parse(src, transformOptions);
167
192
 
168
- const ast = parse(src, parseOptions);
193
+ /**
194
+ * @type {Path<ASTNode[]>}
195
+ */
169
196
  const body = path(ast).get('program', 'body');
170
197
 
171
198
  const inheritsImports = findInheritsImports(body);
@@ -229,9 +256,115 @@ export default function transform(src, parseOptions = {}) {
229
256
 
230
257
  inheritsImports.forEach(m => m.node.replace());
231
258
 
232
- return print(ast).code;
259
+ const {
260
+ code,
261
+ map
262
+ } = print(ast, parseOptions);
263
+
264
+ if (!map) {
265
+ return {
266
+ code
267
+ };
268
+ } else {
269
+
270
+ // source maps are broken through function -> class transformation
271
+ // we have to manually fix them
272
+ const generatedAst = parse(code, transformOptions);
273
+
274
+ return {
275
+ code,
276
+ map: fixSourceMap(generatedAst, map, classes)
277
+ };
278
+ }
233
279
  }
234
280
 
281
+
282
+ /**
283
+ * @param {ASTNode} generatedAst
284
+ * @param {any} map
285
+ * @param {any} originalClasses
286
+ *
287
+ * @return {any} fixed map
288
+ */
289
+ function fixSourceMap(generatedAst, map, originalClasses) {
290
+
291
+ /**
292
+ * @type {Path<ASTNode[]>}
293
+ */
294
+ const body = path(generatedAst).get('program', 'body');
295
+
296
+ const consumer = new SourceMapConsumer(map);
297
+
298
+ const source = consumer.sources[0];
299
+
300
+ const generator = SourceMapGenerator.fromSourceMap(consumer);
301
+
302
+ for (const originalCls of originalClasses) {
303
+
304
+ const clsName = originalCls.identifier.name;
305
+
306
+ // add mappings for classes and their constructors
307
+ // back to the original function
308
+ const generatedCls = findClass(body, clsName);
309
+
310
+ const generatedCtor = findClassCtor(generatedCls.clsDeclaration);
311
+
312
+ // add mapping from class constructor to original fn
313
+ generator.addMapping({
314
+ generated: {
315
+ line: generatedCtor.value.loc.start.line,
316
+ column: generatedCtor.value.loc.start.column,
317
+ },
318
+ name: `${clsName} constructor`,
319
+ source,
320
+ original: {
321
+ line: originalCls.loc.start.line,
322
+ column: originalCls.loc.start.column
323
+ }
324
+ });
325
+
326
+ // add mapping from class to original fn
327
+ generator.addMapping({
328
+ generated: {
329
+ line: generatedCls.node.value.loc.start.line,
330
+ column: generatedCls.node.value.loc.start.column,
331
+ },
332
+ name: `${clsName} class`,
333
+ source,
334
+ original: {
335
+ line: originalCls.loc.start.line,
336
+ column: originalCls.loc.start.column
337
+ }
338
+ });
339
+
340
+ // add mapping for static members
341
+ for (const originalMember of originalCls.staticMembers) {
342
+
343
+ const name = originalMember.identifier.name;
344
+
345
+ const generatedMember = findStaticMember(generatedCls.clsDeclaration, name);
346
+
347
+ // add mapping from member to original
348
+ generator.addMapping({
349
+ generated: {
350
+ line: generatedMember.value.loc.start.line,
351
+ column: generatedMember.value.loc.start.column,
352
+ },
353
+ name: `${clsName}#${name}`,
354
+ source,
355
+ original: {
356
+ line: originalMember.loc.start.line,
357
+ column: originalMember.loc.start.column
358
+ }
359
+ });
360
+ }
361
+ }
362
+
363
+
364
+ return generator.toJSON();
365
+ }
366
+
367
+
235
368
  function findInheritsImports(nodes) {
236
369
  const importsMatches = matcher`
237
370
  import $1 from 'inherits-browser';
@@ -261,14 +394,14 @@ function findSuperDeclarations(cls, nodes, importDeclarations) {
261
394
 
262
395
  if (inheritsExpressions.length === 1) {
263
396
 
264
- const clazz = inheritsExpressions[0][1].value.name;
397
+ const clazz = inheritsExpressions[0][1].value;
265
398
 
266
399
  const inheritsExpression = {
267
400
  node: inheritsExpressions[0][0]
268
401
  };
269
402
 
270
403
  const superCallMatches = matcher`
271
- ${clazz}.call($$$);
404
+ ${clazz.name}.call($$$);
272
405
  `;
273
406
 
274
407
  const superCalls = superCallMatches(cls.ctor.get('body', 'body'));
@@ -284,12 +417,14 @@ function findSuperDeclarations(cls, nodes, importDeclarations) {
284
417
  }
285
418
 
286
419
  /**
287
- * @param {any[]} nodes
420
+ * @param {Path<ASTNode[]>} nodes
288
421
  *
289
422
  * @return { {
290
- * name: string,
423
+ * identifier: any,
291
424
  * ctor: any,
292
- * node: any
425
+ * node: any,
426
+ * name: string,
427
+ * loc: any
293
428
  * }[] }
294
429
  */
295
430
  function findConstructors(nodes) {
@@ -335,14 +470,111 @@ function findConstructors(nodes) {
335
470
  if (isClassName(name) && !isFunctionTagged(node) && !isReturning(ctor)) {
336
471
 
337
472
  return {
473
+ identifier: identifier.value,
338
474
  name,
339
475
  ctor,
340
- node
476
+ node,
477
+ loc: node.value.loc
341
478
  };
342
479
  }
343
480
  }).filter(n => n);
344
481
  }
345
482
 
483
+ /**
484
+ * @param { Path<ASTNode> } clsNode
485
+ *
486
+ * @return { Path<ASTNode & WithLocation> }
487
+ */
488
+ function findClassCtor(clsNode) {
489
+
490
+ const ctors = clsNode.get('body', 'body').filter(node => {
491
+ const value = node.value;
492
+
493
+ return value.type === 'ClassMethod' && value.key?.name === 'constructor';
494
+ });
495
+
496
+ if (!ctors.length) {
497
+ throw new Error('no constructor');
498
+ }
499
+
500
+ return ctors[0];
501
+ }
502
+
503
+ /**
504
+ * @param { Path<ASTNode> } clsNode
505
+ * @param { string } name
506
+ *
507
+ * @return { Path<ASTNode & WithLocation> }> }
508
+ */
509
+ function findStaticMember(clsNode, name) {
510
+
511
+ const members = clsNode.get('body', 'body').filter(node => {
512
+ const value = node.value;
513
+
514
+ return [ 'ClassProperty', 'ClassMethod' ].includes(value.type) && value.static && value.key?.name === name;
515
+ });
516
+
517
+ if (!members.length) {
518
+ throw new Error(`no member ${name}`);
519
+ }
520
+
521
+ return members[0];
522
+ }
523
+
524
+ /**
525
+ * @param { Path<ASTNode[]> } nodes
526
+ * @param { string } name
527
+ *
528
+ * @return { {
529
+ * clsDeclaration: any,
530
+ * node: any
531
+ * } }
532
+ */
533
+ function findClass(nodes, name) {
534
+
535
+ const cls = matcher`
536
+ class ${name} extends $$$ {
537
+ $$$
538
+ }
539
+ `;
540
+
541
+ const namedExportCls = matcher`
542
+ export class ${name} extends $$$ {
543
+ $$$
544
+ }
545
+ `;
546
+
547
+ const defaultExportCls = matcher`
548
+ export default class ${name} extends $$$ {
549
+ $$$
550
+ }
551
+ `;
552
+
553
+ const matches = [
554
+ ...cls(nodes),
555
+ ...namedExportCls(nodes).map(
556
+ m => [ m[0].get('declaration'), m[0] ]
557
+ ),
558
+ ...defaultExportCls(nodes).map(
559
+ m => [ m[0].get('declaration'), m[0] ]
560
+ )
561
+ ].map(match => {
562
+
563
+ const [ clsDeclaration, node = clsDeclaration ] = match;
564
+
565
+ return {
566
+ clsDeclaration,
567
+ node
568
+ };
569
+ });
570
+
571
+ if (!matches.length) {
572
+ throw new Error(`not found: class ${name}`);
573
+ }
574
+
575
+ return matches[0];
576
+ }
577
+
346
578
  /**
347
579
  * @param { string } name
348
580
  *
@@ -389,6 +621,10 @@ function isReturning(ctorPath) {
389
621
  return returnStatements(ctorPath.get('body', 'body')).length > 0;
390
622
  }
391
623
 
624
+ /**
625
+ * @param {any} cls
626
+ * @param {Path<ASTNode[]>} nodes
627
+ */
392
628
  function findStaticMembers(cls, nodes) {
393
629
 
394
630
  const names = matcher`
@@ -399,14 +635,19 @@ function findStaticMembers(cls, nodes) {
399
635
  const [ node, identifier, member ] = match;
400
636
 
401
637
  return {
402
- name: identifier.value.name,
638
+ identifier: identifier.value,
403
639
  member: member.value,
404
640
  comments: node.get('comments').value,
405
- node
641
+ node,
642
+ loc: node.value.loc
406
643
  };
407
644
  });
408
645
  }
409
646
 
647
+ /**
648
+ * @param {any} cls
649
+ * @param {Path<ASTNode[]>} nodes
650
+ */
410
651
  function findProtoMembers(cls, nodes) {
411
652
 
412
653
  const names = matcher`
@@ -417,7 +658,7 @@ function findProtoMembers(cls, nodes) {
417
658
  const [ node, identifier, member ] = match;
418
659
 
419
660
  return {
420
- name: identifier.value.name,
661
+ identifier: identifier.value,
421
662
  member: member.value,
422
663
  comments: node.get('comments').value,
423
664
  node,
@@ -426,6 +667,9 @@ function findProtoMembers(cls, nodes) {
426
667
  });
427
668
  }
428
669
 
670
+ /**
671
+ * @param {any} cls
672
+ */
429
673
  function findEmbeddedMembers(cls) {
430
674
 
431
675
  const memberDeclarations = matcher`
@@ -436,7 +680,7 @@ function findEmbeddedMembers(cls) {
436
680
  const [ node, identifier, member ] = match;
437
681
 
438
682
  return {
439
- name: identifier.value.name,
683
+ identifier: identifier.value,
440
684
  member: member.value,
441
685
  comments: node.get('comments').value,
442
686
  node,
@@ -0,0 +1,43 @@
1
+ import path from 'node:path';
2
+
3
+ /**
4
+ * Read ESM-relevant compiler options from the nearest tsconfig.json,
5
+ * starting from `cwd`.
6
+ *
7
+ * Options are sourced selectively: only `module`, `moduleResolution`, and
8
+ * `target` are returned because these affect how declarations are generated.
9
+ * Other options (strict, lib, paths, …) are not meaningful here.
10
+ *
11
+ * @param { import('typescript') } ts
12
+ * @param { string } [cwd]
13
+ *
14
+ * @return { Partial<import('typescript').CompilerOptions> }
15
+ */
16
+ export function readTsConfigOptions(ts, cwd = process.cwd()) {
17
+
18
+ const configPath = ts.findConfigFile(cwd, ts.sys.fileExists);
19
+
20
+ if (!configPath) {
21
+ return {};
22
+ }
23
+
24
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
25
+
26
+ if (configFile.error) {
27
+ return {};
28
+ }
29
+
30
+ const { options } = ts.parseJsonConfigFileContent(
31
+ configFile.config,
32
+ ts.sys,
33
+ path.dirname(configPath)
34
+ );
35
+
36
+ const result = /** @type { Partial<import('typescript').CompilerOptions> } */ ({});
37
+
38
+ if (options.module !== undefined) result.module = options.module;
39
+ if (options.moduleResolution !== undefined) result.moduleResolution = options.moduleResolution;
40
+ if (options.target !== undefined) result.target = options.target;
41
+
42
+ return result;
43
+ }
package/lib/util.js CHANGED
@@ -6,8 +6,22 @@ import { Path as PathConstructor } from 'ast-types';
6
6
  * @typedef { import('ast-types/lib/path.js').Path<T> } Path
7
7
  */
8
8
 
9
+ /**
10
+ * @typedef { import('recast/lib/printer.js').PrintResultType } PrintResult
11
+ */
12
+
13
+ /**
14
+ * @typedef { {
15
+ * inputSourceMap?: string,
16
+ * sourceFileName?: string,
17
+ * sourceMapName?: string,
18
+ * sourceRoot?: string,
19
+ * } } ParseOptions
20
+ */
21
+
9
22
  import * as typescriptParser from './parsers/typescript.js';
10
23
 
24
+
11
25
  const DBG = /match/.test(process?.env?.LOG_DEBUG);
12
26
 
13
27
  const formatOptions = {
@@ -50,22 +64,35 @@ export function hasProperty(obj, property) {
50
64
 
51
65
  /**
52
66
  * @param { string } code
53
- * @param { { jsx?: boolean } } [parseOptions]
67
+ * @param { ParseOptions & { jsx?: boolean } } [parseOptions]
54
68
  *
55
69
  * @return {any}
56
70
  */
57
- export function parse(code, parseOptions) {
71
+ export function parse(code, parseOptions = {}) {
72
+
73
+ const {
74
+ jsx = false,
75
+ ...options
76
+ } = parseOptions;
77
+
58
78
  const parser = {
59
- parse: parseOptions?.jsx ? typescriptParser.jsx : typescriptParser.js
79
+ parse: jsx ? typescriptParser.jsx : typescriptParser.js
60
80
  };
61
81
 
62
82
  return recastParse(code, {
63
83
  parser,
64
- ...formatOptions
84
+ ...formatOptions,
85
+ ...options
65
86
  });
66
87
  }
67
88
 
68
- export function parseDts(code) {
89
+ /**
90
+ * @param { string } code
91
+ * @param { ParseOptions } [options]
92
+ *
93
+ * @return { any }
94
+ */
95
+ export function parseDts(code, options = {}) {
69
96
 
70
97
  const parser = {
71
98
  parse: typescriptParser.dts
@@ -73,12 +100,22 @@ export function parseDts(code) {
73
100
 
74
101
  return recastParse(code, {
75
102
  parser,
76
- ...formatOptions
103
+ ...formatOptions,
104
+ ...options
77
105
  });
78
106
  }
79
107
 
80
- export function print(node) {
81
- return recastPrint(node, formatOptions);
108
+ /**
109
+ * @param { any } node
110
+ * @param { ParseOptions } [options]
111
+ *
112
+ * @return { PrintResult }
113
+ */
114
+ export function print(node, options = {}) {
115
+ return recastPrint(node, {
116
+ ...formatOptions,
117
+ ...options
118
+ });
82
119
  }
83
120
 
84
121
  /**
@@ -139,6 +176,10 @@ export function matcher(strings, ...args) {
139
176
  node = node[0];
140
177
  }
141
178
 
179
+ if (node && node.type === 'ClassProperty') {
180
+ node = node.key;
181
+ }
182
+
142
183
  if (node && node.type === 'ExpressionStatement') {
143
184
  node = node.expression;
144
185
  }
@@ -182,6 +223,10 @@ export function matcher(strings, ...args) {
182
223
  node = node[0];
183
224
  }
184
225
 
226
+ if (node && node.type === 'ClassProperty') {
227
+ node = node.key;
228
+ }
229
+
185
230
  if (node && node.type === 'ExpressionStatement') {
186
231
  node = node.expression;
187
232
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bio-dts",
3
- "version": "0.14.1",
3
+ "version": "0.15.1",
4
4
  "description": "Generate sane and clean types from JavaScript sources",
5
5
  "type": "module",
6
6
  "bin": "./bin/cmd.js",
@@ -11,14 +11,16 @@
11
11
  "./package.json": "./package.json"
12
12
  },
13
13
  "scripts": {
14
- "all": "run-s lint check-types test test:dts:*",
14
+ "all": "run-s lint check-types test test:dts",
15
15
  "lint": "eslint .",
16
16
  "check-types": "tsc --noEmit",
17
17
  "test": "mocha test",
18
- "test:dts:default": "node bin/cmd.js --outDir test/fixtures/snapshots/default -r test/fixtures/pre",
19
- "test:dts:jsx": "node bin/cmd.js --jsx preserve --outDir test/fixtures/snapshots/jsx -r test/fixtures/jsx",
20
- "test:dts:declaration-map": "node bin/cmd.js --declarationMap --outDir test/fixtures/snapshots/declaration-map -r test/fixtures/pre",
21
- "test:dts:lax": "node bin/cmd.js --declarationMap --lax --outDir test/fixtures/snapshots/lax -r test/fixtures/lax"
18
+ "test:dts": "run-s test:dts:*",
19
+ "test:dts:default": "node bin/cmd.js --module CommonJS --outDir test/fixtures/snapshots/default -r test/fixtures/pre",
20
+ "test:dts:jsx": "node bin/cmd.js --module CommonJS --jsx preserve --outDir test/fixtures/snapshots/jsx -r test/fixtures/jsx",
21
+ "test:dts:declaration-map": "node bin/cmd.js --module CommonJS --declarationMap --outDir test/fixtures/snapshots/declaration-map -r test/fixtures/pre",
22
+ "test:dts:lax": "node bin/cmd.js --module CommonJS --declarationMap --lax --outDir test/fixtures/snapshots/lax -r test/fixtures/lax",
23
+ "test:dts:esm": "node bin/cmd.js --outDir test/fixtures/snapshots/esm -r test/fixtures/esm"
22
24
  },
23
25
  "repository": {
24
26
  "type": "git",
@@ -27,19 +29,21 @@
27
29
  "author": "Nico Rehwaldt",
28
30
  "license": "MIT",
29
31
  "dependencies": {
30
- "@babel/parser": "^7.28.5",
32
+ "@babel/parser": "^7.29.7",
33
+ "@jridgewell/remapping": "^2.3.5",
34
+ "@jridgewell/source-map": "^0.3.11",
31
35
  "recast": "^0.23.11",
32
36
  "tiny-glob": "^0.2.9"
33
37
  },
34
38
  "devDependencies": {
35
- "@babel/types": "^7.28.5",
39
+ "@babel/types": "^7.29.7",
36
40
  "@types/mocha": "^10.0.10",
37
- "@types/node": "^24.10.4",
38
- "chai": "^6.2.1",
39
- "eslint": "^8.56.0",
40
- "eslint-plugin-bpmn-io": "^1.0.0",
41
- "mocha": "^11.7.5",
42
- "npm-run-all2": "^8.0.4",
41
+ "@types/node": "^24.13.0",
42
+ "chai": "^6.2.2",
43
+ "eslint": "^9.39.4",
44
+ "eslint-plugin-bpmn-io": "^2.3.1",
45
+ "mocha": "^11.7.6",
46
+ "npm-run-all2": "^9.0.1",
43
47
  "typescript": "^5.9.3"
44
48
  },
45
49
  "sideEffects": false,