bio-dts 0.1.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 +41 -0
- package/bin/bio-dts.js +79 -0
- package/index.js +5 -0
- package/lib/generate-types.js +62 -0
- package/lib/parsers/typescript-dts.js +15 -0
- package/lib/parsers/typescript.js +1 -0
- package/lib/post-transform.js +64 -0
- package/lib/pre-transform.js +341 -0
- package/lib/util.js +262 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# bio-dts
|
|
2
|
+
|
|
3
|
+
[](https://github.com/nikku/bio-dts/actions/workflows/CI.yml)
|
|
4
|
+
|
|
5
|
+
Utilities to generate sane and clean type definitions from JavaScript files.
|
|
6
|
+
|
|
7
|
+
## About
|
|
8
|
+
|
|
9
|
+
This module provides `pre` and `post` processing helpers to a type definition pipeline, as well as a simple [generator cli](#usage).
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npx bio-dts --outDir dist/types -r lib
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## API
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
import {
|
|
22
|
+
preTransform,
|
|
23
|
+
postTransform,
|
|
24
|
+
generateTypes
|
|
25
|
+
} from 'bio-dts';
|
|
26
|
+
|
|
27
|
+
// transform JS so it keeps the shape,
|
|
28
|
+
// but is properly digestable by the typescript
|
|
29
|
+
// compiler
|
|
30
|
+
const transformedCode = preTransform(jsCode);
|
|
31
|
+
|
|
32
|
+
// post process typescript compiler type code
|
|
33
|
+
// removing internals, and fixing up the definitions
|
|
34
|
+
const transformedCode = postTransform(tsCode);
|
|
35
|
+
|
|
36
|
+
// execute the full pipeline, including invoking the
|
|
37
|
+
// typescript compiler
|
|
38
|
+
generateTypes(files, {
|
|
39
|
+
outDir: 'dist'
|
|
40
|
+
});
|
|
41
|
+
```
|
package/bin/bio-dts.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
import glob from 'tiny-glob';
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
|
|
10
|
+
import generateTypes from '../lib/generate-types.js';
|
|
11
|
+
|
|
12
|
+
async function run() {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
options,
|
|
17
|
+
fileNames
|
|
18
|
+
} = ts.parseCommandLine(args);
|
|
19
|
+
|
|
20
|
+
const help = args.includes('--help') || args.includes('-h');
|
|
21
|
+
const verbose = args.includes('--verbose');
|
|
22
|
+
const recursive = args.includes('--recursive') || args.includes('-r');
|
|
23
|
+
|
|
24
|
+
if (help) {
|
|
25
|
+
console.log(`Usage: bio-dts [options] [...filesOrGlobs]
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--recursive, -r recurse into directories
|
|
29
|
+
--verbose enable verbose logging
|
|
30
|
+
|
|
31
|
+
Additional options will be passed to the typescript generator.
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
$ bio-dts --outDir dist -r lib
|
|
35
|
+
$ bio-dts 'lib/**/*.js'
|
|
36
|
+
`);
|
|
37
|
+
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const globPattern = recursive ? '**/*.js' : '*.js';
|
|
42
|
+
|
|
43
|
+
const files = [];
|
|
44
|
+
|
|
45
|
+
for (const fileName of fileNames) {
|
|
46
|
+
|
|
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) {
|
|
57
|
+
|
|
58
|
+
// ignore
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(`Generating types for ${files.length} files...`);
|
|
63
|
+
|
|
64
|
+
generateTypes(files, {
|
|
65
|
+
allowJs: true,
|
|
66
|
+
declaration: true,
|
|
67
|
+
emitDeclarationOnly: true,
|
|
68
|
+
...options,
|
|
69
|
+
verbose
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log('Done.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
run().catch(err => {
|
|
76
|
+
console.error(err);
|
|
77
|
+
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
package/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
import preTransform from './pre-transform.js';
|
|
6
|
+
|
|
7
|
+
import postTransform from './post-transform.js';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string[]} fileNames
|
|
12
|
+
* @param {ts.CompilerOptions} options
|
|
13
|
+
*/
|
|
14
|
+
export default function generateTypes(fileNames, options) {
|
|
15
|
+
|
|
16
|
+
const names = new Set(fileNames.map((p) => path.resolve(p)));
|
|
17
|
+
|
|
18
|
+
const host = ts.createCompilerHost(options);
|
|
19
|
+
|
|
20
|
+
host._readFile = host.readFile;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} fileName
|
|
24
|
+
*
|
|
25
|
+
* @return {string}
|
|
26
|
+
*/
|
|
27
|
+
host.readFile = (fileName) => {
|
|
28
|
+
options.verbose && console.debug('[generate-types] [pre]', fileName);
|
|
29
|
+
|
|
30
|
+
let src = host._readFile(fileName);
|
|
31
|
+
|
|
32
|
+
if (names.has(path.resolve(fileName))) {
|
|
33
|
+
try {
|
|
34
|
+
src = preTransform(src);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(`failed to parse ${fileName} [pre] with contents ${src}`, err);
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return src;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
host._writeFile = host.writeFile;
|
|
45
|
+
|
|
46
|
+
host.writeFile = (fileName, text, ...args) => {
|
|
47
|
+
options.verbose && console.debug('[generate-types] [post]', fileName, text);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
text = postTransform(text);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(`failed to parse ${fileName} [post] with contents ${text}`, err);
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
host._writeFile(fileName, text, ...args);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const program = ts.createProgram(fileNames, options, host);
|
|
60
|
+
|
|
61
|
+
program.emit();
|
|
62
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { parser } from 'recast/parsers/babel.js';
|
|
2
|
+
import getBabelOptions from 'recast/parsers/_babel_options.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} source
|
|
6
|
+
* @param {Overrides} [options]
|
|
7
|
+
*
|
|
8
|
+
* @return {import('@babel/types').File}
|
|
9
|
+
*/
|
|
10
|
+
export function parse(source, options) {
|
|
11
|
+
|
|
12
|
+
const babelOptions = getBabelOptions.default(options);
|
|
13
|
+
babelOptions.plugins.push([ 'typescript', { dts: true } ]);
|
|
14
|
+
return parser.parse(source, babelOptions);
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from 'recast/parsers/typescript.js';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { print } from 'recast';
|
|
2
|
+
|
|
3
|
+
import { isArray, isNode, keys, parseDts, path } from './util.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param { { name: string, comments?: any[] } }
|
|
8
|
+
*
|
|
9
|
+
* @return {boolean}
|
|
10
|
+
*/
|
|
11
|
+
function isPublic(m) {
|
|
12
|
+
|
|
13
|
+
const name = m?.key?.name;
|
|
14
|
+
|
|
15
|
+
if (!name) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const comments = m?.comments || [];
|
|
20
|
+
|
|
21
|
+
return !name.startsWith('_') && !comments.some(c => c.value.includes('@private'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} src
|
|
26
|
+
* @return {string}
|
|
27
|
+
*/
|
|
28
|
+
export default function transform(src) {
|
|
29
|
+
|
|
30
|
+
const ast = parseDts(src);
|
|
31
|
+
const body = path(ast).get('program', 'body');
|
|
32
|
+
|
|
33
|
+
traverse(body, function(path) {
|
|
34
|
+
|
|
35
|
+
// filter private members
|
|
36
|
+
if (!isPublic(path.value)) {
|
|
37
|
+
path.replace();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// TODO(nikku): transform illegal typescript methods into override calls
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return print(ast).code;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function traverse(path, cb) {
|
|
47
|
+
cb(path);
|
|
48
|
+
|
|
49
|
+
if (path.parentPath) {
|
|
50
|
+
const val = path.value;
|
|
51
|
+
|
|
52
|
+
if (!isNode(val) && !isArray(val)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const key of keys(val).reverse()) {
|
|
57
|
+
if ([ 'loc', 'start', 'end' ].includes(key)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
traverse(path.get(key), cb);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { print } from 'recast';
|
|
2
|
+
|
|
3
|
+
import { matcher, parse, path } from './util.js';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
builders as b
|
|
7
|
+
} from 'ast-types';
|
|
8
|
+
|
|
9
|
+
function superCall() {
|
|
10
|
+
return b.expressionStatement.from({
|
|
11
|
+
'type': 'ExpressionStatement',
|
|
12
|
+
'expression': {
|
|
13
|
+
'type': 'CallExpression',
|
|
14
|
+
'callee': {
|
|
15
|
+
'type': 'Super'
|
|
16
|
+
},
|
|
17
|
+
'arguments': []
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clazzConstructor(cls, comments) {
|
|
23
|
+
const constructor = cls.ctor.value;
|
|
24
|
+
|
|
25
|
+
return b.classMethod.from({
|
|
26
|
+
'type': 'ClassMethod',
|
|
27
|
+
'static': false,
|
|
28
|
+
'key': {
|
|
29
|
+
'type': 'Identifier',
|
|
30
|
+
'name': 'constructor'
|
|
31
|
+
},
|
|
32
|
+
'kind': 'constructor',
|
|
33
|
+
'params': constructor.params,
|
|
34
|
+
'body': constructor.body,
|
|
35
|
+
'comments': comments
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clazzDefinition(cls, ctor) {
|
|
40
|
+
|
|
41
|
+
const members = [
|
|
42
|
+
ctor,
|
|
43
|
+
...cls.members.filter(
|
|
44
|
+
m => m.member.type === 'FunctionExpression'
|
|
45
|
+
).map(m => {
|
|
46
|
+
return {
|
|
47
|
+
'type': 'ClassMethod',
|
|
48
|
+
'static': false,
|
|
49
|
+
'key': {
|
|
50
|
+
'type': 'Identifier',
|
|
51
|
+
'name': m.name
|
|
52
|
+
},
|
|
53
|
+
'kind': 'method',
|
|
54
|
+
'params': m.member.params,
|
|
55
|
+
'body': m.member.body,
|
|
56
|
+
'comments': m.comments
|
|
57
|
+
};
|
|
58
|
+
})
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const staticMembers = cls.staticMembers.map(m => {
|
|
62
|
+
|
|
63
|
+
if (m.member.type === 'FunctionExpression') {
|
|
64
|
+
return {
|
|
65
|
+
'type': 'ClassMethod',
|
|
66
|
+
'static': true,
|
|
67
|
+
'key': {
|
|
68
|
+
'type': 'Identifier',
|
|
69
|
+
'name': m.name
|
|
70
|
+
},
|
|
71
|
+
'kind': 'method',
|
|
72
|
+
'params': m.member.params,
|
|
73
|
+
'body': m.member.body,
|
|
74
|
+
'comments': m.comments
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
'type': 'ClassProperty',
|
|
80
|
+
'static': true,
|
|
81
|
+
'key': {
|
|
82
|
+
'type': 'Identifier',
|
|
83
|
+
'name': m.name
|
|
84
|
+
},
|
|
85
|
+
'value': m.member,
|
|
86
|
+
'comments': m.comments
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return b.classDeclaration.from({
|
|
91
|
+
'type': 'ClassDeclaration',
|
|
92
|
+
'id': {
|
|
93
|
+
'type': 'Identifier',
|
|
94
|
+
'name': cls.name
|
|
95
|
+
},
|
|
96
|
+
'superClass': cls.superDeclaration ? {
|
|
97
|
+
'type': 'Identifier',
|
|
98
|
+
'name': cls.superDeclaration.clazz
|
|
99
|
+
} : null,
|
|
100
|
+
'body': {
|
|
101
|
+
'type': 'ClassBody',
|
|
102
|
+
'body': [
|
|
103
|
+
...members,
|
|
104
|
+
...staticMembers
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function splitComment(cls) {
|
|
111
|
+
|
|
112
|
+
const node = cls.node.value;
|
|
113
|
+
|
|
114
|
+
const [ comment ] = node.comments?.slice(-1) || [];
|
|
115
|
+
|
|
116
|
+
const additionalComments = node.comments?.slice(0, -1) || [];
|
|
117
|
+
|
|
118
|
+
const [ intro, ...rest ] = comment?.value?.split(/\* @param/) || [];
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
ctorComments: rest.length ? [ {
|
|
122
|
+
leading: true,
|
|
123
|
+
trailing: false,
|
|
124
|
+
type: 'CommentBlock',
|
|
125
|
+
value: '*\n * @param' + rest.join('* @param'),
|
|
126
|
+
} ] : [],
|
|
127
|
+
additionalComments: [
|
|
128
|
+
...additionalComments,
|
|
129
|
+
...(
|
|
130
|
+
intro ? [
|
|
131
|
+
{
|
|
132
|
+
leading: true,
|
|
133
|
+
type: 'CommentBlock',
|
|
134
|
+
value: intro
|
|
135
|
+
}
|
|
136
|
+
] : []
|
|
137
|
+
)
|
|
138
|
+
]
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} src
|
|
144
|
+
* @return {string}
|
|
145
|
+
*/
|
|
146
|
+
export default function transform(src) {
|
|
147
|
+
|
|
148
|
+
const ast = parse(src);
|
|
149
|
+
const body = path(ast).get('program', 'body');
|
|
150
|
+
|
|
151
|
+
const classes = findConstructors(body);
|
|
152
|
+
|
|
153
|
+
for (const cls of classes) {
|
|
154
|
+
const members = cls.members = [
|
|
155
|
+
...findEmbeddedMethods(cls),
|
|
156
|
+
...findProtoMethods(cls, body)
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const staticMembers = cls.staticMembers = [
|
|
160
|
+
...findStaticMembers(cls, body)
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const superDeclaration = cls.superDeclaration = findSuperDeclarations(cls, body);
|
|
164
|
+
|
|
165
|
+
if (superDeclaration) {
|
|
166
|
+
cls.ctor.get('body', 'body').unshift(superCall());
|
|
167
|
+
|
|
168
|
+
superDeclaration.superCalls.forEach(m => m.node.replace());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const {
|
|
172
|
+
ctorComments,
|
|
173
|
+
additionalComments
|
|
174
|
+
} = splitComment(cls);
|
|
175
|
+
|
|
176
|
+
const ctor = clazzConstructor(cls, ctorComments);
|
|
177
|
+
|
|
178
|
+
cls.ctor.replace(clazzDefinition(cls, ctor));
|
|
179
|
+
cls.node.get('comments').replace(additionalComments);
|
|
180
|
+
|
|
181
|
+
members.forEach(m => {
|
|
182
|
+
if (m.member.type === 'FunctionExpression') {
|
|
183
|
+
m.node.replace();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
staticMembers.forEach(m => {
|
|
188
|
+
m.node.replace();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (superDeclaration) {
|
|
192
|
+
superDeclaration.importDeclarations.forEach(m => m.node.replace());
|
|
193
|
+
superDeclaration.inheritsExpression.node.replace();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return print(ast).code;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function findSuperDeclarations(cls, nodes) {
|
|
201
|
+
|
|
202
|
+
const importsMatches = matcher`
|
|
203
|
+
import $1 from 'inherits-browser';
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
const importDeclarations = importsMatches(nodes);
|
|
207
|
+
|
|
208
|
+
for (const [ _, identifier ] of importDeclarations) {
|
|
209
|
+
|
|
210
|
+
const inheritsMatches = matcher`
|
|
211
|
+
${identifier.value.name}(${cls.name}, $1);
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
const inheritsExpressions = inheritsMatches(nodes);
|
|
215
|
+
|
|
216
|
+
if (inheritsExpressions.length > 1) {
|
|
217
|
+
throw new Error('more than one <inherits> declaration');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (inheritsExpressions.length === 1) {
|
|
221
|
+
|
|
222
|
+
const clazz = inheritsExpressions[0][1].value.name;
|
|
223
|
+
|
|
224
|
+
const inheritsExpression = {
|
|
225
|
+
node: inheritsExpressions[0][0]
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const superCallMatches = matcher`
|
|
229
|
+
${clazz}.call($$$);
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
const superCalls = superCallMatches(cls.ctor.get('body', 'body'));
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
importDeclarations: importDeclarations.map(m => ({ node: m[0] })),
|
|
236
|
+
superCalls: superCalls.map(m => ({ node: m[0] })),
|
|
237
|
+
clazz,
|
|
238
|
+
inheritsExpression
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function findConstructors(nodes) {
|
|
246
|
+
|
|
247
|
+
const fns = matcher`
|
|
248
|
+
function $1($$$) {
|
|
249
|
+
$$$
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
|
|
253
|
+
const namedExportFns = matcher`
|
|
254
|
+
export function $1($$$) {
|
|
255
|
+
$$$
|
|
256
|
+
}
|
|
257
|
+
`;
|
|
258
|
+
|
|
259
|
+
const defaultExportFns = matcher`
|
|
260
|
+
export default function $1($$$) {
|
|
261
|
+
$$$
|
|
262
|
+
}
|
|
263
|
+
`;
|
|
264
|
+
|
|
265
|
+
return [
|
|
266
|
+
...fns(nodes),
|
|
267
|
+
...namedExportFns(nodes).map(
|
|
268
|
+
m => [ m[0].get('declaration'), m[1], m[0] ]
|
|
269
|
+
),
|
|
270
|
+
...defaultExportFns(nodes).map(
|
|
271
|
+
m => [ m[0].get('declaration'), m[1], m[0] ]
|
|
272
|
+
)
|
|
273
|
+
].map(match => {
|
|
274
|
+
|
|
275
|
+
const [ ctor, identifier, node = ctor ] = match;
|
|
276
|
+
|
|
277
|
+
const name = identifier.value.name;
|
|
278
|
+
|
|
279
|
+
if (/^[A-Z]/.test(name)) {
|
|
280
|
+
return {
|
|
281
|
+
name,
|
|
282
|
+
ctor,
|
|
283
|
+
node
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}).filter(n => n);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function findStaticMembers(cls, nodes) {
|
|
290
|
+
|
|
291
|
+
const names = matcher`
|
|
292
|
+
${cls.name}.$1 = $2;
|
|
293
|
+
`;
|
|
294
|
+
|
|
295
|
+
return names(nodes).map(match => {
|
|
296
|
+
const [ node, identifier, member ] = match;
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
name: identifier.value.name,
|
|
300
|
+
member: member.value,
|
|
301
|
+
comments: node.get('comments').value,
|
|
302
|
+
node
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function findProtoMethods(cls, nodes) {
|
|
308
|
+
|
|
309
|
+
const names = matcher`
|
|
310
|
+
${cls.name}.prototype.$1 = $2;
|
|
311
|
+
`;
|
|
312
|
+
|
|
313
|
+
return names(nodes).map(match => {
|
|
314
|
+
const [ node, identifier, member ] = match;
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
name: identifier.value.name,
|
|
318
|
+
member: member.value,
|
|
319
|
+
comments: node.get('comments').value,
|
|
320
|
+
node
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function findEmbeddedMethods(cls) {
|
|
326
|
+
|
|
327
|
+
const memberDeclarations = matcher`
|
|
328
|
+
this.$1 = $2;
|
|
329
|
+
`;
|
|
330
|
+
|
|
331
|
+
return memberDeclarations(cls.ctor.get('body', 'body')).map(match => {
|
|
332
|
+
const [ node, identifier, member ] = match;
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
name: identifier.value.name,
|
|
336
|
+
member: member.value,
|
|
337
|
+
comments: node.get('comments').value,
|
|
338
|
+
node
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
}
|
package/lib/util.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { parse as recastParse } from 'recast';
|
|
2
|
+
import { Path as PathConstructor } from 'ast-types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef { {
|
|
6
|
+
* get(...any) => any,
|
|
7
|
+
* value: any
|
|
8
|
+
* } } Path
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as typescriptParser from './parsers/typescript.js';
|
|
12
|
+
import * as typescriptDtsParser from './parsers/typescript-dts.js';
|
|
13
|
+
|
|
14
|
+
const DBG = /match/.test(process?.env?.LOG_DEBUG);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Return numeric or string keys of the given object.
|
|
18
|
+
*
|
|
19
|
+
* @param {any} obj
|
|
20
|
+
* @return {(string|number)[]}
|
|
21
|
+
*/
|
|
22
|
+
export function keys(obj) {
|
|
23
|
+
return Object.keys(obj).map(k => {
|
|
24
|
+
const n = parseInt(k, 10);
|
|
25
|
+
|
|
26
|
+
return isNaN(n) ? k : n;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {any} arr
|
|
32
|
+
*
|
|
33
|
+
* @return {boolean}
|
|
34
|
+
*/
|
|
35
|
+
export function isArray(arr) {
|
|
36
|
+
return Array.isArray(arr);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isNode(obj) {
|
|
40
|
+
return Object.prototype.toString.call(obj) === '[object Object]';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function hasProperty(obj, property) {
|
|
44
|
+
return Object.prototype.hasOwnProperty.call(obj, property);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} code
|
|
49
|
+
*
|
|
50
|
+
* @return {any}
|
|
51
|
+
*/
|
|
52
|
+
export function parse(code) {
|
|
53
|
+
return recastParse(code, {
|
|
54
|
+
parser: typescriptParser
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseDts(code) {
|
|
59
|
+
return recastParse(code, {
|
|
60
|
+
parser: typescriptDtsParser
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {any} node
|
|
66
|
+
*
|
|
67
|
+
* @return {Path}
|
|
68
|
+
*/
|
|
69
|
+
export function path(node) {
|
|
70
|
+
return new PathConstructor(node);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string[]} strings
|
|
75
|
+
* @param {...any} args
|
|
76
|
+
*
|
|
77
|
+
* @return { (node) => [][] }
|
|
78
|
+
*/
|
|
79
|
+
export function matcher(strings, ...args) {
|
|
80
|
+
|
|
81
|
+
const code = strings.reduce((code, str, i) => {
|
|
82
|
+
|
|
83
|
+
code += str;
|
|
84
|
+
|
|
85
|
+
if (args[i]) {
|
|
86
|
+
code += args[i];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return code;
|
|
90
|
+
}, '');
|
|
91
|
+
|
|
92
|
+
const ast = parse(code, {
|
|
93
|
+
parser: typescriptParser
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const body = path(ast).get('program', 'body');
|
|
97
|
+
|
|
98
|
+
if (body.value.length !== 1) {
|
|
99
|
+
throw new Error(`${ code } is more than one statement!`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const expr = body.get(0);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Match for wildcards:
|
|
106
|
+
*
|
|
107
|
+
* - block statement with a single $$$
|
|
108
|
+
* - expression statement with $$$
|
|
109
|
+
* - identifier $$$
|
|
110
|
+
* - wrapped as list
|
|
111
|
+
*
|
|
112
|
+
* @param {any} node
|
|
113
|
+
*
|
|
114
|
+
* @return {boolean}
|
|
115
|
+
*/
|
|
116
|
+
function wildcard(node) {
|
|
117
|
+
|
|
118
|
+
if (isArray(node) && node.length === 1) {
|
|
119
|
+
node = node[0];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (node && node.type === 'ExpressionStatement') {
|
|
123
|
+
node = node.expression;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (node && node.type === 'Identifier' && node.name === '$$$') {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Matches ${IDENTIFIER} single node.
|
|
134
|
+
*
|
|
135
|
+
* @param {any} node
|
|
136
|
+
*
|
|
137
|
+
* @return {any|null}
|
|
138
|
+
*/
|
|
139
|
+
function ident(node) {
|
|
140
|
+
if (node.type === 'Identifier' && /^\$.+/.test(node.name)) {
|
|
141
|
+
return node.name.substring(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Matches $${IDENTIFIER} many nodes:
|
|
149
|
+
*
|
|
150
|
+
* - block statement with a single $${IDENT}
|
|
151
|
+
* - expression statement with $${IDENT}
|
|
152
|
+
* - identifier $${IDENT}
|
|
153
|
+
* - wrapped as list
|
|
154
|
+
*
|
|
155
|
+
* @param {any} node
|
|
156
|
+
*
|
|
157
|
+
* @return {any|null}
|
|
158
|
+
*/
|
|
159
|
+
function anyIdent(node) {
|
|
160
|
+
|
|
161
|
+
if (isArray(node) && node.length === 1) {
|
|
162
|
+
node = node[0];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (node && node.type === 'ExpressionStatement') {
|
|
166
|
+
node = node.expression;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (node && node.type === 'Identifier' && /^\$\$.+/.test(node.name)) {
|
|
170
|
+
return node.name.substring(2);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {Path} node
|
|
178
|
+
* @param {Path} expectedNode
|
|
179
|
+
* @param {Path[]} results
|
|
180
|
+
*
|
|
181
|
+
* @return {Path[] || null}
|
|
182
|
+
*/
|
|
183
|
+
function matchNode(node, expectedNode, results = []) {
|
|
184
|
+
|
|
185
|
+
if (!node.value) {
|
|
186
|
+
DBG && console.log('BAIL no value');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const id = ident(expectedNode.value);
|
|
191
|
+
|
|
192
|
+
if (id) {
|
|
193
|
+
results[id] = node;
|
|
194
|
+
|
|
195
|
+
return results;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const key of keys(expectedNode.value)) {
|
|
199
|
+
if ([ 'loc', 'start', 'end' ].includes(key)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!hasProperty(node.value, key)) {
|
|
204
|
+
DBG && console.log('BAIL no property', key);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const expectedVal = expectedNode.get(key);
|
|
209
|
+
const nodeVal = node.get(key);
|
|
210
|
+
|
|
211
|
+
if (wildcard(expectedVal.value)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const id = anyIdent(expectedVal.value);
|
|
216
|
+
|
|
217
|
+
if (id) {
|
|
218
|
+
results[id] = nodeVal;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (isArray(expectedVal.value)) {
|
|
223
|
+
if (
|
|
224
|
+
expectedVal.value.some((el, idx) => {
|
|
225
|
+
return !matchNode(nodeVal.get(idx), expectedVal.get(idx), results);
|
|
226
|
+
})
|
|
227
|
+
) {
|
|
228
|
+
DBG && console.log('BAIL no array match', key);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
} else
|
|
232
|
+
if (isNode(expectedVal.value)) {
|
|
233
|
+
if (!matchNode(nodeVal, expectedVal, results)) {
|
|
234
|
+
DBG && console.log('BAIL no match', key);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
if (nodeVal.value !== expectedVal.value) {
|
|
239
|
+
DBG && console.log('BAIL no primitive match', key);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return function match(nodes) {
|
|
249
|
+
const matches = [];
|
|
250
|
+
|
|
251
|
+
for (const key of keys(nodes.value)) {
|
|
252
|
+
const node = nodes.get(key);
|
|
253
|
+
const matched = matchNode(node, expr);
|
|
254
|
+
|
|
255
|
+
if (matched) {
|
|
256
|
+
matches.push([ node, ...matched.slice(1) ]);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return matches;
|
|
261
|
+
};
|
|
262
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bio-dts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate sane and clean types from JavaScript sources",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": "bin/bio-dts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"all": "npm run lint && npm run test && npm run test:dts",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"test": "mocha test",
|
|
12
|
+
"test:dts": "node bin/bio-dts.js --outDir dist -r test/fixtures"
|
|
13
|
+
},
|
|
14
|
+
"author": "Nico Rehwaldt",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@babel/parser": "^7.21.2",
|
|
18
|
+
"recast": "^0.22.0",
|
|
19
|
+
"tiny-glob": "^0.2.9",
|
|
20
|
+
"typescript": "^4.9.5"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"chai": "^4.3.7",
|
|
24
|
+
"eslint": "^8.36.0",
|
|
25
|
+
"eslint-plugin-bpmn-io": "^1.0.0",
|
|
26
|
+
"mocha": "^10.2.0"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"lib",
|
|
31
|
+
"index.js"
|
|
32
|
+
]
|
|
33
|
+
}
|