@metamask-previews/platform-api-docs 0.0.0-preview-1275d0fda → 0.0.0-preview-e15374a5d
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/dist/cli.cjs +7 -4
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +7 -4
- package/dist/cli.mjs.map +1 -1
- package/dist/extraction.cjs +422 -413
- package/dist/extraction.cjs.map +1 -1
- package/dist/extraction.d.cts +7 -20
- package/dist/extraction.d.cts.map +1 -1
- package/dist/extraction.d.mts +7 -20
- package/dist/extraction.d.mts.map +1 -1
- package/dist/extraction.mjs +421 -411
- package/dist/extraction.mjs.map +1 -1
- package/dist/generate.cjs.map +1 -1
- package/dist/generate.mjs.map +1 -1
- package/dist/markdown.cjs.map +1 -1
- package/dist/markdown.d.cts +2 -2
- package/dist/markdown.d.cts.map +1 -1
- package/dist/markdown.d.mts +2 -2
- package/dist/markdown.d.mts.map +1 -1
- package/dist/markdown.mjs.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +5 -21
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.mts +5 -21
- package/dist/types.d.mts.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +2 -2
- package/site/tsconfig.json +2 -4
package/dist/extraction.cjs
CHANGED
|
@@ -23,16 +23,22 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.
|
|
26
|
+
exports.extractFromSourceFile = exports.createExtractionProject = void 0;
|
|
27
27
|
const path = __importStar(require("node:path"));
|
|
28
28
|
const ts_morph_1 = require("ts-morph");
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
|
+
// NOTE: `ts-morph` is used heavily in this file to parse and extract
|
|
31
|
+
// information from TypeScript files. Although this library is not well
|
|
32
|
+
// documented, it wraps the TypeScript AST fairly well, and you can get a good
|
|
33
|
+
// sense of the AST by using this website: <https://ts-ast-viewer.com>
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
30
36
|
// JSDoc utilities
|
|
31
37
|
// ---------------------------------------------------------------------------
|
|
32
38
|
/**
|
|
33
|
-
* Convert `{@link X}` references inside a string to plain
|
|
34
|
-
* and escape any remaining (out-of-backtick) curly braces
|
|
35
|
-
* safe to
|
|
39
|
+
* Convert `{@link X}` references inside a JSDoc comment string to plain
|
|
40
|
+
* backtick code spans and escape any remaining (out-of-backtick) curly braces.
|
|
41
|
+
* This way the output is safe to render in a MDX document.
|
|
36
42
|
*
|
|
37
43
|
* @param text - The raw text to normalize.
|
|
38
44
|
* @returns The text with `@link` resolved and stray braces escaped.
|
|
@@ -50,32 +56,30 @@ function escapeJsDocTextForMdx(text) {
|
|
|
50
56
|
});
|
|
51
57
|
}
|
|
52
58
|
/**
|
|
53
|
-
* Extract a JSDoc tag
|
|
54
|
-
*
|
|
55
|
-
*
|
|
59
|
+
* Extract the comment text of a JSDoc tag — the part that comes after the tag
|
|
60
|
+
* and any identifier, e.g. "Some param" in "@param foo Some param" —
|
|
61
|
+
* normalizing whitespace to a single space so we can better control how it's
|
|
62
|
+
* rendered within the site.
|
|
56
63
|
*
|
|
57
64
|
* @param tag - The JSDoc tag.
|
|
58
65
|
* @returns The flattened comment text.
|
|
59
66
|
*/
|
|
60
67
|
function extractJsDocTagComment(tag) {
|
|
61
|
-
// istanbul ignore next: ts-morph returns null for tags without comment text,
|
|
62
|
-
// but every fixture tag we extract from carries a comment.
|
|
63
68
|
return (tag.getCommentText() ?? '').replace(/\s+/gu, ' ').trim();
|
|
64
69
|
}
|
|
65
70
|
/**
|
|
66
71
|
* Strip the conventional `- ` separator from the start of a `@param` tag's
|
|
67
|
-
* comment.
|
|
68
|
-
* back `- description` for the comment. The hyphen is purely cosmetic; we'd
|
|
69
|
-
* rather render the description without it.
|
|
72
|
+
* comment.
|
|
70
73
|
*
|
|
71
74
|
* @param comment - The flattened comment text from a `@param` tag.
|
|
72
75
|
* @returns The comment with any leading `- ` (or `– `, `— `) removed.
|
|
73
76
|
*/
|
|
74
|
-
function
|
|
77
|
+
function stripJsDocParamSeparator(comment) {
|
|
75
78
|
return comment.replace(/^[-–—]\s*/u, '');
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
78
|
-
*
|
|
81
|
+
* Extract JSDoc from a TypeScript AST node and decompose it into the parts we
|
|
82
|
+
* need to render docs:
|
|
79
83
|
*
|
|
80
84
|
* - `description` — the body above the first tag, with `@deprecated` comments
|
|
81
85
|
* appended as `**Deprecated:** <comment>` lines and normalized for MDX
|
|
@@ -85,7 +89,7 @@ function stripParamSeparator(comment) {
|
|
|
85
89
|
*
|
|
86
90
|
* Other tags (`@see`, `@throws`, `@template`, `@example`) are dropped.
|
|
87
91
|
*
|
|
88
|
-
* @param node - The AST node to extract JSDoc from.
|
|
92
|
+
* @param node - The AST node to extract JSDoc from (e.g. a type or a method).
|
|
89
93
|
* @returns The decomposed JSDoc; empty strings/arrays when the node has no JSDoc.
|
|
90
94
|
*/
|
|
91
95
|
function extractJsDoc(node) {
|
|
@@ -102,23 +106,23 @@ function extractJsDoc(node) {
|
|
|
102
106
|
const tagName = tag.getTagName();
|
|
103
107
|
if (tagName === 'deprecated') {
|
|
104
108
|
const comment = extractJsDocTagComment(tag);
|
|
105
|
-
// istanbul ignore next: bare `@deprecated` (without explanatory text)
|
|
106
|
-
// doesn't appear in messenger JSDoc in practice.
|
|
107
109
|
deprecatedLines.push(comment ? `**Deprecated:** ${comment}` : '**Deprecated:**');
|
|
108
110
|
}
|
|
109
111
|
else if (tagName === 'param' && ts_morph_1.Node.isJSDocParameterTag(tag)) {
|
|
110
112
|
const nameNode = tag.getNameNode();
|
|
111
|
-
|
|
112
|
-
if (!
|
|
113
|
+
const paramName = nameNode.getText();
|
|
114
|
+
if (!paramName) {
|
|
113
115
|
continue;
|
|
114
116
|
}
|
|
117
|
+
const comment = extractJsDocTagComment(tag);
|
|
115
118
|
params.push({
|
|
116
|
-
name:
|
|
117
|
-
description: escapeJsDocTextForMdx(
|
|
119
|
+
name: paramName,
|
|
120
|
+
description: escapeJsDocTextForMdx(stripJsDocParamSeparator(comment)),
|
|
118
121
|
});
|
|
119
122
|
}
|
|
120
123
|
else if (tagName === 'returns' || tagName === 'return') {
|
|
121
|
-
|
|
124
|
+
const comment = extractJsDocTagComment(tag);
|
|
125
|
+
returns = escapeJsDocTextForMdx(comment);
|
|
122
126
|
}
|
|
123
127
|
}
|
|
124
128
|
const description = escapeJsDocTextForMdx([descriptionBody, ...deprecatedLines]
|
|
@@ -132,7 +136,7 @@ function extractJsDoc(node) {
|
|
|
132
136
|
* @param node - The AST node to check.
|
|
133
137
|
* @returns True if the node has an `@deprecated` tag.
|
|
134
138
|
*/
|
|
135
|
-
function
|
|
139
|
+
function hasDeprecatedJsDocTag(node) {
|
|
136
140
|
return node
|
|
137
141
|
.getJsDocs()
|
|
138
142
|
.flatMap((jsDoc) => jsDoc.getTags())
|
|
@@ -142,101 +146,63 @@ function isDeprecated(node) {
|
|
|
142
146
|
// Type-resolution helpers (powered by ts-morph's type checker)
|
|
143
147
|
// ---------------------------------------------------------------------------
|
|
144
148
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* literal type. Returns null if the type checker can't reduce it to a single
|
|
148
|
-
* string literal.
|
|
149
|
+
* Locates the type that represents a method on a class (e.g.
|
|
150
|
+
* `Class['method']`), which itself comes from a messenger action handler.
|
|
149
151
|
*
|
|
150
|
-
* @param
|
|
151
|
-
* @returns The
|
|
152
|
+
* @param typeNode - The node that represents the indexed access.
|
|
153
|
+
* @returns The found method declaration, or null.
|
|
152
154
|
*/
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
return type.getLiteralValueOrThrow();
|
|
157
|
-
}
|
|
158
|
-
// istanbul ignore next: messenger fixtures always reduce template literal
|
|
159
|
-
// types to a single string literal via `typeof X` constants.
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Resolve a capability-type-constructor's first generic argument to a
|
|
164
|
-
* namespace string. Accepts either `typeof X` (resolved via the type checker)
|
|
165
|
-
* or a string literal.
|
|
166
|
-
*
|
|
167
|
-
* @param typeArg - The first generic argument node.
|
|
168
|
-
* @returns The resolved namespace, or null.
|
|
169
|
-
*/
|
|
170
|
-
function resolveNamespaceFromTypeArg(typeArg) {
|
|
171
|
-
if (ts_morph_1.Node.isTypeQuery(typeArg)) {
|
|
172
|
-
const type = typeArg.getType();
|
|
173
|
-
if (type.isStringLiteral()) {
|
|
174
|
-
return type.getLiteralValueOrThrow();
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (ts_morph_1.Node.isLiteralTypeNode(typeArg)) {
|
|
178
|
-
const literal = typeArg.getLiteral();
|
|
179
|
-
// istanbul ignore else: only string literals are valid namespace args.
|
|
180
|
-
if (ts_morph_1.Node.isStringLiteral(literal)) {
|
|
181
|
-
return literal.getLiteralValue();
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
// istanbul ignore next: namespace args are always a `typeof X` query or a
|
|
185
|
-
// string literal in valid messenger usage.
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* If `typeNode` is `Class['method']`, resolve `Class` to its declaration
|
|
190
|
-
* (across files if needed via the type checker) and look up the method by
|
|
191
|
-
* name. Returns the method declaration when found.
|
|
192
|
-
*
|
|
193
|
-
* @param typeNode - The handler's type node.
|
|
194
|
-
* @returns The method declaration, or null.
|
|
195
|
-
*/
|
|
196
|
-
function resolveIndexedAccessMethod(typeNode) {
|
|
197
|
-
if (!typeNode || !ts_morph_1.Node.isIndexedAccessTypeNode(typeNode)) {
|
|
155
|
+
function findClassMethodDeclaration(typeNode) {
|
|
156
|
+
// Fundamental check: if we don't have `Class['method']`, we can't do anything.
|
|
157
|
+
if (!ts_morph_1.Node.isIndexedAccessTypeNode(typeNode)) {
|
|
198
158
|
return null;
|
|
199
159
|
}
|
|
160
|
+
// The type that represents the class being accessed.
|
|
161
|
+
// EXAMPLE:
|
|
162
|
+
// FooController['someMethod']
|
|
163
|
+
// ^^^^^^^^^^^^^
|
|
200
164
|
const objectType = typeNode.getObjectTypeNode();
|
|
165
|
+
// The type that represents the property being accessed.
|
|
166
|
+
// EXAMPLE:
|
|
167
|
+
// FooController['someMethod']
|
|
168
|
+
// ^^^^^^^^^^^^
|
|
201
169
|
const indexType = typeNode.getIndexTypeNode();
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
// istanbul ignore next: the index in `Class['method']` is always a literal
|
|
208
|
-
// type node in valid handler syntax.
|
|
209
|
-
if (!ts_morph_1.Node.isLiteralTypeNode(indexType)) {
|
|
170
|
+
// To access a property on a type, it must be a type we can access properties of.
|
|
171
|
+
if (!ts_morph_1.Node.isTypeReference(objectType) ||
|
|
172
|
+
!ts_morph_1.Node.isLiteralTypeNode(indexType)) {
|
|
210
173
|
return null;
|
|
211
174
|
}
|
|
212
175
|
const indexLiteral = indexType.getLiteral();
|
|
213
|
-
//
|
|
214
|
-
// The first two land as `StringLiteral`; the bare template literal is a
|
|
215
|
-
// `NoSubstitutionTemplateLiteral`.
|
|
216
|
-
// istanbul ignore next: numeric/boolean indices aren't valid method names.
|
|
176
|
+
// Names of methods must be static strings; they cannot be template strings.
|
|
217
177
|
if (!ts_morph_1.Node.isStringLiteral(indexLiteral) &&
|
|
218
178
|
!ts_morph_1.Node.isNoSubstitutionTemplateLiteral(indexLiteral)) {
|
|
219
179
|
return null;
|
|
220
180
|
}
|
|
221
181
|
const methodName = indexLiteral.getLiteralValue();
|
|
182
|
+
// Reject qualified-name type names, as we need a plain identifier to
|
|
183
|
+
// resolve the symbol.
|
|
184
|
+
// EXAMPLE:
|
|
185
|
+
// import * as somePackage from '...';
|
|
186
|
+
// somePackage.FooController['someMethod']
|
|
187
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
222
188
|
const classNameNode = objectType.getTypeName();
|
|
223
|
-
// istanbul ignore next: qualified-name class references aren't used in
|
|
224
|
-
// messenger handler types.
|
|
225
189
|
if (!ts_morph_1.Node.isIdentifier(classNameNode)) {
|
|
226
190
|
return null;
|
|
227
191
|
}
|
|
192
|
+
// Since we know we have a type reference, we can assume that we have a symbol.
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
228
194
|
const localSymbol = classNameNode.getSymbol();
|
|
229
|
-
//
|
|
230
|
-
// in
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
//
|
|
235
|
-
//
|
|
195
|
+
// If we have a type imported from another file, ensure that when we access
|
|
196
|
+
// the declaration, it's the type declaration in the other file, not the
|
|
197
|
+
// import declaration in this file.
|
|
198
|
+
// EXAMPLE:
|
|
199
|
+
// import { FooController } from '@metamask/foo-controller';
|
|
200
|
+
// FooController['someMethod']
|
|
201
|
+
// ^^^^^^^^^^^^^
|
|
236
202
|
const symbol = localSymbol.getAliasedSymbol() ?? localSymbol;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
203
|
+
for (const declaration of symbol.getDeclarations()) {
|
|
204
|
+
// We must have a class to treat the property on the object type as a
|
|
205
|
+
// method.
|
|
240
206
|
if (ts_morph_1.Node.isClassDeclaration(declaration)) {
|
|
241
207
|
const method = declaration.getMethod(methodName);
|
|
242
208
|
if (method) {
|
|
@@ -244,416 +210,490 @@ function resolveIndexedAccessMethod(typeNode) {
|
|
|
244
210
|
}
|
|
245
211
|
}
|
|
246
212
|
}
|
|
247
|
-
// istanbul ignore next: only reached if the indexed method isn't declared
|
|
248
|
-
// on any of the resolved class declarations, which doesn't happen in valid
|
|
249
|
-
// handler types.
|
|
250
213
|
return null;
|
|
251
214
|
}
|
|
252
215
|
// ---------------------------------------------------------------------------
|
|
253
216
|
// Method info
|
|
254
217
|
// ---------------------------------------------------------------------------
|
|
255
218
|
/**
|
|
256
|
-
* Build
|
|
257
|
-
*
|
|
258
|
-
*
|
|
219
|
+
* Build the textual signature of a class method — its parameter list and
|
|
220
|
+
* return type expressed as a TypeScript function type — so a handler that
|
|
221
|
+
* references `Class['method']` can be rendered as `(arg: T) => R` instead of
|
|
222
|
+
* the bare indexed-access syntax.
|
|
259
223
|
*
|
|
260
224
|
* @param method - The method declaration.
|
|
261
|
-
* @returns The
|
|
225
|
+
* @returns The signature, e.g. `(id: number) => Promise<string>`.
|
|
262
226
|
*/
|
|
263
|
-
function
|
|
227
|
+
function buildMethodSignature(method) {
|
|
264
228
|
const signatureParams = method
|
|
265
229
|
.getParameters()
|
|
266
230
|
.map((param) => {
|
|
231
|
+
const rest = param.isRestParameter() ? '...' : '';
|
|
267
232
|
const paramName = param.getNameNode().getText();
|
|
268
233
|
const optional = param.hasQuestionToken() ? '?' : '';
|
|
269
234
|
const typeNode = param.getTypeNode();
|
|
270
|
-
// istanbul ignore next: handler parameters in messenger fixtures always
|
|
271
|
-
// declare an explicit type.
|
|
272
235
|
const paramType = typeNode ? typeNode.getText() : 'unknown';
|
|
273
|
-
return `${paramName}${optional}: ${paramType}`;
|
|
236
|
+
return `${rest}${paramName}${optional}: ${paramType}`;
|
|
274
237
|
})
|
|
275
238
|
.join(', ');
|
|
276
239
|
const returnTypeNode = method.getReturnTypeNode();
|
|
277
|
-
// istanbul ignore next: handler class methods in messenger fixtures always
|
|
278
|
-
// declare an explicit return type.
|
|
279
240
|
const returnType = returnTypeNode ? returnTypeNode.getText() : 'void';
|
|
280
241
|
// For async methods, the declared return type already includes `Promise<>`,
|
|
281
242
|
// so we don't need to wrap again.
|
|
282
|
-
|
|
283
|
-
const { description, params, returns } = extractJsDoc(method);
|
|
284
|
-
return { jsDoc: description, params, returns, signature };
|
|
243
|
+
return `(${signatureParams}) => ${returnType}`;
|
|
285
244
|
}
|
|
286
245
|
/**
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* — paired with the kind under which they were referenced.
|
|
291
|
-
*
|
|
292
|
-
* The walker follows imported references via ts-morph's symbol resolution,
|
|
293
|
-
* so capability types declared in sibling files (e.g. the auto-generated
|
|
294
|
-
* `*-method-action-types.ts` files) are discovered even though only the
|
|
295
|
-
* `*Messenger` declaration is local to this file.
|
|
246
|
+
* Looks for Messenger types in the source file (that is, those that are type
|
|
247
|
+
* aliases whose names end with "Messenger"), then extracts the `Actions` and
|
|
248
|
+
* `Events` parameters from these types.
|
|
296
249
|
*
|
|
297
250
|
* @param sourceFile - The TypeScript source file to scan.
|
|
298
|
-
* @returns
|
|
251
|
+
* @returns A list of objects that represent messenger types.
|
|
299
252
|
*/
|
|
300
|
-
function
|
|
301
|
-
const
|
|
302
|
-
const seen = new Set();
|
|
253
|
+
function findMessengerTypeAliases(sourceFile) {
|
|
254
|
+
const parsedMessengerTypeAliases = [];
|
|
303
255
|
for (const typeAlias of sourceFile.getTypeAliases()) {
|
|
304
256
|
if (!typeAlias.getName().endsWith('Messenger')) {
|
|
305
257
|
continue;
|
|
306
258
|
}
|
|
307
259
|
const body = typeAlias.getTypeNode();
|
|
260
|
+
// Basic check
|
|
308
261
|
if (!body || !ts_morph_1.Node.isTypeReference(body)) {
|
|
309
262
|
continue;
|
|
310
263
|
}
|
|
311
264
|
const typeArgs = body.getTypeArguments();
|
|
265
|
+
// Messenger types always have 3 type parameters
|
|
266
|
+
// (e.g. `Messenger<'FooController', Actions, Events>`)
|
|
312
267
|
if (typeArgs.length < 3) {
|
|
313
268
|
continue;
|
|
314
269
|
}
|
|
315
|
-
|
|
316
|
-
|
|
270
|
+
parsedMessengerTypeAliases.push({
|
|
271
|
+
actionsTypeParameter: typeArgs[1],
|
|
272
|
+
eventsTypeParameter: typeArgs[2],
|
|
273
|
+
});
|
|
317
274
|
}
|
|
318
|
-
return
|
|
275
|
+
return parsedMessengerTypeAliases;
|
|
319
276
|
}
|
|
320
277
|
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* names are followed across files); a resolved alias whose body is itself a
|
|
325
|
-
* union expands transparently, leaving the intermediate alias unrecorded.
|
|
326
|
-
*
|
|
327
|
-
* A resolved alias whose body is a single non-union type reference (e.g.
|
|
328
|
-
* `type AllowedActions = ConnectivityControllerGetStateAction`) is treated
|
|
329
|
-
* as an opaque re-export — the walk stops there, leaving the target to be
|
|
330
|
-
* documented from its home package by the dedup logic later.
|
|
331
|
-
*
|
|
332
|
-
* For example, given:
|
|
278
|
+
* Walks the `Actions` and `Events` type parameters of the given messenger
|
|
279
|
+
* types, extracted in a previous step, to find all type declarations (i.e.,
|
|
280
|
+
* statements) that represent individual messenger actions or events.
|
|
333
281
|
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
*
|
|
282
|
+
* @param parsedMessengerTypeAliases - The list of objects representing
|
|
283
|
+
* messenger types, parsed in a previous step.
|
|
284
|
+
* @returns The list of type aliases that represent messenger capabilities among
|
|
285
|
+
* the given messenger types.
|
|
286
|
+
*/
|
|
287
|
+
function findAllMessengerCapabilityTypeDeclarations(parsedMessengerTypeAliases) {
|
|
288
|
+
const allCapabilityTypeDeclarations = [];
|
|
289
|
+
let allVisitedTypeDeclarations = new Set();
|
|
290
|
+
for (const { actionsTypeParameter, eventsTypeParameter, } of parsedMessengerTypeAliases) {
|
|
291
|
+
for (const [typeParameter, kind] of [
|
|
292
|
+
[actionsTypeParameter, 'action'],
|
|
293
|
+
[eventsTypeParameter, 'event'],
|
|
294
|
+
]) {
|
|
295
|
+
const result = recursivelyFindMessengerCapabilityTypeDeclarations(typeParameter, kind, allVisitedTypeDeclarations);
|
|
296
|
+
allCapabilityTypeDeclarations.push(...result.capabilityTypeDeclarations);
|
|
297
|
+
allVisitedTypeDeclarations = result.visitedTypeDeclarations;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return allCapabilityTypeDeclarations;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Recursively walks a `ts-morph` AST node — at first the `Actions` or `Events`
|
|
304
|
+
* type parameter of a messenger type, and then a node within that parameter —
|
|
305
|
+
* to find all type aliases that represent individual messenger actions or
|
|
306
|
+
* events, no matter how deeply the type aliases exist in the tree or in which
|
|
307
|
+
* file they are located.
|
|
356
308
|
*
|
|
357
|
-
* @param node - The
|
|
358
|
-
* @param kind - Whether to tag found
|
|
359
|
-
* @param
|
|
360
|
-
*
|
|
361
|
-
*
|
|
309
|
+
* @param node - The `ts-morph` AST node to walk.
|
|
310
|
+
* @param kind - Whether to tag found type aliases as 'action' or 'event'.
|
|
311
|
+
* @param visitedTypeDeclarations - A variable that tracks visited type aliases
|
|
312
|
+
* and prevents duplicates.
|
|
313
|
+
* @returns The list of extracted messenger capability type aliases as well as
|
|
314
|
+
* an updated version of `visitedTypeDeclarations`.
|
|
362
315
|
*/
|
|
363
|
-
function
|
|
316
|
+
function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedTypeDeclarations) {
|
|
317
|
+
const result = {
|
|
318
|
+
capabilityTypeDeclarations: [],
|
|
319
|
+
visitedTypeDeclarations: new Set([...visitedTypeDeclarations]),
|
|
320
|
+
};
|
|
321
|
+
// If `node` is a union type, walk each type within it.
|
|
322
|
+
// EXAMPLES:
|
|
323
|
+
// type Actions = FooControllerSomeAction | FooControllerSomeOtherAction
|
|
324
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
325
|
+
// type FooControllerActions = FooControllerSomeAction | FooControllerSomeOtherAction
|
|
326
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
364
327
|
if (ts_morph_1.Node.isUnionTypeNode(node)) {
|
|
365
|
-
for (const
|
|
366
|
-
|
|
328
|
+
for (const typeNode of node.getTypeNodes()) {
|
|
329
|
+
const innerResult = recursivelyFindMessengerCapabilityTypeDeclarations(typeNode, kind, result.visitedTypeDeclarations);
|
|
330
|
+
result.capabilityTypeDeclarations.push(...innerResult.capabilityTypeDeclarations);
|
|
331
|
+
for (const typeDeclaration of innerResult.visitedTypeDeclarations) {
|
|
332
|
+
result.visitedTypeDeclarations.add(typeDeclaration);
|
|
333
|
+
}
|
|
367
334
|
}
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
// If `node` is not a type reference, don't walk it.
|
|
338
|
+
// EXAMPLE:
|
|
339
|
+
// // Bad
|
|
340
|
+
// type Actions = { ... }
|
|
341
|
+
// ^^^^^^^
|
|
342
|
+
// // Good
|
|
343
|
+
// type Actions = FooControllerSomeAction;
|
|
344
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^
|
|
345
|
+
// // Good
|
|
346
|
+
// type Actions = ControllerGetStateAction<...>;
|
|
347
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
370
348
|
if (!ts_morph_1.Node.isTypeReference(node)) {
|
|
371
|
-
return;
|
|
349
|
+
return result;
|
|
372
350
|
}
|
|
373
351
|
const nameNode = node.getTypeName();
|
|
374
|
-
//
|
|
375
|
-
//
|
|
352
|
+
// Reject qualified-name type names, as we need a plain identifier to
|
|
353
|
+
// resolve the symbol.
|
|
354
|
+
// EXAMPLE:
|
|
355
|
+
// import * as somePackage from '...';
|
|
356
|
+
// type Actions = somePackage.FooControllerSomeAction;
|
|
357
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
376
358
|
if (!ts_morph_1.Node.isIdentifier(nameNode)) {
|
|
377
|
-
return;
|
|
359
|
+
return result;
|
|
378
360
|
}
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
// `getAliasedSymbol()` follows the alias to the original declaration in
|
|
382
|
-
// the imported file. Use it when present; otherwise fall back to the
|
|
383
|
-
// (already-local) symbol.
|
|
361
|
+
// Since we know we have a type reference, we can assume that we have a symbol.
|
|
362
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
384
363
|
const localSymbol = nameNode.getSymbol();
|
|
385
|
-
//
|
|
386
|
-
// in
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
364
|
+
// If we have a type imported from another file, ensure that when we access
|
|
365
|
+
// the declaration, it's the type declaration in the other file, not the
|
|
366
|
+
// import declaration in this file.
|
|
367
|
+
// EXAMPLE:
|
|
368
|
+
// import { FooControllerSomeAction } from '@metamask/foo-controller';
|
|
369
|
+
// type Actions = FooControllerSomeAction;
|
|
370
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^
|
|
390
371
|
const symbol = localSymbol.getAliasedSymbol() ?? localSymbol;
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
372
|
+
// At this point, we have a type *reference*, but we need to find the type
|
|
373
|
+
// *declaration*.
|
|
374
|
+
// For instance, if we have `FooControllerSomeAction`, we need to find
|
|
375
|
+
// the full `type FooControllerSomeAction = ...` statement.
|
|
376
|
+
for (const declaration of symbol.getDeclarations()) {
|
|
377
|
+
// Prevent duplicates
|
|
378
|
+
if (result.visitedTypeDeclarations.has(declaration)) {
|
|
395
379
|
continue;
|
|
396
380
|
}
|
|
397
|
-
|
|
381
|
+
result.visitedTypeDeclarations.add(declaration);
|
|
382
|
+
// If we have a type alias, then we have to handle a few scenarios.
|
|
383
|
+
// EXAMPLES:
|
|
384
|
+
// type FooControllerMethodActions = ...
|
|
385
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
386
|
+
// type FooControllerSomeAction = ...
|
|
387
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
398
388
|
if (ts_morph_1.Node.isTypeAliasDeclaration(declaration)) {
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
389
|
+
const body = declaration.getTypeNode();
|
|
390
|
+
// If the body is a union type or a plain type reference (not a utility
|
|
391
|
+
// type), walk it.
|
|
392
|
+
// EXAMPLES:
|
|
393
|
+
// type FooControllerMethodActions = FooControllerSomeAction | FooControllerSomeOtherAction
|
|
394
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
395
|
+
// type DelegationControllerMethodActions = DelegationControllerSignDelegationAction
|
|
396
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
397
|
+
if (body &&
|
|
398
|
+
(ts_morph_1.Node.isUnionTypeNode(body) ||
|
|
399
|
+
(ts_morph_1.Node.isTypeReference(body) &&
|
|
400
|
+
body.getTypeArguments().length === 0))) {
|
|
401
|
+
const innerResult = recursivelyFindMessengerCapabilityTypeDeclarations(body, kind, result.visitedTypeDeclarations);
|
|
402
|
+
result.capabilityTypeDeclarations.push(...innerResult.capabilityTypeDeclarations);
|
|
403
|
+
for (const typeDeclaration of innerResult.visitedTypeDeclarations) {
|
|
404
|
+
result.visitedTypeDeclarations.add(typeDeclaration);
|
|
405
|
+
}
|
|
403
406
|
continue;
|
|
404
407
|
}
|
|
405
|
-
// A
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
408
|
+
// A TypeReference body with type arguments is a capability-type-
|
|
409
|
+
// constructor invocation (e.g. `ControllerGetStateAction<typeof name,
|
|
410
|
+
// State>`). Tag it so the constructor extractor can read `body`
|
|
411
|
+
// directly without re-checking its shape.
|
|
412
|
+
// EXAMPLE:
|
|
413
|
+
// type FooControllerSomeAction = ControllerGetStateAction<...>
|
|
414
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
415
|
+
if (body && ts_morph_1.Node.isTypeReference(body)) {
|
|
416
|
+
// Reject qualified-name constructor type names, as we need a plain
|
|
417
|
+
// identifier to match the constructor by name.
|
|
418
|
+
// EXAMPLE:
|
|
419
|
+
// // Bad
|
|
420
|
+
// import * as somePackage from '...';
|
|
421
|
+
// type FooControllerSomeAction = somePackage.ControllerGetStateAction<...>
|
|
422
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
423
|
+
const constructorTypeName = body.getTypeName();
|
|
424
|
+
if (ts_morph_1.Node.isIdentifier(constructorTypeName)) {
|
|
425
|
+
result.capabilityTypeDeclarations.push({
|
|
426
|
+
bodyShape: 'constructor',
|
|
427
|
+
kind,
|
|
428
|
+
declaration,
|
|
429
|
+
body,
|
|
430
|
+
typeName: constructorTypeName,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
414
433
|
continue;
|
|
415
434
|
}
|
|
416
|
-
//
|
|
417
|
-
|
|
435
|
+
// Anything else (a type literal, intersection, conditional, …) gets
|
|
436
|
+
// tagged for the literal extractor, which knows how to read members
|
|
437
|
+
// off a type literal and rejects exotic shapes.
|
|
438
|
+
// EXAMPLE:
|
|
439
|
+
// type FooControllerSomeAction = { ... }
|
|
440
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
441
|
+
result.capabilityTypeDeclarations.push({
|
|
442
|
+
bodyShape: 'object',
|
|
443
|
+
kind,
|
|
444
|
+
declaration,
|
|
445
|
+
});
|
|
418
446
|
}
|
|
447
|
+
// Interfaces always carry their members directly — tag for the literal
|
|
448
|
+
// extractor.
|
|
449
|
+
// EXAMPLE:
|
|
450
|
+
// interface FooControllerSomeAction { ... }
|
|
451
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
419
452
|
else if (ts_morph_1.Node.isInterfaceDeclaration(declaration)) {
|
|
420
|
-
|
|
453
|
+
result.capabilityTypeDeclarations.push({
|
|
454
|
+
bodyShape: 'object',
|
|
455
|
+
kind,
|
|
456
|
+
declaration,
|
|
457
|
+
});
|
|
421
458
|
}
|
|
422
459
|
}
|
|
460
|
+
return result;
|
|
423
461
|
}
|
|
424
462
|
// ---------------------------------------------------------------------------
|
|
425
463
|
// Per-statement extraction
|
|
426
464
|
// ---------------------------------------------------------------------------
|
|
427
465
|
/**
|
|
428
|
-
*
|
|
429
|
-
*
|
|
466
|
+
* Given the declaration of a messenger capability type, extract information
|
|
467
|
+
* about it (action/event type string, handler/payload arguments and return
|
|
468
|
+
* type, etc.)
|
|
430
469
|
*
|
|
431
|
-
* @param
|
|
432
|
-
*
|
|
470
|
+
* @param capabilityTypeDeclaration - The statement that declared the type for a
|
|
471
|
+
* messenger action or event, extracted in a previous step.
|
|
433
472
|
* @param projectPath - Project root, used for computing relative source paths.
|
|
434
|
-
* @returns
|
|
473
|
+
* @returns Information that may be extracted from the messenger capability type
|
|
474
|
+
* (may be `null` if the type is ineligible for extraction).
|
|
435
475
|
*/
|
|
436
|
-
function
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
return inlineCapability;
|
|
440
|
-
}
|
|
441
|
-
if (ts_morph_1.Node.isTypeAliasDeclaration(statement)) {
|
|
442
|
-
return extractFromCapabilityTypeConstructor(statement, kind, projectPath);
|
|
476
|
+
function extractFromMessengerCapabilityTypeDeclaration(capabilityTypeDeclaration, projectPath) {
|
|
477
|
+
if (capabilityTypeDeclaration.bodyShape === 'constructor') {
|
|
478
|
+
return tryToExtractFromCapabilityTypeConstructor(capabilityTypeDeclaration, projectPath);
|
|
443
479
|
}
|
|
444
|
-
|
|
445
|
-
// therefore an inline shape, so the inline branch above always returns
|
|
446
|
-
// non-null here.
|
|
447
|
-
return null;
|
|
480
|
+
return tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaration, projectPath);
|
|
448
481
|
}
|
|
449
482
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
483
|
+
* If a messenger capability type is a type alias or interface and its body is
|
|
484
|
+
* a literal object type — i.e. one of:
|
|
485
|
+
*
|
|
486
|
+
* - `{ type: '...'; handler: ... }` (action)
|
|
487
|
+
* - `{ type: '...'; payload: ... }` (event)
|
|
488
|
+
*
|
|
489
|
+
* then this function extracts information about the type (action/event type
|
|
490
|
+
* string, handler/payload arguments and return type, etc.)
|
|
454
491
|
*
|
|
455
|
-
* @param
|
|
456
|
-
*
|
|
492
|
+
* @param capabilityTypeDeclaration - The statement that declared the type for a
|
|
493
|
+
* messenger action or event, extracted in a previous step.
|
|
457
494
|
* @param projectPath - Project root, used for computing relative source paths.
|
|
458
|
-
* @returns The extracted capability, or null if the shape
|
|
495
|
+
* @returns The extracted capability packet, or null if the shape of the type
|
|
496
|
+
* doesn't match.
|
|
459
497
|
*/
|
|
460
|
-
function
|
|
498
|
+
function tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaration, projectPath) {
|
|
499
|
+
const { declaration, kind } = capabilityTypeDeclaration;
|
|
500
|
+
// We must have a object type alias or an interface, and the body must not be empty.
|
|
501
|
+
// EXAMPLES:
|
|
502
|
+
// // Good
|
|
503
|
+
// type FooControllerSomeAction = {
|
|
504
|
+
// type: 'FooController:getState';
|
|
505
|
+
// handler: FooController['getState'];
|
|
506
|
+
// }
|
|
507
|
+
// // Good
|
|
508
|
+
// interface FooControllerSomeAction {
|
|
509
|
+
// type: 'FooController:getState';
|
|
510
|
+
// handler: FooController['getState'];
|
|
511
|
+
// }
|
|
512
|
+
// // Bad
|
|
513
|
+
// type FooControllerSomeAction = {};
|
|
514
|
+
// // Bad
|
|
515
|
+
// interface FooControllerSomeAction {};
|
|
461
516
|
let members;
|
|
462
|
-
if (ts_morph_1.Node.isTypeAliasDeclaration(
|
|
463
|
-
const body =
|
|
517
|
+
if (ts_morph_1.Node.isTypeAliasDeclaration(declaration)) {
|
|
518
|
+
const body = declaration.getTypeNode();
|
|
464
519
|
if (body && ts_morph_1.Node.isTypeLiteral(body)) {
|
|
465
520
|
members = body.getMembers();
|
|
466
521
|
}
|
|
467
522
|
}
|
|
468
523
|
else {
|
|
469
|
-
members =
|
|
524
|
+
members = declaration.getMembers();
|
|
470
525
|
}
|
|
471
526
|
if (!members) {
|
|
472
527
|
return null;
|
|
473
528
|
}
|
|
474
|
-
const
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
if (!typeString
|
|
529
|
+
const propertySignatures = members.filter(ts_morph_1.Node.isPropertySignature.bind(ts_morph_1.Node));
|
|
530
|
+
// Actions and events must have a `type`, and it must be a string.
|
|
531
|
+
const typeString = getMessengerCapabilityTypeString(propertySignatures);
|
|
532
|
+
if (!typeString) {
|
|
478
533
|
return null;
|
|
479
534
|
}
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
// istanbul ignore next: actions always have `handler` and events always
|
|
484
|
-
// have `payload`; the messenger walk wouldn't have surfaced this
|
|
485
|
-
// declaration otherwise.
|
|
486
|
-
if (!rawSourceMember) {
|
|
535
|
+
// Actions must have a `handler`, and events must have a `payload`.
|
|
536
|
+
const handlerOrPayloadProperty = findProperty(propertySignatures, kind === 'action' ? 'handler' : 'payload');
|
|
537
|
+
if (!handlerOrPayloadProperty) {
|
|
487
538
|
return null;
|
|
488
539
|
}
|
|
489
|
-
const
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
// For actions
|
|
498
|
-
//
|
|
499
|
-
//
|
|
540
|
+
const handlerOrPayloadPropertyTypeNode =
|
|
541
|
+
// We can assume the property has a type; otherwise it wouldn't compile.
|
|
542
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
543
|
+
handlerOrPayloadProperty.getTypeNode();
|
|
544
|
+
let handlerOrPayloadSignature = handlerOrPayloadPropertyTypeNode
|
|
545
|
+
.getText()
|
|
546
|
+
.trim();
|
|
547
|
+
const { description: jsDoc, params, returns } = extractJsDoc(declaration);
|
|
548
|
+
// For actions that represent methods (e.g. `Class['method']`), walk the
|
|
549
|
+
// handler type to find the underlying handler signature
|
|
550
|
+
// (e.g. `(id: number) => Promise<string>`).
|
|
500
551
|
if (kind === 'action') {
|
|
501
|
-
const
|
|
502
|
-
if (
|
|
503
|
-
|
|
504
|
-
handlerOrPayload = info.signature;
|
|
505
|
-
if (!jsDoc && info.jsDoc) {
|
|
506
|
-
jsDoc = info.jsDoc;
|
|
507
|
-
}
|
|
508
|
-
if (params.length === 0 && info.params.length > 0) {
|
|
509
|
-
params = info.params;
|
|
510
|
-
}
|
|
511
|
-
if (!returns && info.returns) {
|
|
512
|
-
returns = info.returns;
|
|
513
|
-
}
|
|
552
|
+
const methodDeclaration = findClassMethodDeclaration(handlerOrPayloadPropertyTypeNode);
|
|
553
|
+
if (methodDeclaration) {
|
|
554
|
+
handlerOrPayloadSignature = buildMethodSignature(methodDeclaration);
|
|
514
555
|
}
|
|
515
556
|
}
|
|
516
|
-
const sourceFile =
|
|
557
|
+
const sourceFile = declaration.getSourceFile();
|
|
517
558
|
return {
|
|
518
|
-
typeName:
|
|
559
|
+
typeName: declaration.getName(),
|
|
519
560
|
typeString,
|
|
520
561
|
kind,
|
|
521
562
|
jsDoc,
|
|
522
563
|
params,
|
|
523
564
|
returns,
|
|
524
|
-
handlerOrPayload,
|
|
565
|
+
handlerOrPayload: handlerOrPayloadSignature,
|
|
525
566
|
sourceFile: path.relative(projectPath, sourceFile.getFilePath()),
|
|
526
|
-
line:
|
|
527
|
-
deprecated:
|
|
567
|
+
line: declaration.getStartLineNumber(),
|
|
568
|
+
deprecated: hasDeprecatedJsDocTag(declaration),
|
|
528
569
|
};
|
|
529
570
|
}
|
|
530
571
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
572
|
+
* Searches the property signatures of a messenger capability type alias or
|
|
573
|
+
* interface to find the value of the `type` property, and then resolves it to a
|
|
574
|
+
* string (assuming it is already a string or a resolvable template literal).
|
|
534
575
|
*
|
|
535
|
-
* @param
|
|
536
|
-
*
|
|
576
|
+
* @param capabilityTypeProperties - The property signatures of the messenger
|
|
577
|
+
* capability type.
|
|
578
|
+
* @returns The extracted capability type string, or null if `type` cannot be
|
|
579
|
+
* found in the members or it is an unexpected node.
|
|
537
580
|
*/
|
|
538
|
-
function
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
return
|
|
570
|
-
}
|
|
571
|
-
if (ts_morph_1.Node.isTemplateLiteralTypeNode(typeNode)) {
|
|
572
|
-
return resolveTemplateLiteralType(typeNode);
|
|
581
|
+
function getMessengerCapabilityTypeString(capabilityTypeProperties) {
|
|
582
|
+
const typeProperty = findProperty(capabilityTypeProperties, 'type');
|
|
583
|
+
if (!typeProperty) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
// A `type` property without an explicit type wouldn't compile.
|
|
587
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
588
|
+
const typeNode = typeProperty.getTypeNode();
|
|
589
|
+
// Ask the type checker to resolve the value of `type`. We're looking for
|
|
590
|
+
// `type` to be either a string literal or template literal.
|
|
591
|
+
//
|
|
592
|
+
// EXAMPLES:
|
|
593
|
+
// type FooControllerSomeAction = {
|
|
594
|
+
// type: 'FooController:someAction';
|
|
595
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
596
|
+
// }
|
|
597
|
+
// type FooControllerSomeAction = {
|
|
598
|
+
// type: `FooController:someAction`;
|
|
599
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
600
|
+
// }
|
|
601
|
+
// type FooControllerSomeAction = {
|
|
602
|
+
// type: `${typeof CONTROLLER_NAME}:someAction`;
|
|
603
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
604
|
+
// }
|
|
605
|
+
const resolvedType = typeNode.getType();
|
|
606
|
+
if (resolvedType.isStringLiteral()) {
|
|
607
|
+
// Type assertion: There aren't any type guards we can use to narrow this
|
|
608
|
+
// type further.
|
|
609
|
+
const literalValue = resolvedType.getLiteralValueOrThrow();
|
|
610
|
+
// Messenger action/event types need to be namespaced.
|
|
611
|
+
if (literalValue.includes(':')) {
|
|
612
|
+
return literalValue;
|
|
573
613
|
}
|
|
574
614
|
}
|
|
575
|
-
// istanbul ignore next: the inline shape always has a `type` member that
|
|
576
|
-
// produces either a literal or template-literal type.
|
|
577
615
|
return null;
|
|
578
616
|
}
|
|
579
617
|
/**
|
|
580
|
-
*
|
|
618
|
+
* Finds a specific property in a list of property signatures for an object
|
|
619
|
+
* type.
|
|
581
620
|
*
|
|
582
|
-
* @param
|
|
583
|
-
*
|
|
621
|
+
* @param propertySignatures - The property signatures of the messenger
|
|
622
|
+
* capability type.
|
|
623
|
+
* @param name - The property name to find.
|
|
584
624
|
* @returns The property signature, or null.
|
|
585
625
|
*/
|
|
586
|
-
function
|
|
587
|
-
for (const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
continue;
|
|
592
|
-
}
|
|
593
|
-
const memberNameNode = member.getNameNode();
|
|
594
|
-
if (
|
|
595
|
-
// istanbul ignore next: capability properties always have identifier names.
|
|
596
|
-
!ts_morph_1.Node.isIdentifier(memberNameNode) ||
|
|
597
|
-
memberNameNode.getText() !== propName) {
|
|
626
|
+
function findProperty(propertySignatures, name) {
|
|
627
|
+
for (const property of propertySignatures) {
|
|
628
|
+
const propertyNameNode = property.getNameNode();
|
|
629
|
+
if (!ts_morph_1.Node.isIdentifier(propertyNameNode) ||
|
|
630
|
+
propertyNameNode.getText() !== name) {
|
|
598
631
|
continue;
|
|
599
632
|
}
|
|
600
|
-
return
|
|
633
|
+
return property;
|
|
601
634
|
}
|
|
602
635
|
return null;
|
|
603
636
|
}
|
|
604
637
|
/**
|
|
605
|
-
*
|
|
606
|
-
* `ControllerGetStateAction
|
|
607
|
-
*
|
|
638
|
+
* If a messenger capability type is a type alias for either the
|
|
639
|
+
* `ControllerGetStateAction` or `ControllerStateChangeEvent` type constructors,
|
|
640
|
+
* then this function extracts information about the type (action/event type
|
|
641
|
+
* string, handler/payload arguments and return type, etc.)
|
|
608
642
|
*
|
|
609
|
-
* @param
|
|
610
|
-
*
|
|
643
|
+
* @param capabilityTypeDeclaration - The statement that declared the type for a
|
|
644
|
+
* messenger action or event, extracted in a previous step.
|
|
611
645
|
* @param projectPath - Project root, used for computing relative source paths.
|
|
612
|
-
* @returns The extracted capability, or null if the
|
|
646
|
+
* @returns The extracted capability packet, or null if the shape of the type
|
|
647
|
+
* doesn't match.
|
|
613
648
|
*/
|
|
614
|
-
function
|
|
615
|
-
const
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
// istanbul ignore next: qualified-name type references aren't used.
|
|
622
|
-
if (!ts_morph_1.Node.isIdentifier(nameNode)) {
|
|
623
|
-
return null;
|
|
624
|
-
}
|
|
625
|
-
const constructorName = nameNode.getText();
|
|
626
|
-
const typeArgs = aliasBody.getTypeArguments();
|
|
627
|
-
if (typeArgs.length < 2) {
|
|
628
|
-
return null;
|
|
629
|
-
}
|
|
649
|
+
function tryToExtractFromCapabilityTypeConstructor(capabilityTypeDeclaration, projectPath) {
|
|
650
|
+
const { declaration, kind, body, typeName } = capabilityTypeDeclaration;
|
|
651
|
+
// The name of the utility type should be either `ControllerGetStateAction`
|
|
652
|
+
// (for actions) or `ControllerStateChangeEvent` (for events).
|
|
653
|
+
// EXAMPLES:
|
|
654
|
+
// type FooControllerSomeAction = ControllerGetStateAction<...>
|
|
655
|
+
// type FooControllerSomeEvent = ControllerStateChangeEvent<...>
|
|
630
656
|
const expectedConstructor = kind === 'action'
|
|
631
657
|
? 'ControllerGetStateAction'
|
|
632
658
|
: 'ControllerStateChangeEvent';
|
|
633
|
-
if (
|
|
659
|
+
if (typeName.getText() !== expectedConstructor) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
// The utility type should take two parameters.
|
|
663
|
+
// EXAMPLES:
|
|
664
|
+
// type FooControllerSomeAction = ControllerGetStateAction<..., ...>
|
|
665
|
+
// type FooControllerSomeEvent = ControllerStateChangeEvent<..., ...>
|
|
666
|
+
const typeArgs = body.getTypeArguments();
|
|
667
|
+
if (typeArgs.length < 2) {
|
|
634
668
|
return null;
|
|
635
669
|
}
|
|
636
|
-
const
|
|
637
|
-
//
|
|
638
|
-
|
|
670
|
+
const namespaceArgType = typeArgs[0].getType();
|
|
671
|
+
// The first parameter should be a string literal.
|
|
672
|
+
// EXAMPLES:
|
|
673
|
+
// type FooControllerSomeAction = ControllerGetStateAction<'FooController', ...>
|
|
674
|
+
// type FooControllerSomeAction = ControllerGetStateAction<typeof CONTROLLER_NAME, ...>
|
|
675
|
+
if (!namespaceArgType.isStringLiteral()) {
|
|
639
676
|
return null;
|
|
640
677
|
}
|
|
678
|
+
// Type assertion: There aren't any type guards we can use to narrow this type
|
|
679
|
+
// further.
|
|
680
|
+
const namespace = namespaceArgType.getLiteralValueOrThrow();
|
|
681
|
+
const typeString = kind === 'action' ? `${namespace}:getState` : `${namespace}:stateChange`;
|
|
641
682
|
const stateArgText = typeArgs[1].getText();
|
|
642
|
-
const
|
|
643
|
-
const
|
|
683
|
+
const handlerOrPayload = kind === 'action' ? `() => ${stateArgText}` : `[${stateArgText}, Patch[]]`;
|
|
684
|
+
const { description, params, returns } = extractJsDoc(declaration);
|
|
685
|
+
const sourceFile = declaration.getSourceFile();
|
|
644
686
|
return {
|
|
645
|
-
typeName:
|
|
646
|
-
typeString
|
|
687
|
+
typeName: declaration.getName(),
|
|
688
|
+
typeString,
|
|
647
689
|
kind,
|
|
648
690
|
jsDoc: description,
|
|
649
691
|
params,
|
|
650
692
|
returns,
|
|
651
|
-
handlerOrPayload
|
|
652
|
-
? `() => ${stateArgText}`
|
|
653
|
-
: `[${stateArgText}, Patch[]]`,
|
|
693
|
+
handlerOrPayload,
|
|
654
694
|
sourceFile: path.relative(projectPath, sourceFile.getFilePath()),
|
|
655
|
-
line:
|
|
656
|
-
deprecated:
|
|
695
|
+
line: declaration.getStartLineNumber(),
|
|
696
|
+
deprecated: hasDeprecatedJsDocTag(declaration),
|
|
657
697
|
};
|
|
658
698
|
}
|
|
659
699
|
// ---------------------------------------------------------------------------
|
|
@@ -686,60 +726,29 @@ function createExtractionProject() {
|
|
|
686
726
|
}
|
|
687
727
|
exports.createExtractionProject = createExtractionProject;
|
|
688
728
|
/**
|
|
689
|
-
* Extract
|
|
690
|
-
*
|
|
729
|
+
* Extract information (action/event type string, handler/payload arguments and
|
|
730
|
+
* return type, etc.) about every messenger action or event type which is
|
|
731
|
+
* reachable through all of a source file's `*Messenger` type declarations.
|
|
691
732
|
*
|
|
692
733
|
* The caller is responsible for ensuring `sourceFile` (plus any files it
|
|
693
|
-
* imports from) belongs to a ts-morph Project so cross-file symbol resolution
|
|
734
|
+
* imports from) belongs to a `ts-morph` Project so cross-file symbol resolution
|
|
694
735
|
* works.
|
|
695
736
|
*
|
|
696
737
|
* @param sourceFile - The TypeScript source file to extract from.
|
|
697
738
|
* @param projectPath - Project root, used for computing relative source paths.
|
|
698
|
-
* @returns The extracted
|
|
739
|
+
* @returns The extracted information about actions and events.
|
|
699
740
|
*/
|
|
700
741
|
function extractFromSourceFile(sourceFile, projectPath) {
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
if (item) {
|
|
709
|
-
items.push(item);
|
|
742
|
+
const messengerTypeAliases = findMessengerTypeAliases(sourceFile);
|
|
743
|
+
const capabilityTypeDeclarations = findAllMessengerCapabilityTypeDeclarations(messengerTypeAliases);
|
|
744
|
+
const messengerCapabilityPackets = [];
|
|
745
|
+
for (const capabilityTypeDeclaration of capabilityTypeDeclarations) {
|
|
746
|
+
const messengerCapabilityPacket = extractFromMessengerCapabilityTypeDeclaration(capabilityTypeDeclaration, projectPath);
|
|
747
|
+
if (messengerCapabilityPacket) {
|
|
748
|
+
messengerCapabilityPackets.push(messengerCapabilityPacket);
|
|
710
749
|
}
|
|
711
750
|
}
|
|
712
|
-
return
|
|
751
|
+
return messengerCapabilityPackets;
|
|
713
752
|
}
|
|
714
753
|
exports.extractFromSourceFile = extractFromSourceFile;
|
|
715
|
-
/**
|
|
716
|
-
* Convenience wrapper: extract from a single file by path. Loads the file
|
|
717
|
-
* and any sibling files in its parent directory into a fresh Project so
|
|
718
|
-
* relative imports resolve.
|
|
719
|
-
*
|
|
720
|
-
* For batch operations across many files, prefer
|
|
721
|
-
* {@link createExtractionProject} + {@link extractFromSourceFile} so one
|
|
722
|
-
* Project amortizes the type-checker setup across the whole run.
|
|
723
|
-
*
|
|
724
|
-
* @param filePath - The absolute path to the TypeScript file.
|
|
725
|
-
* @param projectPath - Base path for computing relative source paths.
|
|
726
|
-
* @returns A promise that resolves to the extracted capability list.
|
|
727
|
-
*/
|
|
728
|
-
async function extractFromFile(filePath, projectPath) {
|
|
729
|
-
const project = createExtractionProject();
|
|
730
|
-
const parentDir = path.dirname(filePath);
|
|
731
|
-
// Load the file's directory so single-hop relative imports resolve.
|
|
732
|
-
project.addSourceFilesAtPaths([
|
|
733
|
-
path.join(parentDir, '**/*.ts'),
|
|
734
|
-
path.join(parentDir, '**/*.d.cts'),
|
|
735
|
-
]);
|
|
736
|
-
const sourceFile = project.getSourceFile(filePath);
|
|
737
|
-
// istanbul ignore next: the path always exists since the caller just
|
|
738
|
-
// wrote the file or it was scanned from disk.
|
|
739
|
-
if (!sourceFile) {
|
|
740
|
-
return [];
|
|
741
|
-
}
|
|
742
|
-
return extractFromSourceFile(sourceFile, projectPath);
|
|
743
|
-
}
|
|
744
|
-
exports.extractFromFile = extractFromFile;
|
|
745
754
|
//# sourceMappingURL=extraction.cjs.map
|