@moku-labs/game 0.6.0 → 0.7.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 +3 -2
- package/dist/index.mjs +2 -0
- package/dist/lint.d.mts +27 -6
- package/dist/lint.mjs +180 -52
- package/dist/project.d.mts +23 -12
- package/dist/project.mjs +1222 -853
- package/llms.txt +5 -3
- package/package.json +1 -1
package/dist/project.mjs
CHANGED
|
@@ -3,1022 +3,1299 @@ import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { watch } from "node:fs";
|
|
6
|
-
|
|
7
|
-
const DEFINERS = [
|
|
8
|
-
"defineFlow",
|
|
9
|
-
"defineFeature",
|
|
10
|
-
"defineScene",
|
|
11
|
-
"projection",
|
|
12
|
-
"defineEmitter",
|
|
13
|
-
"defineTextStyles",
|
|
14
|
-
"defineStyle"
|
|
15
|
-
];
|
|
6
|
+
//#region src/project/extract/scope.ts
|
|
16
7
|
/**
|
|
17
|
-
*
|
|
8
|
+
* Unwraps the expressions that change no value: parentheses, `as`, `satisfies`, `!` and `<T>`.
|
|
18
9
|
*
|
|
19
|
-
* @param
|
|
20
|
-
* @
|
|
21
|
-
* @
|
|
22
|
-
* ```ts
|
|
23
|
-
* isDefiner("defineScene"); // true
|
|
24
|
-
* ```
|
|
10
|
+
* @param typescript - The TypeScript module.
|
|
11
|
+
* @param node - An expression.
|
|
12
|
+
* @returns The expression inside.
|
|
25
13
|
*/
|
|
26
|
-
function
|
|
27
|
-
|
|
14
|
+
function unwrap(typescript, node) {
|
|
15
|
+
let inner = node;
|
|
16
|
+
while (typescript.isParenthesizedExpression(inner) || typescript.isAsExpression(inner) || typescript.isSatisfiesExpression(inner) || typescript.isNonNullExpression(inner) || typescript.isTypeAssertionExpression(inner)) inner = inner.expression;
|
|
17
|
+
return inner;
|
|
28
18
|
}
|
|
29
19
|
/**
|
|
30
|
-
*
|
|
20
|
+
* The text of a property name written literally: an identifier, a string or a number.
|
|
31
21
|
*
|
|
32
22
|
* @param typescript - The TypeScript module.
|
|
33
|
-
* @param
|
|
34
|
-
* @
|
|
35
|
-
* @returns True when the modifier is there.
|
|
23
|
+
* @param name - A property name.
|
|
24
|
+
* @returns The text, or `undefined` for a computed or a private name.
|
|
36
25
|
*/
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
return typescript.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false;
|
|
26
|
+
function literalNameOf(typescript, name) {
|
|
27
|
+
return typescript.isIdentifier(name) || typescript.isStringLiteral(name) || typescript.isNumericLiteral(name) ? name.text : void 0;
|
|
40
28
|
}
|
|
41
29
|
/**
|
|
42
|
-
* The
|
|
30
|
+
* The name of a property-like node, when it is written literally: a property, a shorthand, a
|
|
31
|
+
* method or an accessor.
|
|
43
32
|
*
|
|
44
33
|
* @param typescript - The TypeScript module.
|
|
45
|
-
* @param
|
|
46
|
-
* @returns The
|
|
34
|
+
* @param node - Any node.
|
|
35
|
+
* @returns The name, or `undefined` for a spread, a computed name or a node without a name.
|
|
47
36
|
*/
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
return name.elements.flatMap((element) => typescript.isBindingElement(element) ? namesOf(typescript, element.name) : []);
|
|
37
|
+
function propertyNameOf(typescript, node) {
|
|
38
|
+
return typescript.isPropertyAssignment(node) || typescript.isShorthandPropertyAssignment(node) || typescript.isMethodDeclaration(node) || typescript.isGetAccessorDeclaration(node) || typescript.isSetAccessorDeclaration(node) ? literalNameOf(typescript, node.name) : void 0;
|
|
51
39
|
}
|
|
52
40
|
/**
|
|
53
|
-
*
|
|
41
|
+
* Whether a node holds statements: a block, a source file, a module block or a case clause.
|
|
54
42
|
*
|
|
55
|
-
* @param
|
|
56
|
-
* @
|
|
57
|
-
* @param draft - The facts so far.
|
|
43
|
+
* @param node - A node.
|
|
44
|
+
* @returns True when it has a `statements` list.
|
|
58
45
|
*/
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
if (clause === void 0 || clause.isTypeOnly) return;
|
|
62
|
-
if (!typescript.isStringLiteral(statement.moduleSpecifier)) return;
|
|
63
|
-
const specifier = statement.moduleSpecifier.text;
|
|
64
|
-
const bindings = clause.namedBindings;
|
|
65
|
-
if (clause.name !== void 0) draft.imports.set(clause.name.text, {
|
|
66
|
-
specifier,
|
|
67
|
-
imported: "default"
|
|
68
|
-
});
|
|
69
|
-
if (bindings === void 0) return;
|
|
70
|
-
if (typescript.isNamespaceImport(bindings)) {
|
|
71
|
-
draft.imports.set(bindings.name.text, {
|
|
72
|
-
specifier,
|
|
73
|
-
imported: "*"
|
|
74
|
-
});
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
for (const element of bindings.elements) {
|
|
78
|
-
if (element.isTypeOnly) continue;
|
|
79
|
-
const imported = element.propertyName?.text ?? element.name.text;
|
|
80
|
-
draft.imports.set(element.name.text, {
|
|
81
|
-
specifier,
|
|
82
|
-
imported
|
|
83
|
-
});
|
|
84
|
-
}
|
|
46
|
+
function holdsStatements(node) {
|
|
47
|
+
return "statements" in node && Array.isArray(node.statements);
|
|
85
48
|
}
|
|
86
49
|
/**
|
|
87
|
-
*
|
|
88
|
-
* bindings.
|
|
50
|
+
* Whether a node is a function whose parameters a name can be.
|
|
89
51
|
*
|
|
90
52
|
* @param typescript - The TypeScript module.
|
|
91
|
-
* @param
|
|
92
|
-
* @
|
|
53
|
+
* @param node - A node.
|
|
54
|
+
* @returns True for function declarations, expressions, arrows and methods.
|
|
93
55
|
*/
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
const specifier = source !== void 0 && typescript.isStringLiteral(source) ? source.text : "";
|
|
97
|
-
const clause = statement.exportClause;
|
|
98
|
-
if (statement.isTypeOnly) return;
|
|
99
|
-
if (clause === void 0) {
|
|
100
|
-
if (specifier !== "") draft.stars.push(specifier);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (typescript.isNamespaceExport(clause)) {
|
|
104
|
-
draft.exports.set(clause.name.text, {
|
|
105
|
-
specifier,
|
|
106
|
-
imported: "*"
|
|
107
|
-
});
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
for (const element of clause.elements) {
|
|
111
|
-
if (element.isTypeOnly) continue;
|
|
112
|
-
const inner = element.propertyName?.text ?? element.name.text;
|
|
113
|
-
const target = specifier === "" ? { local: inner } : {
|
|
114
|
-
specifier,
|
|
115
|
-
imported: inner
|
|
116
|
-
};
|
|
117
|
-
draft.exports.set(element.name.text, target);
|
|
118
|
-
}
|
|
56
|
+
function isFunctionScope(typescript, node) {
|
|
57
|
+
return typescript.isFunctionDeclaration(node) || typescript.isFunctionExpression(node) || typescript.isArrowFunction(node) || typescript.isMethodDeclaration(node);
|
|
119
58
|
}
|
|
120
59
|
/**
|
|
121
|
-
*
|
|
60
|
+
* The meaning of a `const` declarator: a function when its initializer is one.
|
|
122
61
|
*
|
|
123
|
-
* @param
|
|
124
|
-
* @param
|
|
125
|
-
* @returns
|
|
126
|
-
* @example
|
|
127
|
-
* ```ts
|
|
128
|
-
* isGameImport({ specifier: "@moku-labs/game", imported: "defineGame" }, "defineGame"); // true
|
|
129
|
-
* ```
|
|
62
|
+
* @param typescript - The TypeScript module.
|
|
63
|
+
* @param initializer - The initializer of the const.
|
|
64
|
+
* @returns The meaning.
|
|
130
65
|
*/
|
|
131
|
-
function
|
|
132
|
-
|
|
66
|
+
function constMeaning(typescript, initializer) {
|
|
67
|
+
const inner = unwrap(typescript, initializer);
|
|
68
|
+
return typescript.isArrowFunction(inner) || typescript.isFunctionExpression(inner) ? {
|
|
69
|
+
kind: "function",
|
|
70
|
+
node: inner
|
|
71
|
+
} : {
|
|
72
|
+
kind: "const",
|
|
73
|
+
initializer
|
|
74
|
+
};
|
|
133
75
|
}
|
|
134
76
|
/**
|
|
135
|
-
*
|
|
136
|
-
* through a namespace import of the package.
|
|
77
|
+
* Looks for a name among the statements of one block.
|
|
137
78
|
*
|
|
138
79
|
* @param typescript - The TypeScript module.
|
|
139
|
-
* @param
|
|
140
|
-
* @param
|
|
141
|
-
* @returns
|
|
80
|
+
* @param holder - The block.
|
|
81
|
+
* @param name - The name.
|
|
82
|
+
* @returns What the name means there, or `undefined`.
|
|
142
83
|
*/
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
84
|
+
function declaredIn(typescript, holder, name) {
|
|
85
|
+
for (const statement of holder.statements) {
|
|
86
|
+
if (typescript.isFunctionDeclaration(statement) && statement.name?.text === name) return {
|
|
87
|
+
kind: "function",
|
|
88
|
+
node: statement
|
|
89
|
+
};
|
|
90
|
+
if (!typescript.isVariableStatement(statement)) continue;
|
|
91
|
+
const isConst = (statement.declarationList.flags & typescript.NodeFlags.Const) !== 0;
|
|
92
|
+
const declaration = statement.declarationList.declarations.find((item) => typescript.isIdentifier(item.name) && item.name.text === name);
|
|
93
|
+
if (isConst && declaration?.initializer !== void 0) return constMeaning(typescript, declaration.initializer);
|
|
94
|
+
}
|
|
148
95
|
}
|
|
149
96
|
/**
|
|
150
|
-
*
|
|
97
|
+
* Looks for a name among the names of a destructured parameter (`({ id }) => …`).
|
|
151
98
|
*
|
|
152
99
|
* @param typescript - The TypeScript module.
|
|
153
100
|
* @param pattern - The destructure.
|
|
154
|
-
* @param
|
|
101
|
+
* @param name - The name.
|
|
102
|
+
* @returns The parameter meaning with the property it takes, or `undefined`.
|
|
155
103
|
*/
|
|
156
|
-
function
|
|
104
|
+
function destructuredIn(typescript, pattern, name) {
|
|
157
105
|
for (const element of pattern.elements) {
|
|
158
|
-
if (!typescript.isIdentifier(element.name)) continue;
|
|
106
|
+
if (!(typescript.isBindingElement(element) && typescript.isIdentifier(element.name) && element.name.text === name)) continue;
|
|
159
107
|
const property = element.propertyName;
|
|
160
|
-
|
|
161
|
-
|
|
108
|
+
return {
|
|
109
|
+
kind: "param",
|
|
110
|
+
property: property !== void 0 && typescript.isIdentifier(property) ? property.text : name
|
|
111
|
+
};
|
|
162
112
|
}
|
|
163
113
|
}
|
|
164
114
|
/**
|
|
165
|
-
*
|
|
115
|
+
* Looks for a name among the parameters of one function: a plain parameter, or a property of a
|
|
116
|
+
* destructured one.
|
|
166
117
|
*
|
|
167
118
|
* @param typescript - The TypeScript module.
|
|
168
|
-
* @param
|
|
169
|
-
* @param
|
|
119
|
+
* @param scope - The function.
|
|
120
|
+
* @param name - The name.
|
|
121
|
+
* @returns The parameter meaning, or `undefined`.
|
|
170
122
|
*/
|
|
171
|
-
function
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const pattern = declaration.name;
|
|
179
|
-
if (typescript.isObjectBindingPattern(pattern) && callsDefineGame(typescript, declaration.initializer, draft.imports)) readKit(typescript, pattern, draft);
|
|
123
|
+
function parameterIn(typescript, scope, name) {
|
|
124
|
+
for (const parameter of scope.parameters) {
|
|
125
|
+
const binding = parameter.name;
|
|
126
|
+
if (typescript.isIdentifier(binding) && binding.text === name) return { kind: "param" };
|
|
127
|
+
if (typescript.isIdentifier(binding)) continue;
|
|
128
|
+
const meaning = destructuredIn(typescript, binding, name);
|
|
129
|
+
if (meaning !== void 0) return meaning;
|
|
180
130
|
}
|
|
181
131
|
}
|
|
182
132
|
/**
|
|
183
|
-
*
|
|
133
|
+
* Says what a name means at the place it is used, walking up from there.
|
|
184
134
|
*
|
|
185
135
|
* @param typescript - The TypeScript module.
|
|
186
|
-
* @param
|
|
187
|
-
* @
|
|
136
|
+
* @param identifier - The name where it is used.
|
|
137
|
+
* @returns What it means, or `undefined` when the file does not declare it.
|
|
188
138
|
*/
|
|
189
|
-
function
|
|
190
|
-
const name =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
139
|
+
function meaningOf(typescript, identifier) {
|
|
140
|
+
const name = identifier.text;
|
|
141
|
+
for (let node = identifier.parent; node !== void 0; node = node.parent) {
|
|
142
|
+
const parameter = isFunctionScope(typescript, node) ? parameterIn(typescript, node, name) : void 0;
|
|
143
|
+
if (parameter !== void 0) return parameter;
|
|
144
|
+
const declared = holdsStatements(node) ? declaredIn(typescript, node, name) : void 0;
|
|
145
|
+
if (declared !== void 0) return declared;
|
|
146
|
+
}
|
|
196
147
|
}
|
|
197
148
|
/**
|
|
198
|
-
*
|
|
199
|
-
* in the file knows where its `defineGame` comes from.
|
|
149
|
+
* The statement directly under the file that a node sits in.
|
|
200
150
|
*
|
|
201
151
|
* @param typescript - The TypeScript module.
|
|
202
|
-
* @param
|
|
203
|
-
* @returns The
|
|
152
|
+
* @param node - Any node.
|
|
153
|
+
* @returns The statement, or the file itself for the file.
|
|
204
154
|
*/
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
stars: [],
|
|
210
|
-
declared: /* @__PURE__ */ new Set(),
|
|
211
|
-
kit: /* @__PURE__ */ new Map()
|
|
212
|
-
};
|
|
213
|
-
for (const statement of source.statements) if (typescript.isImportDeclaration(statement)) readImport(typescript, statement, draft);
|
|
214
|
-
for (const statement of source.statements) {
|
|
215
|
-
if (typescript.isVariableStatement(statement)) readVariables(typescript, statement, draft);
|
|
216
|
-
if (typescript.isFunctionDeclaration(statement) || typescript.isClassDeclaration(statement)) readNamed(typescript, statement, draft);
|
|
217
|
-
if (typescript.isExportDeclaration(statement)) readExport(typescript, statement, draft);
|
|
218
|
-
if (typescript.isExportAssignment(statement) && typescript.isIdentifier(statement.expression)) draft.exports.set("default", { local: statement.expression.text });
|
|
219
|
-
}
|
|
220
|
-
return draft;
|
|
155
|
+
function topStatementOf(typescript, node) {
|
|
156
|
+
let statement = node;
|
|
157
|
+
while (statement.parent !== void 0 && !typescript.isSourceFile(statement.parent)) statement = statement.parent;
|
|
158
|
+
return statement;
|
|
221
159
|
}
|
|
222
|
-
//#endregion
|
|
223
|
-
//#region src/project/extract/resolve.ts
|
|
224
160
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
161
|
+
* The name of the top-level declaration a node sits in: the function, class or first const of
|
|
162
|
+
* the statement directly under the file.
|
|
163
|
+
*
|
|
164
|
+
* @param typescript - The TypeScript module.
|
|
165
|
+
* @param node - Any node.
|
|
166
|
+
* @returns The declared name, or `undefined` for a statement that declares none.
|
|
230
167
|
*/
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
168
|
+
function enclosingName(typescript, node) {
|
|
169
|
+
const statement = topStatementOf(typescript, node);
|
|
170
|
+
if (typescript.isFunctionDeclaration(statement) || typescript.isClassDeclaration(statement)) return statement.name?.text;
|
|
171
|
+
if (!typescript.isVariableStatement(statement)) return void 0;
|
|
172
|
+
const [first] = statement.declarationList.declarations;
|
|
173
|
+
return first !== void 0 && typescript.isIdentifier(first.name) ? first.name.text : void 0;
|
|
174
|
+
}
|
|
235
175
|
/**
|
|
236
|
-
*
|
|
176
|
+
* The function a const declarator holds, through parentheses, `as` and `satisfies`.
|
|
237
177
|
*
|
|
238
|
-
* @param
|
|
239
|
-
* @
|
|
240
|
-
* @
|
|
241
|
-
* ```ts
|
|
242
|
-
* isRelative("../kit"); // true
|
|
243
|
-
* ```
|
|
178
|
+
* @param typescript - The TypeScript module.
|
|
179
|
+
* @param declaration - The declarator.
|
|
180
|
+
* @returns The arrow or function expression, or `undefined` for any other value.
|
|
244
181
|
*/
|
|
245
|
-
function
|
|
246
|
-
|
|
182
|
+
function functionValueOf(typescript, declaration) {
|
|
183
|
+
const value = declaration.initializer === void 0 ? void 0 : unwrap(typescript, declaration.initializer);
|
|
184
|
+
return value !== void 0 && (typescript.isArrowFunction(value) || typescript.isFunctionExpression(value)) ? value : void 0;
|
|
247
185
|
}
|
|
248
186
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
187
|
+
* The functions one module-level statement declares: a named function with a body, or the
|
|
188
|
+
* consts whose value is an arrow or a function expression. An overload signature, a `let`, a
|
|
189
|
+
* `var` and a const of any other value declare none.
|
|
251
190
|
*
|
|
252
|
-
* @param
|
|
253
|
-
* @param
|
|
254
|
-
* @
|
|
255
|
-
* @returns The root-relative path, or `undefined` for a package or a file the index does not hold.
|
|
191
|
+
* @param typescript - The TypeScript module.
|
|
192
|
+
* @param statement - A statement directly under the file.
|
|
193
|
+
* @returns The functions, in source order.
|
|
256
194
|
*/
|
|
257
|
-
function
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
195
|
+
function declaredFunctions(typescript, statement) {
|
|
196
|
+
if (typescript.isFunctionDeclaration(statement)) {
|
|
197
|
+
const name = statement.name?.text;
|
|
198
|
+
return name !== void 0 && statement.body !== void 0 ? [{
|
|
199
|
+
name,
|
|
200
|
+
node: statement
|
|
201
|
+
}] : [];
|
|
202
|
+
}
|
|
203
|
+
if (!typescript.isVariableStatement(statement)) return [];
|
|
204
|
+
if ((statement.declarationList.flags & typescript.NodeFlags.Const) === 0) return [];
|
|
205
|
+
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
206
|
+
const node = functionValueOf(typescript, declaration);
|
|
207
|
+
if (node === void 0 || !typescript.isIdentifier(declaration.name)) return [];
|
|
208
|
+
return [{
|
|
209
|
+
name: declaration.name.text,
|
|
210
|
+
node
|
|
211
|
+
}];
|
|
212
|
+
});
|
|
268
213
|
}
|
|
269
214
|
/**
|
|
270
|
-
*
|
|
215
|
+
* The module-level function a node sits in, at any depth: a named function declaration, or a
|
|
216
|
+
* const whose value is a function. A node in a class, an object const or a statement that
|
|
217
|
+
* declares no function has none.
|
|
271
218
|
*
|
|
272
|
-
* @param
|
|
273
|
-
* @param
|
|
274
|
-
* @
|
|
275
|
-
* @param hops - How many hops were taken so far.
|
|
276
|
-
* @returns Where the name ends up.
|
|
219
|
+
* @param typescript - The TypeScript module.
|
|
220
|
+
* @param node - Any node.
|
|
221
|
+
* @returns The name of the function, or `undefined`.
|
|
277
222
|
*/
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
kind: "package",
|
|
281
|
-
...ref
|
|
282
|
-
};
|
|
283
|
-
if (ref.imported === "*") return void 0;
|
|
284
|
-
const target = resolveModulePath(modules, from, ref.specifier);
|
|
285
|
-
return target === void 0 ? void 0 : resolveExport(modules, target, ref.imported, hops + 1);
|
|
223
|
+
function enclosingFunction(typescript, node) {
|
|
224
|
+
return declaredFunctions(typescript, topStatementOf(typescript, node)).find((item) => item.node.pos <= node.pos && node.end <= item.node.end)?.name;
|
|
286
225
|
}
|
|
287
226
|
/**
|
|
288
|
-
*
|
|
227
|
+
* The name of the object property whose value an expression is: `disc` for the call in
|
|
228
|
+
* `{ disc: defineStyle(…) }`, through parentheses, `as` and `satisfies`.
|
|
289
229
|
*
|
|
290
|
-
* @param
|
|
291
|
-
* @param
|
|
292
|
-
* @
|
|
293
|
-
* @param hops - How many hops were taken so far.
|
|
294
|
-
* @returns Where the name ends up.
|
|
230
|
+
* @param typescript - The TypeScript module.
|
|
231
|
+
* @param node - An expression.
|
|
232
|
+
* @returns The property name when it is written literally; `undefined` for anything else.
|
|
295
233
|
*/
|
|
296
|
-
function
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
for (const specifier of facts.stars) {
|
|
303
|
-
const found = followImport(modules, file, {
|
|
304
|
-
specifier,
|
|
305
|
-
imported: name
|
|
306
|
-
}, hops);
|
|
307
|
-
if (found !== void 0) return found;
|
|
308
|
-
}
|
|
234
|
+
function propertyOfValue(typescript, node) {
|
|
235
|
+
let outer = node;
|
|
236
|
+
while (typescript.isExpression(outer.parent) && unwrap(typescript, outer.parent) === node) outer = outer.parent;
|
|
237
|
+
const holder = outer.parent;
|
|
238
|
+
if (!typescript.isPropertyAssignment(holder) || holder.initializer !== outer) return void 0;
|
|
239
|
+
return literalNameOf(typescript, holder.name);
|
|
309
240
|
}
|
|
310
241
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* @param modules - The module facts by path.
|
|
315
|
-
* @param file - The file that uses the name.
|
|
316
|
-
* @param local - The name as the file writes it.
|
|
317
|
-
* @param hops - How many hops were taken so far.
|
|
318
|
-
* @returns Where the name ends up, or `undefined` when it cannot be followed.
|
|
242
|
+
* A hole filled at run time by a key-carrying prop of the component: `{id}`, `{amountKey}`. The
|
|
243
|
+
* regex is global, so `.test` keeps `lastIndex` between calls: test a pattern with `hasHole`, never
|
|
244
|
+
* `HOLE.test`.
|
|
319
245
|
*/
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
kind: "definer",
|
|
326
|
-
definer
|
|
327
|
-
};
|
|
328
|
-
if (facts.declared.has(local)) return {
|
|
329
|
-
kind: "declared",
|
|
330
|
-
path: file,
|
|
331
|
-
binding: local
|
|
332
|
-
};
|
|
333
|
-
const ref = facts.imports.get(local);
|
|
334
|
-
return ref === void 0 ? void 0 : followImport(modules, file, ref, hops);
|
|
335
|
-
}
|
|
246
|
+
const HOLE = /\{\w+\}/g;
|
|
247
|
+
/** A key-carrying prop: `id`, or a name that ends in `Key`. */
|
|
248
|
+
const KEY_PROP = /^(?:id|\w+Key)$/;
|
|
249
|
+
/** How deep names and calls are followed before a hole is given up on. */
|
|
250
|
+
const MAX_DEPTH = 8;
|
|
336
251
|
/**
|
|
337
|
-
* The
|
|
252
|
+
* The hole a prop of a component leaves in a key.
|
|
338
253
|
*
|
|
339
|
-
* @param
|
|
340
|
-
* @returns The
|
|
254
|
+
* @param property - The name of the prop.
|
|
255
|
+
* @returns The hole: `{amountKey}` for `amountKey`.
|
|
341
256
|
* @example
|
|
342
257
|
* ```ts
|
|
343
|
-
*
|
|
258
|
+
* holeOf("amountKey"); // "{amountKey}"
|
|
344
259
|
* ```
|
|
345
260
|
*/
|
|
346
|
-
function
|
|
347
|
-
|
|
348
|
-
if (target?.kind !== "package" || target.specifier !== "@moku-labs/game") return void 0;
|
|
349
|
-
return isDefiner(target.imported) ? target.imported : void 0;
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Adds the definers a namespace import reaches as `ns.name`.
|
|
353
|
-
*
|
|
354
|
-
* @param modules - The module facts by path.
|
|
355
|
-
* @param file - The importing file.
|
|
356
|
-
* @param namespace - The local name of the namespace.
|
|
357
|
-
* @param specifier - The module it imports.
|
|
358
|
-
* @param definers - The map to add to.
|
|
359
|
-
*/
|
|
360
|
-
function addNamespace(modules, file, namespace, specifier, definers) {
|
|
361
|
-
if (specifier === "@moku-labs/game") {
|
|
362
|
-
for (const definer of DEFINERS) definers.set(`${namespace}.${definer}`, definer);
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
const target = resolveModulePath(modules, file, specifier);
|
|
366
|
-
const names = target === void 0 ? [] : [...modules.get(target)?.exports.keys() ?? []];
|
|
367
|
-
for (const name of names) {
|
|
368
|
-
const definer = definerOf(resolveExport(modules, target ?? "", name));
|
|
369
|
-
if (definer !== void 0) definers.set(`${namespace}.${name}`, definer);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* The definers a file can call, by the callee as the file writes it: `defineFlow`, a renamed
|
|
374
|
-
* `scene`, or `kit.defineScene` through a namespace.
|
|
375
|
-
*
|
|
376
|
-
* @param modules - The module facts by path.
|
|
377
|
-
* @param file - The file.
|
|
378
|
-
* @returns The callee text to the definer.
|
|
379
|
-
*/
|
|
380
|
-
function definersOf(modules, file) {
|
|
381
|
-
const facts = modules.get(file);
|
|
382
|
-
const definers = new Map(facts?.kit);
|
|
383
|
-
for (const [local, ref] of facts?.imports ?? []) {
|
|
384
|
-
if (ref.imported === "*") {
|
|
385
|
-
addNamespace(modules, file, local, ref.specifier, definers);
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
const definer = definerOf(resolveBinding(modules, file, local));
|
|
389
|
-
if (definer !== void 0) definers.set(local, definer);
|
|
390
|
-
}
|
|
391
|
-
return definers;
|
|
261
|
+
function holeOf(property) {
|
|
262
|
+
return `{${property}}`;
|
|
392
263
|
}
|
|
393
|
-
//#endregion
|
|
394
|
-
//#region src/project/hash.ts
|
|
395
|
-
/**
|
|
396
|
-
* @file project — the two hashes of the index: the sha1 of a file's bytes, the same value the
|
|
397
|
-
* editor uses as a file version, and the revision over every file of the index.
|
|
398
|
-
*/
|
|
399
264
|
/**
|
|
400
|
-
*
|
|
265
|
+
* Whether a pattern holds a hole a prop fills.
|
|
401
266
|
*
|
|
402
|
-
* @param
|
|
403
|
-
* @returns
|
|
267
|
+
* @param pattern - A pattern or a key.
|
|
268
|
+
* @returns True when it holds `{id}`, `{amountKey}` or another prop hole.
|
|
404
269
|
* @example
|
|
405
270
|
* ```ts
|
|
406
|
-
*
|
|
271
|
+
* hasHole("{id}Close"); // true
|
|
407
272
|
* ```
|
|
408
273
|
*/
|
|
409
|
-
function
|
|
410
|
-
return
|
|
274
|
+
function hasHole(pattern) {
|
|
275
|
+
return pattern.search(HOLE) !== -1;
|
|
411
276
|
}
|
|
412
277
|
/**
|
|
413
|
-
* The
|
|
278
|
+
* The hole a prop of a parameter object leaves: its own hole for a key-carrying prop, `*` else.
|
|
414
279
|
*
|
|
415
|
-
* @param
|
|
416
|
-
* @returns The
|
|
280
|
+
* @param property - The name of the prop, when the parameter is read through one.
|
|
281
|
+
* @returns The hole.
|
|
417
282
|
* @example
|
|
418
283
|
* ```ts
|
|
419
|
-
*
|
|
284
|
+
* propertyHole("tab"); // "*"
|
|
420
285
|
* ```
|
|
421
286
|
*/
|
|
422
|
-
function
|
|
423
|
-
return
|
|
287
|
+
function propertyHole(property) {
|
|
288
|
+
return property !== void 0 && KEY_PROP.test(property) ? holeOf(property) : "*";
|
|
424
289
|
}
|
|
425
|
-
//#endregion
|
|
426
|
-
//#region src/project/assemble.ts
|
|
427
290
|
/**
|
|
428
|
-
* The
|
|
291
|
+
* The one expression a function returns, when it has exactly one.
|
|
429
292
|
*
|
|
430
|
-
* @param
|
|
431
|
-
* @param
|
|
432
|
-
* @returns
|
|
293
|
+
* @param typescript - The TypeScript module.
|
|
294
|
+
* @param node - The function.
|
|
295
|
+
* @returns The returned expression, or `undefined`.
|
|
433
296
|
*/
|
|
434
|
-
function
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
297
|
+
function returnedBy(typescript, node) {
|
|
298
|
+
const body = node.body;
|
|
299
|
+
if (body === void 0) return void 0;
|
|
300
|
+
if (!typescript.isBlock(body)) return body;
|
|
301
|
+
const returns = [];
|
|
302
|
+
const collect = (child) => {
|
|
303
|
+
if (typescript.isReturnStatement(child)) returns.push(child);
|
|
304
|
+
if (!typescript.isFunctionLike(child)) typescript.forEachChild(child, collect);
|
|
440
305
|
};
|
|
441
|
-
|
|
442
|
-
return
|
|
306
|
+
typescript.forEachChild(body, collect);
|
|
307
|
+
return returns.length === 1 ? returns[0]?.expression : void 0;
|
|
443
308
|
}
|
|
444
309
|
/**
|
|
445
|
-
*
|
|
446
|
-
* it, and the table is its use; a slot or an inline node is anchored at the table. A name that
|
|
447
|
-
* cannot be followed is anchored at the table and listed as unresolved.
|
|
310
|
+
* Reads a name: a bound parameter, a const, or a destructured key-carrying prop.
|
|
448
311
|
*
|
|
449
|
-
* @param
|
|
450
|
-
* @param
|
|
451
|
-
* @param
|
|
452
|
-
* @param
|
|
453
|
-
* @
|
|
312
|
+
* @param typescript - The TypeScript module.
|
|
313
|
+
* @param identifier - The name.
|
|
314
|
+
* @param bindings - The parameters bound by the call being read.
|
|
315
|
+
* @param depth - How deep the reading is.
|
|
316
|
+
* @returns The pattern.
|
|
454
317
|
*/
|
|
455
|
-
function
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const atTable = {
|
|
463
|
-
...use,
|
|
464
|
-
key: name
|
|
465
|
-
};
|
|
466
|
-
const target = ref === void 0 ? void 0 : resolveBinding(assembly.modules, file, ref);
|
|
467
|
-
if (target?.kind === "declared") {
|
|
468
|
-
entry.def.push({
|
|
469
|
-
path: target.path,
|
|
470
|
-
binding: target.binding
|
|
471
|
-
});
|
|
472
|
-
entry.uses.push(atTable);
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
entry.def.push(atTable);
|
|
476
|
-
if (ref !== void 0) {
|
|
477
|
-
const reason = `node "${table.flow}/${name}": "${ref}" is not declared in a file of the root`;
|
|
478
|
-
unresolved.push({
|
|
479
|
-
path: file,
|
|
480
|
-
reason
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
}
|
|
318
|
+
function readName(typescript, identifier, bindings, depth) {
|
|
319
|
+
const bound = bindings.get(identifier.text);
|
|
320
|
+
if (bound !== void 0) return bound;
|
|
321
|
+
const meaning = meaningOf(typescript, identifier);
|
|
322
|
+
if (meaning?.kind === "const") return read(typescript, meaning.initializer, bindings, depth + 1);
|
|
323
|
+
if (meaning?.kind === "param") return propertyHole(meaning.property);
|
|
324
|
+
return "*";
|
|
484
325
|
}
|
|
485
326
|
/**
|
|
486
|
-
*
|
|
327
|
+
* Reads `props.id` or `props.amountKey`: a key-carrying prop of a parameter object becomes its
|
|
328
|
+
* hole, any other prop `*`.
|
|
487
329
|
*
|
|
488
|
-
* @param
|
|
489
|
-
* @param
|
|
330
|
+
* @param typescript - The TypeScript module.
|
|
331
|
+
* @param access - The property access.
|
|
332
|
+
* @param bindings - The parameters bound by the call being read.
|
|
333
|
+
* @returns The pattern.
|
|
490
334
|
*/
|
|
491
|
-
function
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
(target?.kind === "declared" ? symbols.get(`style:${target.path}#${target.binding}`) : void 0)?.uses.push({
|
|
495
|
-
path: file,
|
|
496
|
-
binding: local
|
|
497
|
-
});
|
|
498
|
-
}
|
|
335
|
+
function readAccess(typescript, access, bindings) {
|
|
336
|
+
const owner = access.expression;
|
|
337
|
+
return typescript.isIdentifier(owner) && !bindings.has(owner.text) && meaningOf(typescript, owner)?.kind === "param" ? propertyHole(access.name.text) : "*";
|
|
499
338
|
}
|
|
500
339
|
/**
|
|
501
|
-
*
|
|
502
|
-
*
|
|
340
|
+
* Reads a call of a same-file function: its one returned expression, with each parameter bound
|
|
341
|
+
* to the pattern of its argument.
|
|
503
342
|
*
|
|
504
|
-
* @param
|
|
505
|
-
* @
|
|
343
|
+
* @param typescript - The TypeScript module.
|
|
344
|
+
* @param call - The call.
|
|
345
|
+
* @param bindings - The parameters bound by the call being read.
|
|
346
|
+
* @param depth - How deep the reading is.
|
|
347
|
+
* @returns The pattern.
|
|
506
348
|
*/
|
|
507
|
-
function
|
|
508
|
-
const
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
349
|
+
function readCall$1(typescript, call, bindings, depth) {
|
|
350
|
+
const callee = call.expression;
|
|
351
|
+
const meaning = typescript.isIdentifier(callee) ? meaningOf(typescript, callee) : void 0;
|
|
352
|
+
if (meaning?.kind !== "function") return "*";
|
|
353
|
+
const returned = returnedBy(typescript, meaning.node);
|
|
354
|
+
if (returned === void 0) return "*";
|
|
355
|
+
const inner = /* @__PURE__ */ new Map();
|
|
356
|
+
for (const [index, parameter] of meaning.node.parameters.entries()) {
|
|
357
|
+
const argument = call.arguments[index];
|
|
358
|
+
if (!typescript.isIdentifier(parameter.name)) continue;
|
|
359
|
+
inner.set(parameter.name.text, argument === void 0 ? "*" : read(typescript, argument, bindings, depth + 1));
|
|
517
360
|
}
|
|
518
|
-
return
|
|
361
|
+
return read(typescript, returned, inner, depth + 1);
|
|
519
362
|
}
|
|
520
363
|
/**
|
|
521
|
-
*
|
|
364
|
+
* Reads a template: its literal parts stay, each span is read into a pattern.
|
|
522
365
|
*
|
|
523
|
-
* @param
|
|
524
|
-
* @
|
|
366
|
+
* @param typescript - The TypeScript module.
|
|
367
|
+
* @param template - The template expression.
|
|
368
|
+
* @param bindings - The parameters bound by the call being read.
|
|
369
|
+
* @param depth - How deep the reading is.
|
|
370
|
+
* @returns The pattern.
|
|
525
371
|
*/
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
for (const child of Object.values(value)) deepFreeze(child);
|
|
530
|
-
return value;
|
|
372
|
+
function readTemplate(typescript, template, bindings, depth) {
|
|
373
|
+
const spans = template.templateSpans.map((span) => read(typescript, span.expression, bindings, depth + 1) + span.literal.text);
|
|
374
|
+
return template.head.text + spans.join("");
|
|
531
375
|
}
|
|
532
376
|
/**
|
|
533
|
-
*
|
|
377
|
+
* Reads one expression into a pattern.
|
|
534
378
|
*
|
|
535
|
-
* @param
|
|
536
|
-
* @param
|
|
537
|
-
* @
|
|
379
|
+
* @param typescript - The TypeScript module.
|
|
380
|
+
* @param node - The expression.
|
|
381
|
+
* @param bindings - The parameters bound by the call being read.
|
|
382
|
+
* @param depth - How deep the reading is.
|
|
383
|
+
* @returns The pattern, holes not yet merged.
|
|
538
384
|
*/
|
|
539
|
-
function
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const extracted = record.extracted?.result;
|
|
550
|
-
if (record.good === void 0 || extracted === void 0) {
|
|
551
|
-
unresolved.push({
|
|
552
|
-
path: file,
|
|
553
|
-
reason: record.error ?? "the file never parsed"
|
|
554
|
-
});
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
557
|
-
for (const { key, anchor } of extracted.definitions) entryOf(symbols, key).def.push(anchor);
|
|
558
|
-
unresolved.push(...extracted.unresolved);
|
|
559
|
-
for (const table of extracted.tables) addTable(assembly, symbols, unresolved, file, table);
|
|
560
|
-
}
|
|
561
|
-
addStyleUses(assembly, symbols);
|
|
562
|
-
return deepFreeze({
|
|
563
|
-
schemaVersion: 1,
|
|
564
|
-
revision: revisionOf(files),
|
|
565
|
-
...manifest === void 0 ? {} : { manifest },
|
|
566
|
-
symbols: finishSymbols(symbols),
|
|
567
|
-
files,
|
|
568
|
-
unresolved
|
|
569
|
-
});
|
|
385
|
+
function read(typescript, node, bindings, depth) {
|
|
386
|
+
const expression = unwrap(typescript, node);
|
|
387
|
+
if (depth > MAX_DEPTH) return "*";
|
|
388
|
+
if (typescript.isStringLiteralLike(expression) || typescript.isNumericLiteral(expression)) return expression.text;
|
|
389
|
+
if (typescript.isTemplateExpression(expression)) return readTemplate(typescript, expression, bindings, depth);
|
|
390
|
+
if (typescript.isBinaryExpression(expression) && expression.operatorToken.kind === typescript.SyntaxKind.PlusToken) return read(typescript, expression.left, bindings, depth + 1) + read(typescript, expression.right, bindings, depth + 1);
|
|
391
|
+
if (typescript.isIdentifier(expression)) return readName(typescript, expression, bindings, depth);
|
|
392
|
+
if (typescript.isPropertyAccessExpression(expression)) return readAccess(typescript, expression, bindings);
|
|
393
|
+
if (typescript.isCallExpression(expression)) return readCall$1(typescript, expression, bindings, depth);
|
|
394
|
+
return "*";
|
|
570
395
|
}
|
|
571
|
-
//#endregion
|
|
572
|
-
//#region src/project/extract/scope.ts
|
|
573
396
|
/**
|
|
574
|
-
*
|
|
397
|
+
* Reduces a key expression to its pattern, with neighbouring `*` holes merged into one.
|
|
575
398
|
*
|
|
576
399
|
* @param typescript - The TypeScript module.
|
|
577
|
-
* @param
|
|
578
|
-
* @returns The
|
|
400
|
+
* @param expression - The expression of a JSX `key` attribute.
|
|
401
|
+
* @returns The pattern: `card*Picture`, `{id}Close`, `{amountKey}`, `hudRow`, or `*` when nothing
|
|
402
|
+
* is literal.
|
|
579
403
|
*/
|
|
580
|
-
function
|
|
581
|
-
|
|
582
|
-
while (typescript.isParenthesizedExpression(inner) || typescript.isAsExpression(inner) || typescript.isSatisfiesExpression(inner) || typescript.isNonNullExpression(inner) || typescript.isTypeAssertionExpression(inner)) inner = inner.expression;
|
|
583
|
-
return inner;
|
|
404
|
+
function patternOf(typescript, expression) {
|
|
405
|
+
return read(typescript, expression, /* @__PURE__ */ new Map(), 0).replaceAll(/\*+/g, "*");
|
|
584
406
|
}
|
|
585
407
|
/**
|
|
586
|
-
*
|
|
408
|
+
* The literal head of a pattern: the text before its first hole.
|
|
587
409
|
*
|
|
588
|
-
* @param
|
|
589
|
-
* @returns
|
|
410
|
+
* @param pattern - A pattern.
|
|
411
|
+
* @returns The head, empty when the pattern starts with a hole.
|
|
412
|
+
* @example
|
|
413
|
+
* ```ts
|
|
414
|
+
* stemOf("card*Picture"); // "card"
|
|
415
|
+
* ```
|
|
590
416
|
*/
|
|
591
|
-
function
|
|
592
|
-
|
|
417
|
+
function stemOf(pattern) {
|
|
418
|
+
const holes = [pattern.indexOf("*"), pattern.search(HOLE)].filter((at) => at >= 0);
|
|
419
|
+
return pattern.slice(0, holes.length === 0 ? void 0 : Math.min(...holes));
|
|
593
420
|
}
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/project/extract/jsx.ts
|
|
423
|
+
/** The longest key expression an unresolved reason quotes. */
|
|
424
|
+
const QUOTE_LENGTH = 60;
|
|
425
|
+
/** A component tag: its name starts upper-case. */
|
|
426
|
+
const COMPONENT_TAG = /^[A-Z]/;
|
|
594
427
|
/**
|
|
595
|
-
*
|
|
428
|
+
* The expression a JSX attribute holds: the string of `key="x"` or the code of `key={x}`.
|
|
596
429
|
*
|
|
597
430
|
* @param typescript - The TypeScript module.
|
|
598
|
-
* @param
|
|
599
|
-
* @returns
|
|
431
|
+
* @param attribute - The attribute.
|
|
432
|
+
* @returns The expression, or `undefined` for `<a key />` and `key={}`.
|
|
600
433
|
*/
|
|
601
|
-
function
|
|
602
|
-
|
|
434
|
+
function attributeValue(typescript, attribute) {
|
|
435
|
+
const initializer = attribute.initializer;
|
|
436
|
+
if (initializer === void 0) return void 0;
|
|
437
|
+
if (typescript.isStringLiteral(initializer)) return initializer;
|
|
438
|
+
if (typescript.isJsxExpression(initializer)) return initializer.expression;
|
|
603
439
|
}
|
|
604
440
|
/**
|
|
605
|
-
*
|
|
441
|
+
* How a key expression is written.
|
|
606
442
|
*
|
|
607
443
|
* @param typescript - The TypeScript module.
|
|
608
|
-
* @param
|
|
609
|
-
* @returns
|
|
444
|
+
* @param expression - The key expression.
|
|
445
|
+
* @returns `literal` for a string, `template` for a template with holes, `ident` for the rest.
|
|
610
446
|
*/
|
|
611
|
-
function
|
|
612
|
-
const inner = unwrap(typescript,
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
node: inner
|
|
616
|
-
} : {
|
|
617
|
-
kind: "const",
|
|
618
|
-
initializer
|
|
619
|
-
};
|
|
447
|
+
function kindOf$1(typescript, expression) {
|
|
448
|
+
const inner = unwrap(typescript, expression);
|
|
449
|
+
if (typescript.isStringLiteralLike(inner)) return "literal";
|
|
450
|
+
return typescript.isTemplateExpression(inner) ? "template" : "ident";
|
|
620
451
|
}
|
|
621
452
|
/**
|
|
622
|
-
*
|
|
453
|
+
* The upper-case top-level declaration a node sits in: the component a `{prop}` pattern belongs to.
|
|
623
454
|
*
|
|
624
455
|
* @param typescript - The TypeScript module.
|
|
625
|
-
* @param
|
|
626
|
-
* @
|
|
627
|
-
* @returns What the name means there, or `undefined`.
|
|
456
|
+
* @param node - The attribute.
|
|
457
|
+
* @returns The component name, or `undefined` inside a lower-case helper.
|
|
628
458
|
*/
|
|
629
|
-
function
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
kind: "function",
|
|
633
|
-
node: statement
|
|
634
|
-
};
|
|
635
|
-
if (!typescript.isVariableStatement(statement)) continue;
|
|
636
|
-
const isConst = (statement.declarationList.flags & typescript.NodeFlags.Const) !== 0;
|
|
637
|
-
const declaration = statement.declarationList.declarations.find((item) => typescript.isIdentifier(item.name) && item.name.text === name);
|
|
638
|
-
if (isConst && declaration?.initializer !== void 0) return constMeaning(typescript, declaration.initializer);
|
|
639
|
-
}
|
|
459
|
+
function componentAround(typescript, node) {
|
|
460
|
+
const name = enclosingName(typescript, node);
|
|
461
|
+
return name !== void 0 && COMPONENT_TAG.test(name) ? name : void 0;
|
|
640
462
|
}
|
|
641
463
|
/**
|
|
642
|
-
*
|
|
464
|
+
* Quotes a key expression for an unresolved reason: one line, cut to a readable length.
|
|
643
465
|
*
|
|
644
|
-
* @param
|
|
645
|
-
* @
|
|
646
|
-
* @
|
|
647
|
-
*
|
|
466
|
+
* @param text - The expression as written.
|
|
467
|
+
* @returns The quote.
|
|
468
|
+
* @example
|
|
469
|
+
* ```ts
|
|
470
|
+
* quoteOf("card.key"); // "card.key"
|
|
471
|
+
* ```
|
|
648
472
|
*/
|
|
649
|
-
function
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const property = element.propertyName;
|
|
653
|
-
return {
|
|
654
|
-
kind: "param",
|
|
655
|
-
property: property !== void 0 && typescript.isIdentifier(property) ? property.text : name
|
|
656
|
-
};
|
|
657
|
-
}
|
|
473
|
+
function quoteOf(text) {
|
|
474
|
+
const line = text.replaceAll(/\s+/g, " ");
|
|
475
|
+
return line.length > QUOTE_LENGTH ? `${line.slice(0, QUOTE_LENGTH)}…` : line;
|
|
658
476
|
}
|
|
659
477
|
/**
|
|
660
|
-
*
|
|
661
|
-
* destructured one.
|
|
478
|
+
* The hit of a `key` attribute.
|
|
662
479
|
*
|
|
663
480
|
* @param typescript - The TypeScript module.
|
|
664
|
-
* @param
|
|
665
|
-
* @param
|
|
666
|
-
* @
|
|
481
|
+
* @param attribute - The attribute.
|
|
482
|
+
* @param place - The element and its tag.
|
|
483
|
+
* @param place.element - The whole element.
|
|
484
|
+
* @param place.tag - The tag as written.
|
|
485
|
+
* @returns The hit, or `undefined` for a key without a value.
|
|
667
486
|
*/
|
|
668
|
-
function
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
487
|
+
function keyHit(typescript, attribute, place) {
|
|
488
|
+
const expression = attributeValue(typescript, attribute);
|
|
489
|
+
if (expression === void 0) return void 0;
|
|
490
|
+
const kind = kindOf$1(typescript, expression);
|
|
491
|
+
const key = patternOf(typescript, expression);
|
|
492
|
+
const stem = kind === "literal" ? "" : stemOf(key);
|
|
493
|
+
const component = hasHole(key) ? componentAround(typescript, attribute) : void 0;
|
|
494
|
+
const written = quoteOf(expression.getText());
|
|
495
|
+
return {
|
|
496
|
+
key,
|
|
497
|
+
kind,
|
|
498
|
+
attribute,
|
|
499
|
+
...place,
|
|
500
|
+
written,
|
|
501
|
+
...stem === "" ? {} : { stem },
|
|
502
|
+
...component === void 0 ? {} : { component }
|
|
503
|
+
};
|
|
676
504
|
}
|
|
677
505
|
/**
|
|
678
|
-
*
|
|
506
|
+
* The hit of a literal key-carrying prop on a component: `id="settingsBoard"`,
|
|
507
|
+
* `amountKey="giftReward"`.
|
|
679
508
|
*
|
|
680
509
|
* @param typescript - The TypeScript module.
|
|
681
|
-
* @param
|
|
682
|
-
* @
|
|
510
|
+
* @param attribute - The attribute.
|
|
511
|
+
* @param property - The name of the attribute.
|
|
512
|
+
* @param place - The element and its tag.
|
|
513
|
+
* @param place.element - The whole element.
|
|
514
|
+
* @param place.tag - The tag as written.
|
|
515
|
+
* @returns The hit, or `undefined` when the value is not a string literal.
|
|
683
516
|
*/
|
|
684
|
-
function
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
517
|
+
function idHit(typescript, attribute, property, place) {
|
|
518
|
+
const expression = attributeValue(typescript, attribute);
|
|
519
|
+
const value = expression === void 0 ? void 0 : unwrap(typescript, expression);
|
|
520
|
+
if (value === void 0 || !typescript.isStringLiteralLike(value)) return void 0;
|
|
521
|
+
return {
|
|
522
|
+
key: value.text,
|
|
523
|
+
kind: "idProp",
|
|
524
|
+
component: place.tag,
|
|
525
|
+
prop: property,
|
|
526
|
+
attribute,
|
|
527
|
+
...place,
|
|
528
|
+
written: value.text
|
|
529
|
+
};
|
|
692
530
|
}
|
|
693
531
|
/**
|
|
694
|
-
* The
|
|
695
|
-
* the statement directly under the file.
|
|
532
|
+
* The hits of the attributes of one opening or self-closing element.
|
|
696
533
|
*
|
|
697
534
|
* @param typescript - The TypeScript module.
|
|
698
|
-
* @param
|
|
699
|
-
* @returns
|
|
535
|
+
* @param opening - The opening or self-closing element.
|
|
536
|
+
* @returns Its hits.
|
|
700
537
|
*/
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
538
|
+
function hitsOf(typescript, opening) {
|
|
539
|
+
const place = {
|
|
540
|
+
element: typescript.isJsxOpeningElement(opening) ? opening.parent : opening,
|
|
541
|
+
tag: opening.tagName.getText()
|
|
542
|
+
};
|
|
543
|
+
const isComponent = COMPONENT_TAG.test(place.tag);
|
|
544
|
+
const hits = [];
|
|
545
|
+
for (const attribute of opening.attributes.properties) {
|
|
546
|
+
if (!typescript.isJsxAttribute(attribute) || !typescript.isIdentifier(attribute.name)) continue;
|
|
547
|
+
const name = attribute.name.text;
|
|
548
|
+
const keyEntry = name === "key" ? keyHit(typescript, attribute, place) : void 0;
|
|
549
|
+
const idEntry = isComponent && KEY_PROP.test(name) ? idHit(typescript, attribute, name, place) : void 0;
|
|
550
|
+
if (keyEntry !== void 0) hits.push(keyEntry);
|
|
551
|
+
if (idEntry !== void 0) hits.push(idEntry);
|
|
552
|
+
}
|
|
553
|
+
return hits;
|
|
708
554
|
}
|
|
709
|
-
/** A hole filled by the `id` prop of the component, at run time. */
|
|
710
|
-
const ID = "{id}";
|
|
711
|
-
/** How deep names and calls are followed before a hole is given up on. */
|
|
712
|
-
const MAX_DEPTH = 8;
|
|
713
555
|
/**
|
|
714
|
-
*
|
|
556
|
+
* Collects every JSX key of a file, in source order.
|
|
715
557
|
*
|
|
716
558
|
* @param typescript - The TypeScript module.
|
|
717
|
-
* @param
|
|
718
|
-
* @returns The
|
|
559
|
+
* @param source - The parsed file.
|
|
560
|
+
* @returns The hits; a key that reads `*` is among them.
|
|
719
561
|
*/
|
|
720
|
-
function
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const collect = (child) => {
|
|
726
|
-
if (typescript.isReturnStatement(child)) returns.push(child);
|
|
727
|
-
if (!typescript.isFunctionLike(child)) typescript.forEachChild(child, collect);
|
|
562
|
+
function collectJsx(typescript, source) {
|
|
563
|
+
const hits = [];
|
|
564
|
+
const visit = (node) => {
|
|
565
|
+
if (typescript.isJsxOpeningElement(node) || typescript.isJsxSelfClosingElement(node)) hits.push(...hitsOf(typescript, node));
|
|
566
|
+
typescript.forEachChild(node, visit);
|
|
728
567
|
};
|
|
729
|
-
typescript.
|
|
730
|
-
return
|
|
568
|
+
if (source.languageVariant === typescript.LanguageVariant.JSX) visit(source);
|
|
569
|
+
return hits;
|
|
731
570
|
}
|
|
732
571
|
/**
|
|
733
|
-
*
|
|
572
|
+
* Whether a hit holds nothing literal: its key reads `*` alone.
|
|
734
573
|
*
|
|
735
|
-
* @param
|
|
736
|
-
* @
|
|
737
|
-
* @param bindings - The parameters bound by the call being read.
|
|
738
|
-
* @param depth - How deep the reading is.
|
|
739
|
-
* @returns The pattern.
|
|
574
|
+
* @param hit - A hit.
|
|
575
|
+
* @returns True for a key nothing in the file can read.
|
|
740
576
|
*/
|
|
741
|
-
function
|
|
742
|
-
|
|
743
|
-
if (bound !== void 0) return bound;
|
|
744
|
-
const meaning = meaningOf(typescript, identifier);
|
|
745
|
-
if (meaning?.kind === "const") return read(typescript, meaning.initializer, bindings, depth + 1);
|
|
746
|
-
if (meaning?.kind === "param" && meaning.property === "id") return ID;
|
|
747
|
-
return "*";
|
|
577
|
+
function isOpaque(hit) {
|
|
578
|
+
return hit.key === "*";
|
|
748
579
|
}
|
|
749
580
|
/**
|
|
750
|
-
*
|
|
581
|
+
* The anchor of a hit.
|
|
751
582
|
*
|
|
752
|
-
* @param
|
|
753
|
-
* @param
|
|
754
|
-
* @
|
|
755
|
-
* @returns The pattern.
|
|
583
|
+
* @param file - The root-relative path of the file.
|
|
584
|
+
* @param hit - The hit.
|
|
585
|
+
* @returns The anchor: path, key, kind, and the stem, component and prop when there are.
|
|
756
586
|
*/
|
|
757
|
-
function
|
|
758
|
-
|
|
759
|
-
|
|
587
|
+
function anchorOfHit(file, hit) {
|
|
588
|
+
return {
|
|
589
|
+
path: file,
|
|
590
|
+
key: hit.key,
|
|
591
|
+
kind: hit.kind,
|
|
592
|
+
...hit.stem === void 0 ? {} : { stem: hit.stem },
|
|
593
|
+
...hit.component === void 0 ? {} : { component: hit.component },
|
|
594
|
+
...hit.prop === void 0 ? {} : { prop: hit.prop }
|
|
595
|
+
};
|
|
760
596
|
}
|
|
761
597
|
/**
|
|
762
|
-
*
|
|
763
|
-
* to the pattern of its argument.
|
|
598
|
+
* The unresolved reason of a key that reads `*`.
|
|
764
599
|
*
|
|
765
|
-
* @param
|
|
766
|
-
* @
|
|
767
|
-
* @param bindings - The parameters bound by the call being read.
|
|
768
|
-
* @param depth - How deep the reading is.
|
|
769
|
-
* @returns The pattern.
|
|
600
|
+
* @param hit - The hit.
|
|
601
|
+
* @returns The reason: `JSX key "card.key" on <row> resolves to "*"`.
|
|
770
602
|
*/
|
|
771
|
-
function
|
|
772
|
-
|
|
773
|
-
const meaning = typescript.isIdentifier(callee) ? meaningOf(typescript, callee) : void 0;
|
|
774
|
-
if (meaning?.kind !== "function") return "*";
|
|
775
|
-
const returned = returnedBy(typescript, meaning.node);
|
|
776
|
-
if (returned === void 0) return "*";
|
|
777
|
-
const inner = /* @__PURE__ */ new Map();
|
|
778
|
-
for (const [index, parameter] of meaning.node.parameters.entries()) {
|
|
779
|
-
const argument = call.arguments[index];
|
|
780
|
-
if (!typescript.isIdentifier(parameter.name)) continue;
|
|
781
|
-
inner.set(parameter.name.text, argument === void 0 ? "*" : read(typescript, argument, bindings, depth + 1));
|
|
782
|
-
}
|
|
783
|
-
return read(typescript, returned, inner, depth + 1);
|
|
603
|
+
function opaqueReason(hit) {
|
|
604
|
+
return `JSX key "${hit.written}" on <${hit.tag}> resolves to "*"`;
|
|
784
605
|
}
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region src/project/extract/components.ts
|
|
608
|
+
/** The prefix of a component key. */
|
|
609
|
+
const COMPONENT = "component:";
|
|
610
|
+
/** The extension of the files whose plain functions can be components. */
|
|
611
|
+
const JSX_FILE = ".tsx";
|
|
785
612
|
/**
|
|
786
|
-
* Reads
|
|
613
|
+
* Reads the plain components of a file: the module-level upper-case functions of a `.tsx` file.
|
|
787
614
|
*
|
|
788
615
|
* @param typescript - The TypeScript module.
|
|
789
|
-
* @param
|
|
790
|
-
* @param
|
|
791
|
-
* @
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
616
|
+
* @param source - The parsed file.
|
|
617
|
+
* @param path - The root-relative path of the file.
|
|
618
|
+
* @returns One `component:<Name>` definition per component, bound to its name, in source order.
|
|
619
|
+
*/
|
|
620
|
+
function readComponents(typescript, source, path) {
|
|
621
|
+
if (!path.endsWith(JSX_FILE)) return [];
|
|
622
|
+
return source.statements.flatMap((statement) => declaredFunctions(typescript, statement).map((item) => item.name)).filter((name) => COMPONENT_TAG.test(name)).map((binding) => ({
|
|
623
|
+
key: `${COMPONENT}${binding}`,
|
|
624
|
+
anchor: {
|
|
625
|
+
path,
|
|
626
|
+
binding
|
|
627
|
+
}
|
|
628
|
+
}));
|
|
797
629
|
}
|
|
798
630
|
/**
|
|
799
|
-
*
|
|
631
|
+
* The component name a tag renders: an upper-case identifier, or the last segment of a member
|
|
632
|
+
* or namespaced tag. A lower-case identifier is an element of the engine, not a component.
|
|
800
633
|
*
|
|
801
634
|
* @param typescript - The TypeScript module.
|
|
802
|
-
* @param
|
|
803
|
-
* @
|
|
804
|
-
* @param depth - How deep the reading is.
|
|
805
|
-
* @returns The pattern, holes not yet merged.
|
|
635
|
+
* @param tag - The tag name of an element.
|
|
636
|
+
* @returns The name, or `undefined` for an element of the engine and for `this`.
|
|
806
637
|
*/
|
|
807
|
-
function
|
|
808
|
-
|
|
809
|
-
if (
|
|
810
|
-
if (typescript.isStringLiteralLike(expression) || typescript.isNumericLiteral(expression)) return expression.text;
|
|
811
|
-
if (typescript.isTemplateExpression(expression)) return readTemplate(typescript, expression, bindings, depth);
|
|
812
|
-
if (typescript.isBinaryExpression(expression) && expression.operatorToken.kind === typescript.SyntaxKind.PlusToken) return read(typescript, expression.left, bindings, depth + 1) + read(typescript, expression.right, bindings, depth + 1);
|
|
813
|
-
if (typescript.isIdentifier(expression)) return readName(typescript, expression, bindings, depth);
|
|
814
|
-
if (typescript.isPropertyAccessExpression(expression)) return readAccess(typescript, expression, bindings);
|
|
815
|
-
if (typescript.isCallExpression(expression)) return readCall$1(typescript, expression, bindings, depth);
|
|
816
|
-
return "*";
|
|
638
|
+
function renderedName(typescript, tag) {
|
|
639
|
+
if (typescript.isIdentifier(tag)) return COMPONENT_TAG.test(tag.text) ? tag.text : void 0;
|
|
640
|
+
if (typescript.isPropertyAccessExpression(tag) || typescript.isJsxNamespacedName(tag)) return tag.name.text;
|
|
817
641
|
}
|
|
818
642
|
/**
|
|
819
|
-
*
|
|
643
|
+
* The component names a file renders, each once.
|
|
820
644
|
*
|
|
821
645
|
* @param typescript - The TypeScript module.
|
|
822
|
-
* @param
|
|
823
|
-
* @returns The
|
|
646
|
+
* @param source - The parsed file.
|
|
647
|
+
* @returns The names, sorted; none for a file without JSX.
|
|
824
648
|
*/
|
|
825
|
-
function
|
|
826
|
-
|
|
649
|
+
function renderedOf(typescript, source) {
|
|
650
|
+
const names = /* @__PURE__ */ new Set();
|
|
651
|
+
const visit = (node) => {
|
|
652
|
+
const name = typescript.isJsxOpeningElement(node) || typescript.isJsxSelfClosingElement(node) ? renderedName(typescript, node.tagName) : void 0;
|
|
653
|
+
if (name !== void 0) names.add(name);
|
|
654
|
+
typescript.forEachChild(node, visit);
|
|
655
|
+
};
|
|
656
|
+
if (source.languageVariant === typescript.LanguageVariant.JSX) visit(source);
|
|
657
|
+
return [...names].toSorted();
|
|
827
658
|
}
|
|
659
|
+
/** The definers whose calls make keys. */
|
|
660
|
+
const DEFINERS = [
|
|
661
|
+
"defineFlow",
|
|
662
|
+
"defineFeature",
|
|
663
|
+
"defineScene",
|
|
664
|
+
"projection",
|
|
665
|
+
"defineEmitter",
|
|
666
|
+
"defineTextStyles",
|
|
667
|
+
"defineStyle",
|
|
668
|
+
"defineComponent"
|
|
669
|
+
];
|
|
828
670
|
/**
|
|
829
|
-
*
|
|
671
|
+
* Whether a name is a definer whose calls make keys.
|
|
830
672
|
*
|
|
831
|
-
* @param
|
|
832
|
-
* @returns
|
|
673
|
+
* @param name - A name.
|
|
674
|
+
* @returns True for the definers of `DEFINERS`.
|
|
833
675
|
* @example
|
|
834
676
|
* ```ts
|
|
835
|
-
*
|
|
677
|
+
* isDefiner("defineScene"); // true
|
|
836
678
|
* ```
|
|
837
679
|
*/
|
|
838
|
-
function
|
|
839
|
-
|
|
840
|
-
return pattern.slice(0, holes.length === 0 ? void 0 : Math.min(...holes));
|
|
680
|
+
function isDefiner(name) {
|
|
681
|
+
return DEFINERS.includes(name);
|
|
841
682
|
}
|
|
842
|
-
//#endregion
|
|
843
|
-
//#region src/project/extract/jsx.ts
|
|
844
|
-
/** The longest key expression an unresolved reason quotes. */
|
|
845
|
-
const QUOTE_LENGTH = 60;
|
|
846
|
-
/** A component tag: its name starts upper-case. */
|
|
847
|
-
const COMPONENT_TAG = /^[A-Z]/;
|
|
848
683
|
/**
|
|
849
|
-
*
|
|
684
|
+
* Whether a statement carries a modifier.
|
|
850
685
|
*
|
|
851
686
|
* @param typescript - The TypeScript module.
|
|
852
|
-
* @param
|
|
853
|
-
* @
|
|
687
|
+
* @param node - The statement.
|
|
688
|
+
* @param kind - `ExportKeyword` or `DefaultKeyword`.
|
|
689
|
+
* @returns True when the modifier is there.
|
|
854
690
|
*/
|
|
855
|
-
function
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
if (typescript.isStringLiteral(initializer)) return initializer;
|
|
859
|
-
if (typescript.isJsxExpression(initializer)) return initializer.expression;
|
|
691
|
+
function hasModifier(typescript, node, kind) {
|
|
692
|
+
if (!typescript.canHaveModifiers(node)) return false;
|
|
693
|
+
return typescript.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false;
|
|
860
694
|
}
|
|
861
695
|
/**
|
|
862
|
-
*
|
|
696
|
+
* The names a binding declares: the identifier, or every name of a destructure.
|
|
863
697
|
*
|
|
864
698
|
* @param typescript - The TypeScript module.
|
|
865
|
-
* @param
|
|
866
|
-
* @returns
|
|
699
|
+
* @param name - The binding name.
|
|
700
|
+
* @returns The declared names.
|
|
867
701
|
*/
|
|
868
|
-
function
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
return typescript.isTemplateExpression(inner) ? "template" : "ident";
|
|
702
|
+
function namesOf(typescript, name) {
|
|
703
|
+
if (typescript.isIdentifier(name)) return [name.text];
|
|
704
|
+
return name.elements.flatMap((element) => typescript.isBindingElement(element) ? namesOf(typescript, element.name) : []);
|
|
872
705
|
}
|
|
873
706
|
/**
|
|
874
|
-
*
|
|
707
|
+
* Records the value imports of one import statement.
|
|
875
708
|
*
|
|
876
709
|
* @param typescript - The TypeScript module.
|
|
877
|
-
* @param
|
|
878
|
-
* @
|
|
710
|
+
* @param statement - The import statement.
|
|
711
|
+
* @param draft - The facts so far.
|
|
879
712
|
*/
|
|
880
|
-
function
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
713
|
+
function readImport(typescript, statement, draft) {
|
|
714
|
+
const clause = statement.importClause;
|
|
715
|
+
if (clause === void 0 || clause.isTypeOnly) return;
|
|
716
|
+
if (!typescript.isStringLiteral(statement.moduleSpecifier)) return;
|
|
717
|
+
const specifier = statement.moduleSpecifier.text;
|
|
718
|
+
const bindings = clause.namedBindings;
|
|
719
|
+
if (clause.name !== void 0) draft.imports.set(clause.name.text, {
|
|
720
|
+
specifier,
|
|
721
|
+
imported: "default"
|
|
722
|
+
});
|
|
723
|
+
if (bindings === void 0) return;
|
|
724
|
+
if (typescript.isNamespaceImport(bindings)) {
|
|
725
|
+
draft.imports.set(bindings.name.text, {
|
|
726
|
+
specifier,
|
|
727
|
+
imported: "*"
|
|
728
|
+
});
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
for (const element of bindings.elements) {
|
|
732
|
+
if (element.isTypeOnly) continue;
|
|
733
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
734
|
+
draft.imports.set(element.name.text, {
|
|
735
|
+
specifier,
|
|
736
|
+
imported
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Records one export statement: a re-export from another module, a star, or a list of local
|
|
742
|
+
* bindings.
|
|
743
|
+
*
|
|
744
|
+
* @param typescript - The TypeScript module.
|
|
745
|
+
* @param statement - The export statement.
|
|
746
|
+
* @param draft - The facts so far.
|
|
747
|
+
*/
|
|
748
|
+
function readExport(typescript, statement, draft) {
|
|
749
|
+
const source = statement.moduleSpecifier;
|
|
750
|
+
const specifier = source !== void 0 && typescript.isStringLiteral(source) ? source.text : "";
|
|
751
|
+
const clause = statement.exportClause;
|
|
752
|
+
if (statement.isTypeOnly) return;
|
|
753
|
+
if (clause === void 0) {
|
|
754
|
+
if (specifier !== "") draft.stars.push(specifier);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (typescript.isNamespaceExport(clause)) {
|
|
758
|
+
draft.exports.set(clause.name.text, {
|
|
759
|
+
specifier,
|
|
760
|
+
imported: "*"
|
|
761
|
+
});
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
for (const element of clause.elements) {
|
|
765
|
+
if (element.isTypeOnly) continue;
|
|
766
|
+
const inner = element.propertyName?.text ?? element.name.text;
|
|
767
|
+
const target = specifier === "" ? { local: inner } : {
|
|
768
|
+
specifier,
|
|
769
|
+
imported: inner
|
|
770
|
+
};
|
|
771
|
+
draft.exports.set(element.name.text, target);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Whether an import brings a name of the engine package.
|
|
776
|
+
*
|
|
777
|
+
* @param ref - The import, if any.
|
|
778
|
+
* @param name - The name taken from the package, `*` for a namespace.
|
|
779
|
+
* @returns True for that import of `@moku-labs/game`.
|
|
780
|
+
* @example
|
|
781
|
+
* ```ts
|
|
782
|
+
* isGameImport({ specifier: "@moku-labs/game", imported: "defineGame" }, "defineGame"); // true
|
|
783
|
+
* ```
|
|
784
|
+
*/
|
|
785
|
+
function isGameImport(ref, name) {
|
|
786
|
+
return ref?.specifier === "@moku-labs/game" && ref.imported === name;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Whether an expression calls `defineGame` of the engine: by a named import, renamed or not, or
|
|
790
|
+
* through a namespace import of the package.
|
|
791
|
+
*
|
|
792
|
+
* @param typescript - The TypeScript module.
|
|
793
|
+
* @param expression - The initializer of a destructure.
|
|
794
|
+
* @param imports - The value imports of the file.
|
|
795
|
+
* @returns True for the kit call.
|
|
796
|
+
*/
|
|
797
|
+
function callsDefineGame(typescript, expression, imports) {
|
|
798
|
+
if (expression === void 0 || !typescript.isCallExpression(expression)) return false;
|
|
799
|
+
const callee = expression.expression;
|
|
800
|
+
if (typescript.isIdentifier(callee)) return isGameImport(imports.get(callee.text), "defineGame");
|
|
801
|
+
return typescript.isPropertyAccessExpression(callee) && typescript.isIdentifier(callee.expression) && callee.name.text === "defineGame" && isGameImport(imports.get(callee.expression.text), "*");
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Records the definers a `defineGame` destructure hands out, under their local names.
|
|
805
|
+
*
|
|
806
|
+
* @param typescript - The TypeScript module.
|
|
807
|
+
* @param pattern - The destructure.
|
|
808
|
+
* @param draft - The facts so far.
|
|
809
|
+
*/
|
|
810
|
+
function readKit(typescript, pattern, draft) {
|
|
811
|
+
for (const element of pattern.elements) {
|
|
812
|
+
if (!typescript.isIdentifier(element.name)) continue;
|
|
813
|
+
const property = element.propertyName;
|
|
814
|
+
const handed = property !== void 0 && typescript.isIdentifier(property) ? property.text : element.name.text;
|
|
815
|
+
if (isDefiner(handed)) draft.kit.set(element.name.text, handed);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Records one top-level variable statement: its names, its exports and a kit destructure.
|
|
820
|
+
*
|
|
821
|
+
* @param typescript - The TypeScript module.
|
|
822
|
+
* @param statement - The statement.
|
|
823
|
+
* @param draft - The facts so far.
|
|
824
|
+
*/
|
|
825
|
+
function readVariables(typescript, statement, draft) {
|
|
826
|
+
const exported = hasModifier(typescript, statement, typescript.SyntaxKind.ExportKeyword);
|
|
827
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
828
|
+
for (const name of namesOf(typescript, declaration.name)) {
|
|
829
|
+
draft.declared.add(name);
|
|
830
|
+
if (exported) draft.exports.set(name, { local: name });
|
|
831
|
+
}
|
|
832
|
+
const pattern = declaration.name;
|
|
833
|
+
if (typescript.isObjectBindingPattern(pattern) && callsDefineGame(typescript, declaration.initializer, draft.imports)) readKit(typescript, pattern, draft);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Records a top-level function or class: its name, and its export.
|
|
838
|
+
*
|
|
839
|
+
* @param typescript - The TypeScript module.
|
|
840
|
+
* @param statement - The declaration.
|
|
841
|
+
* @param draft - The facts so far.
|
|
842
|
+
*/
|
|
843
|
+
function readNamed(typescript, statement, draft) {
|
|
844
|
+
const name = statement.name?.text;
|
|
845
|
+
if (name === void 0) return;
|
|
846
|
+
draft.declared.add(name);
|
|
847
|
+
if (!hasModifier(typescript, statement, typescript.SyntaxKind.ExportKeyword)) return;
|
|
848
|
+
const isDefault = hasModifier(typescript, statement, typescript.SyntaxKind.DefaultKeyword);
|
|
849
|
+
draft.exports.set(isDefault ? "default" : name, { local: name });
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Reads the module facts of one parsed file. Imports come first, so a kit destructure anywhere
|
|
853
|
+
* in the file knows where its `defineGame` comes from.
|
|
854
|
+
*
|
|
855
|
+
* @param typescript - The TypeScript module.
|
|
856
|
+
* @param source - The parsed file.
|
|
857
|
+
* @returns The facts.
|
|
858
|
+
*/
|
|
859
|
+
function readModule(typescript, source) {
|
|
860
|
+
const draft = {
|
|
861
|
+
imports: /* @__PURE__ */ new Map(),
|
|
862
|
+
exports: /* @__PURE__ */ new Map(),
|
|
863
|
+
stars: [],
|
|
864
|
+
declared: /* @__PURE__ */ new Set(),
|
|
865
|
+
kit: /* @__PURE__ */ new Map()
|
|
866
|
+
};
|
|
867
|
+
for (const statement of source.statements) if (typescript.isImportDeclaration(statement)) readImport(typescript, statement, draft);
|
|
868
|
+
for (const statement of source.statements) {
|
|
869
|
+
if (typescript.isVariableStatement(statement)) readVariables(typescript, statement, draft);
|
|
870
|
+
if (typescript.isFunctionDeclaration(statement) || typescript.isClassDeclaration(statement)) readNamed(typescript, statement, draft);
|
|
871
|
+
if (typescript.isExportDeclaration(statement)) readExport(typescript, statement, draft);
|
|
872
|
+
if (typescript.isExportAssignment(statement) && typescript.isIdentifier(statement.expression)) draft.exports.set("default", { local: statement.expression.text });
|
|
873
|
+
}
|
|
874
|
+
return draft;
|
|
875
|
+
}
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/project/extract/resolve.ts
|
|
878
|
+
/**
|
|
879
|
+
* @file project, extract — following a name across files with the module facts only: an import
|
|
880
|
+
* to the module it names, a re-export to the module behind it, a star export to the first module
|
|
881
|
+
* that has the name, until the name is declared, handed out by the kit, or comes from a package.
|
|
882
|
+
* This is how a definer is recognised by binding (a renamed import still is the definer) and how
|
|
883
|
+
* a node of a flow table reaches the file that declares it.
|
|
884
|
+
*/
|
|
885
|
+
/** How many imports and re-exports a name is followed through before giving up. */
|
|
886
|
+
const MAX_HOPS = 12;
|
|
887
|
+
/** The extensions a TypeScript import may name instead of the source file. */
|
|
888
|
+
const SCRIPT_EXTENSION = /\.(?:[cm]?js|jsx)$/;
|
|
889
|
+
/**
|
|
890
|
+
* Whether a module specifier is relative to the importing file.
|
|
891
|
+
*
|
|
892
|
+
* @param specifier - The module specifier.
|
|
893
|
+
* @returns True for `./…` and `../…`.
|
|
894
|
+
* @example
|
|
895
|
+
* ```ts
|
|
896
|
+
* isRelative("../kit"); // true
|
|
897
|
+
* ```
|
|
898
|
+
*/
|
|
899
|
+
function isRelative(specifier) {
|
|
900
|
+
return specifier === "." || specifier === ".." || specifier.startsWith("./") || specifier.startsWith("../");
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Resolves a relative module specifier to an indexed file, the way a bundler does: the path as
|
|
904
|
+
* written, then with `.ts`, `.tsx`, `/index.ts` and `/index.tsx`.
|
|
905
|
+
*
|
|
906
|
+
* @param modules - The module facts by path.
|
|
907
|
+
* @param from - The importing file.
|
|
908
|
+
* @param specifier - The module specifier.
|
|
909
|
+
* @returns The root-relative path, or `undefined` for a package or a file the index does not hold.
|
|
910
|
+
*/
|
|
911
|
+
function resolveModulePath(modules, from, specifier) {
|
|
912
|
+
if (!isRelative(specifier)) return void 0;
|
|
913
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(from), specifier));
|
|
914
|
+
const stem = base.replace(SCRIPT_EXTENSION, "");
|
|
915
|
+
return [
|
|
916
|
+
base,
|
|
917
|
+
`${stem}.ts`,
|
|
918
|
+
`${stem}.tsx`,
|
|
919
|
+
`${stem}/index.ts`,
|
|
920
|
+
`${stem}/index.tsx`
|
|
921
|
+
].find((candidate) => modules.has(candidate));
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Follows an imported name into the module that the specifier names.
|
|
925
|
+
*
|
|
926
|
+
* @param modules - The module facts by path.
|
|
927
|
+
* @param from - The importing file.
|
|
928
|
+
* @param ref - The specifier and the name.
|
|
929
|
+
* @param hops - How many hops were taken so far.
|
|
930
|
+
* @returns Where the name ends up.
|
|
931
|
+
*/
|
|
932
|
+
function followImport(modules, from, ref, hops) {
|
|
933
|
+
if (!isRelative(ref.specifier)) return {
|
|
934
|
+
kind: "package",
|
|
935
|
+
...ref
|
|
936
|
+
};
|
|
937
|
+
if (ref.imported === "*") return void 0;
|
|
938
|
+
const target = resolveModulePath(modules, from, ref.specifier);
|
|
939
|
+
return target === void 0 ? void 0 : resolveExport(modules, target, ref.imported, hops + 1);
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Follows an exported name of a module: a local binding, a re-export, or a star export.
|
|
943
|
+
*
|
|
944
|
+
* @param modules - The module facts by path.
|
|
945
|
+
* @param file - The module.
|
|
946
|
+
* @param name - The exported name.
|
|
947
|
+
* @param hops - How many hops were taken so far.
|
|
948
|
+
* @returns Where the name ends up.
|
|
949
|
+
*/
|
|
950
|
+
function resolveExport(modules, file, name, hops = 0) {
|
|
951
|
+
const facts = modules.get(file);
|
|
952
|
+
if (facts === void 0 || hops > MAX_HOPS) return void 0;
|
|
953
|
+
const ref = facts.exports.get(name);
|
|
954
|
+
if (ref !== void 0 && "local" in ref) return resolveBinding(modules, file, ref.local, hops + 1);
|
|
955
|
+
if (ref !== void 0) return followImport(modules, file, ref, hops);
|
|
956
|
+
for (const specifier of facts.stars) {
|
|
957
|
+
const found = followImport(modules, file, {
|
|
958
|
+
specifier,
|
|
959
|
+
imported: name
|
|
960
|
+
}, hops);
|
|
961
|
+
if (found !== void 0) return found;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Follows a name used in a file to where it ends up: the kit hands it out, the file declares it,
|
|
966
|
+
* or an import brings it.
|
|
967
|
+
*
|
|
968
|
+
* @param modules - The module facts by path.
|
|
969
|
+
* @param file - The file that uses the name.
|
|
970
|
+
* @param local - The name as the file writes it.
|
|
971
|
+
* @param hops - How many hops were taken so far.
|
|
972
|
+
* @returns Where the name ends up, or `undefined` when it cannot be followed.
|
|
973
|
+
*/
|
|
974
|
+
function resolveBinding(modules, file, local, hops = 0) {
|
|
975
|
+
const facts = modules.get(file);
|
|
976
|
+
if (facts === void 0 || hops > MAX_HOPS) return void 0;
|
|
977
|
+
const definer = facts.kit.get(local);
|
|
978
|
+
if (definer !== void 0) return {
|
|
979
|
+
kind: "definer",
|
|
980
|
+
definer
|
|
981
|
+
};
|
|
982
|
+
if (facts.declared.has(local)) return {
|
|
983
|
+
kind: "declared",
|
|
984
|
+
path: file,
|
|
985
|
+
binding: local
|
|
986
|
+
};
|
|
987
|
+
const ref = facts.imports.get(local);
|
|
988
|
+
return ref === void 0 ? void 0 : followImport(modules, file, ref, hops);
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* The definer a target is, if any: one the kit hands out, or one imported from the engine.
|
|
992
|
+
*
|
|
993
|
+
* @param target - Where a name ends up.
|
|
994
|
+
* @returns The definer, or `undefined`.
|
|
995
|
+
* @example
|
|
996
|
+
* ```ts
|
|
997
|
+
* definerOf({ kind: "package", specifier: "@moku-labs/game", imported: "defineStyle" }); // "defineStyle"
|
|
998
|
+
* ```
|
|
999
|
+
*/
|
|
1000
|
+
function definerOf(target) {
|
|
1001
|
+
if (target?.kind === "definer") return target.definer;
|
|
1002
|
+
if (target?.kind !== "package" || target.specifier !== "@moku-labs/game") return void 0;
|
|
1003
|
+
return isDefiner(target.imported) ? target.imported : void 0;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Adds the definers a namespace import reaches as `ns.name`.
|
|
1007
|
+
*
|
|
1008
|
+
* @param modules - The module facts by path.
|
|
1009
|
+
* @param file - The importing file.
|
|
1010
|
+
* @param namespace - The local name of the namespace.
|
|
1011
|
+
* @param specifier - The module it imports.
|
|
1012
|
+
* @param definers - The map to add to.
|
|
1013
|
+
*/
|
|
1014
|
+
function addNamespace(modules, file, namespace, specifier, definers) {
|
|
1015
|
+
if (specifier === "@moku-labs/game") {
|
|
1016
|
+
for (const definer of DEFINERS) definers.set(`${namespace}.${definer}`, definer);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const target = resolveModulePath(modules, file, specifier);
|
|
1020
|
+
const names = target === void 0 ? [] : [...modules.get(target)?.exports.keys() ?? []];
|
|
1021
|
+
for (const name of names) {
|
|
1022
|
+
const definer = definerOf(resolveExport(modules, target ?? "", name));
|
|
1023
|
+
if (definer !== void 0) definers.set(`${namespace}.${name}`, definer);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* The definers a file can call, by the callee as the file writes it: `defineFlow`, a renamed
|
|
1028
|
+
* `scene`, or `kit.defineScene` through a namespace.
|
|
1029
|
+
*
|
|
1030
|
+
* @param modules - The module facts by path.
|
|
1031
|
+
* @param file - The file.
|
|
1032
|
+
* @returns The callee text to the definer.
|
|
1033
|
+
*/
|
|
1034
|
+
function definersOf(modules, file) {
|
|
1035
|
+
const facts = modules.get(file);
|
|
1036
|
+
const definers = new Map(facts?.kit);
|
|
1037
|
+
for (const [local, ref] of facts?.imports ?? []) {
|
|
1038
|
+
if (ref.imported === "*") {
|
|
1039
|
+
addNamespace(modules, file, local, ref.specifier, definers);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
const definer = definerOf(resolveBinding(modules, file, local));
|
|
1043
|
+
if (definer !== void 0) definers.set(local, definer);
|
|
1044
|
+
}
|
|
1045
|
+
return definers;
|
|
1046
|
+
}
|
|
1047
|
+
//#endregion
|
|
1048
|
+
//#region src/project/hash.ts
|
|
1049
|
+
/**
|
|
1050
|
+
* @file project — the two hashes of the index: the sha1 of a file's bytes, the same value the
|
|
1051
|
+
* editor uses as a file version, and the revision over every file of the index.
|
|
1052
|
+
*/
|
|
1053
|
+
/**
|
|
1054
|
+
* Hashes bytes or text with sha1.
|
|
1055
|
+
*
|
|
1056
|
+
* @param bytes - The file bytes, or a text.
|
|
1057
|
+
* @returns The hex digest.
|
|
1058
|
+
* @example
|
|
1059
|
+
* ```ts
|
|
1060
|
+
* sha1(""); // "da39a3ee5e6b4b0d3255bfef95601890afd80709"
|
|
1061
|
+
* ```
|
|
1062
|
+
*/
|
|
1063
|
+
function sha1(bytes) {
|
|
1064
|
+
return createHash("sha1").update(bytes).digest("hex");
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* The revision of an index: the sha1 of the lines `${path}\0${hash}\n`, sorted by path.
|
|
1068
|
+
*
|
|
1069
|
+
* @param files - Every indexed file with its hash.
|
|
1070
|
+
* @returns The hex digest.
|
|
1071
|
+
* @example
|
|
1072
|
+
* ```ts
|
|
1073
|
+
* revisionOf({}); // "da39a3ee5e6b4b0d3255bfef95601890afd80709"
|
|
1074
|
+
* ```
|
|
1075
|
+
*/
|
|
1076
|
+
function revisionOf(files) {
|
|
1077
|
+
return sha1(Object.entries(files).map(([file, entry]) => `${file}\0${entry.hash}\n`).toSorted().join(""));
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* The anchors of the JSX patterns and of the literal key-carrying props of an index.
|
|
1081
|
+
*
|
|
1082
|
+
* @param index - The index.
|
|
1083
|
+
* @returns The prop-hole patterns, the `*` patterns and the `idProp` anchors.
|
|
1084
|
+
*/
|
|
1085
|
+
function jsxShapes(index) {
|
|
1086
|
+
const shapes = {
|
|
1087
|
+
holePatterns: [],
|
|
1088
|
+
wildPatterns: [],
|
|
1089
|
+
idProps: []
|
|
1090
|
+
};
|
|
1091
|
+
for (const [key, entry] of Object.entries(index.symbols)) {
|
|
1092
|
+
if (!key.startsWith("jsx:")) continue;
|
|
1093
|
+
for (const anchor of entry.def) {
|
|
1094
|
+
const pattern = anchor.key ?? "";
|
|
1095
|
+
if (anchor.kind === "idProp") shapes.idProps.push(anchor);
|
|
1096
|
+
else if (hasHole(pattern)) shapes.holePatterns.push(anchor);
|
|
1097
|
+
else if (pattern.includes("*")) shapes.wildPatterns.push(anchor);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return shapes;
|
|
1101
|
+
}
|
|
1102
|
+
//#endregion
|
|
1103
|
+
//#region src/project/assemble.ts
|
|
1104
|
+
/** The prefix of a node key. */
|
|
1105
|
+
const NODE = "node:";
|
|
1106
|
+
/**
|
|
1107
|
+
* The entry of a key, made on first use.
|
|
1108
|
+
*
|
|
1109
|
+
* @param symbols - The keys so far.
|
|
1110
|
+
* @param key - The key.
|
|
1111
|
+
* @returns Its entry.
|
|
1112
|
+
*/
|
|
1113
|
+
function entryOf(symbols, key) {
|
|
1114
|
+
const existing = symbols.get(key);
|
|
1115
|
+
if (existing !== void 0) return existing;
|
|
1116
|
+
const entry = {
|
|
1117
|
+
def: [],
|
|
1118
|
+
uses: []
|
|
1119
|
+
};
|
|
1120
|
+
symbols.set(key, entry);
|
|
1121
|
+
return entry;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Adds the `node:` keys of one flow table. A plain name is followed to the file that declares
|
|
1125
|
+
* it, and the table is its use; a slot or an inline node is anchored at the table. A name that
|
|
1126
|
+
* cannot be followed is anchored at the table and listed as unresolved.
|
|
1127
|
+
*
|
|
1128
|
+
* @param assembly - The records and module facts.
|
|
1129
|
+
* @param symbols - The keys so far.
|
|
1130
|
+
* @param unresolved - The unresolved list so far.
|
|
1131
|
+
* @param file - The flow file.
|
|
1132
|
+
* @param table - The table.
|
|
1133
|
+
*/
|
|
1134
|
+
function addTable(assembly, symbols, unresolved, file, table) {
|
|
1135
|
+
const use = table.binding === void 0 ? { path: file } : {
|
|
1136
|
+
path: file,
|
|
1137
|
+
binding: table.binding
|
|
1138
|
+
};
|
|
1139
|
+
for (const { name, ref } of table.entries) {
|
|
1140
|
+
const entry = entryOf(symbols, `${NODE}${table.flow}/${name}`);
|
|
1141
|
+
const atTable = {
|
|
1142
|
+
...use,
|
|
1143
|
+
key: name
|
|
1144
|
+
};
|
|
1145
|
+
const target = ref === void 0 ? void 0 : resolveBinding(assembly.modules, file, ref);
|
|
1146
|
+
if (target?.kind === "declared") {
|
|
1147
|
+
entry.def.push({
|
|
1148
|
+
path: target.path,
|
|
1149
|
+
binding: target.binding
|
|
1150
|
+
});
|
|
1151
|
+
entry.uses.push(atTable);
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
entry.def.push(atTable);
|
|
1155
|
+
if (ref !== void 0) {
|
|
1156
|
+
const reason = `node "${table.flow}/${name}": "${ref}" is not declared in a file of the root`;
|
|
1157
|
+
unresolved.push({
|
|
1158
|
+
path: file,
|
|
1159
|
+
reason
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Adds, to every style key, the files that import its binding by name.
|
|
1166
|
+
*
|
|
1167
|
+
* @param assembly - The records and module facts.
|
|
1168
|
+
* @param symbols - The keys so far.
|
|
893
1169
|
*/
|
|
894
|
-
function
|
|
895
|
-
const
|
|
896
|
-
|
|
1170
|
+
function addStyleUses(assembly, symbols) {
|
|
1171
|
+
for (const [file, record] of assembly.files) for (const [local, ref] of record.good?.module.imports ?? []) {
|
|
1172
|
+
const target = ref.imported === "*" ? void 0 : resolveBinding(assembly.modules, file, local);
|
|
1173
|
+
(target?.kind === "declared" ? symbols.get(`style:${target.path}#${target.binding}`) : void 0)?.uses.push({
|
|
1174
|
+
path: file,
|
|
1175
|
+
binding: local
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
897
1178
|
}
|
|
898
1179
|
/**
|
|
899
|
-
* The
|
|
1180
|
+
* The component entries by the binding they are rendered as. Two anchors of one name in a
|
|
1181
|
+
* conflict share one binding, so a file that renders it is one use.
|
|
900
1182
|
*
|
|
901
|
-
* @param
|
|
902
|
-
* @
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const key = patternOf(typescript, expression);
|
|
913
|
-
const stem = kind === "literal" ? "" : stemOf(key);
|
|
914
|
-
const component = key.includes("{id}") ? componentAround(typescript, attribute) : void 0;
|
|
915
|
-
const written = quoteOf(expression.getText());
|
|
916
|
-
return {
|
|
917
|
-
key,
|
|
918
|
-
kind,
|
|
919
|
-
attribute,
|
|
920
|
-
...place,
|
|
921
|
-
written,
|
|
922
|
-
...stem === "" ? {} : { stem },
|
|
923
|
-
...component === void 0 ? {} : { component }
|
|
924
|
-
};
|
|
1183
|
+
* @param symbols - The keys so far.
|
|
1184
|
+
* @returns Binding to the component entries bound to it.
|
|
1185
|
+
*/
|
|
1186
|
+
function componentsByBinding(symbols) {
|
|
1187
|
+
const byBinding = /* @__PURE__ */ new Map();
|
|
1188
|
+
for (const [key, entry] of symbols) {
|
|
1189
|
+
if (!key.startsWith("component:")) continue;
|
|
1190
|
+
const bindings = new Set(entry.def.flatMap((anchor) => anchor.binding ?? []));
|
|
1191
|
+
for (const binding of bindings) byBinding.set(binding, [...byBinding.get(binding) ?? [], entry]);
|
|
1192
|
+
}
|
|
1193
|
+
return byBinding;
|
|
925
1194
|
}
|
|
926
1195
|
/**
|
|
927
|
-
* The
|
|
1196
|
+
* The component names one file renders.
|
|
928
1197
|
*
|
|
929
|
-
* @param
|
|
930
|
-
* @
|
|
931
|
-
* @param place - The element and its tag.
|
|
932
|
-
* @param place.element - The whole element.
|
|
933
|
-
* @param place.tag - The tag as written.
|
|
934
|
-
* @returns The hit, or `undefined` when the value is not a string literal.
|
|
1198
|
+
* @param record - The record of the file.
|
|
1199
|
+
* @returns The names; none for a broken file.
|
|
935
1200
|
*/
|
|
936
|
-
function
|
|
937
|
-
|
|
938
|
-
const value = expression === void 0 ? void 0 : unwrap(typescript, expression);
|
|
939
|
-
if (value === void 0 || !typescript.isStringLiteralLike(value)) return void 0;
|
|
940
|
-
return {
|
|
941
|
-
key: value.text,
|
|
942
|
-
kind: "idProp",
|
|
943
|
-
component: place.tag,
|
|
944
|
-
attribute,
|
|
945
|
-
...place,
|
|
946
|
-
written: value.text
|
|
947
|
-
};
|
|
1201
|
+
function renderedBy(record) {
|
|
1202
|
+
return record.good === void 0 ? [] : record.extracted?.result.rendered ?? [];
|
|
948
1203
|
}
|
|
949
1204
|
/**
|
|
950
|
-
*
|
|
1205
|
+
* Adds one use of a rendered binding to every component entry bound to it.
|
|
951
1206
|
*
|
|
952
|
-
* @param
|
|
953
|
-
* @param
|
|
954
|
-
* @
|
|
1207
|
+
* @param byBinding - Binding to the component entries bound to it.
|
|
1208
|
+
* @param file - The file that renders the binding.
|
|
1209
|
+
* @param binding - The rendered name.
|
|
955
1210
|
*/
|
|
956
|
-
function
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
};
|
|
961
|
-
const isComponent = COMPONENT_TAG.test(place.tag);
|
|
962
|
-
const hits = [];
|
|
963
|
-
for (const attribute of opening.attributes.properties) {
|
|
964
|
-
if (!typescript.isJsxAttribute(attribute) || !typescript.isIdentifier(attribute.name)) continue;
|
|
965
|
-
const name = attribute.name.text;
|
|
966
|
-
const keyEntry = name === "key" ? keyHit(typescript, attribute, place) : void 0;
|
|
967
|
-
const idEntry = name === "id" && isComponent ? idHit(typescript, attribute, place) : void 0;
|
|
968
|
-
if (keyEntry !== void 0) hits.push(keyEntry);
|
|
969
|
-
if (idEntry !== void 0) hits.push(idEntry);
|
|
970
|
-
}
|
|
971
|
-
return hits;
|
|
1211
|
+
function useComponent(byBinding, file, binding) {
|
|
1212
|
+
for (const entry of byBinding.get(binding) ?? []) entry.uses.push({
|
|
1213
|
+
path: file,
|
|
1214
|
+
binding
|
|
1215
|
+
});
|
|
972
1216
|
}
|
|
973
1217
|
/**
|
|
974
|
-
*
|
|
1218
|
+
* Adds, to every component key, the files that render its binding: one use per file and binding,
|
|
1219
|
+
* in path order. A member tag `<ui.RoundButton>` renders `RoundButton`.
|
|
975
1220
|
*
|
|
976
|
-
* @param
|
|
977
|
-
* @param
|
|
978
|
-
* @returns The hits; a key that reads `*` is among them.
|
|
1221
|
+
* @param assembly - The records and module facts.
|
|
1222
|
+
* @param symbols - The keys so far.
|
|
979
1223
|
*/
|
|
980
|
-
function
|
|
981
|
-
const
|
|
982
|
-
const
|
|
983
|
-
if (typescript.isJsxOpeningElement(node) || typescript.isJsxSelfClosingElement(node)) hits.push(...hitsOf(typescript, node));
|
|
984
|
-
typescript.forEachChild(node, visit);
|
|
985
|
-
};
|
|
986
|
-
if (source.languageVariant === typescript.LanguageVariant.JSX) visit(source);
|
|
987
|
-
return hits;
|
|
1224
|
+
function addComponentUses(assembly, symbols) {
|
|
1225
|
+
const byBinding = componentsByBinding(symbols);
|
|
1226
|
+
for (const [file, record] of assembly.files) for (const binding of renderedBy(record)) useComponent(byBinding, file, binding);
|
|
988
1227
|
}
|
|
989
1228
|
/**
|
|
990
|
-
*
|
|
1229
|
+
* The symbols as the index holds them: sorted by key, `uses` only when there are some, and
|
|
1230
|
+
* `conflict` on a key other than `jsx:` defined twice.
|
|
991
1231
|
*
|
|
992
|
-
* @param
|
|
993
|
-
* @returns
|
|
1232
|
+
* @param symbols - The keys as collected.
|
|
1233
|
+
* @returns The symbols of the index.
|
|
994
1234
|
*/
|
|
995
|
-
function
|
|
996
|
-
|
|
1235
|
+
function finishSymbols(symbols) {
|
|
1236
|
+
const finished = {};
|
|
1237
|
+
const sorted = [...symbols].toSorted(([first], [second]) => first < second ? -1 : 1);
|
|
1238
|
+
for (const [key, { def, uses }] of sorted) {
|
|
1239
|
+
const isConflict = !key.startsWith("jsx:") && def.length > 1;
|
|
1240
|
+
finished[key] = {
|
|
1241
|
+
def,
|
|
1242
|
+
...uses.length === 0 ? {} : { uses },
|
|
1243
|
+
...isConflict ? { conflict: true } : {}
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
return finished;
|
|
997
1247
|
}
|
|
998
1248
|
/**
|
|
999
|
-
*
|
|
1249
|
+
* Freezes a value and everything it holds.
|
|
1000
1250
|
*
|
|
1001
|
-
* @param
|
|
1002
|
-
* @
|
|
1003
|
-
* @returns The anchor: path, key, kind, and the stem and component when there are.
|
|
1251
|
+
* @param value - A plain value.
|
|
1252
|
+
* @returns The same value, frozen.
|
|
1004
1253
|
*/
|
|
1005
|
-
function
|
|
1006
|
-
return
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
...hit.stem === void 0 ? {} : { stem: hit.stem },
|
|
1011
|
-
...hit.component === void 0 ? {} : { component: hit.component }
|
|
1012
|
-
};
|
|
1254
|
+
function deepFreeze(value) {
|
|
1255
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
|
|
1256
|
+
Object.freeze(value);
|
|
1257
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
1258
|
+
return value;
|
|
1013
1259
|
}
|
|
1014
1260
|
/**
|
|
1015
|
-
*
|
|
1261
|
+
* Assembles the index of a catalog.
|
|
1016
1262
|
*
|
|
1017
|
-
* @param
|
|
1018
|
-
* @
|
|
1263
|
+
* @param assembly - The records sorted by path, and the module facts of the good files.
|
|
1264
|
+
* @param manifest - The root-relative manifest path when the file exists.
|
|
1265
|
+
* @returns The index, frozen.
|
|
1019
1266
|
*/
|
|
1020
|
-
function
|
|
1021
|
-
|
|
1267
|
+
function assembleIndex(assembly, manifest) {
|
|
1268
|
+
const symbols = /* @__PURE__ */ new Map();
|
|
1269
|
+
const files = {};
|
|
1270
|
+
const unresolved = [];
|
|
1271
|
+
for (const [file, record] of assembly.files) {
|
|
1272
|
+
files[file] = {
|
|
1273
|
+
hash: record.hash,
|
|
1274
|
+
state: record.state,
|
|
1275
|
+
...record.error === void 0 ? {} : { error: record.error }
|
|
1276
|
+
};
|
|
1277
|
+
const extracted = record.extracted?.result;
|
|
1278
|
+
if (record.good === void 0 || extracted === void 0) {
|
|
1279
|
+
unresolved.push({
|
|
1280
|
+
path: file,
|
|
1281
|
+
reason: record.error ?? "the file never parsed"
|
|
1282
|
+
});
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
for (const { key, anchor } of extracted.definitions) entryOf(symbols, key).def.push(anchor);
|
|
1286
|
+
unresolved.push(...extracted.unresolved);
|
|
1287
|
+
for (const table of extracted.tables) addTable(assembly, symbols, unresolved, file, table);
|
|
1288
|
+
}
|
|
1289
|
+
addStyleUses(assembly, symbols);
|
|
1290
|
+
addComponentUses(assembly, symbols);
|
|
1291
|
+
return deepFreeze({
|
|
1292
|
+
schemaVersion: 1,
|
|
1293
|
+
revision: revisionOf(files),
|
|
1294
|
+
...manifest === void 0 ? {} : { manifest },
|
|
1295
|
+
symbols: finishSymbols(symbols),
|
|
1296
|
+
files,
|
|
1297
|
+
unresolved
|
|
1298
|
+
});
|
|
1022
1299
|
}
|
|
1023
1300
|
//#endregion
|
|
1024
1301
|
//#region src/project/extract/definitions.ts
|
|
@@ -1027,7 +1304,8 @@ const ID_PREFIXES = {
|
|
|
1027
1304
|
defineFlow: "flow",
|
|
1028
1305
|
defineFeature: "feature",
|
|
1029
1306
|
defineScene: "scene",
|
|
1030
|
-
defineEmitter: "emitter"
|
|
1307
|
+
defineEmitter: "emitter",
|
|
1308
|
+
defineComponent: "component"
|
|
1031
1309
|
};
|
|
1032
1310
|
/**
|
|
1033
1311
|
* The text of a string literal or a template without holes.
|
|
@@ -1041,18 +1319,6 @@ function literalOf(typescript, node) {
|
|
|
1041
1319
|
return inner !== void 0 && typescript.isStringLiteralLike(inner) ? inner.text : void 0;
|
|
1042
1320
|
}
|
|
1043
1321
|
/**
|
|
1044
|
-
* The name of an object property when it is written literally.
|
|
1045
|
-
*
|
|
1046
|
-
* @param typescript - The TypeScript module.
|
|
1047
|
-
* @param property - The property.
|
|
1048
|
-
* @returns The name, or `undefined` for a spread or a computed name.
|
|
1049
|
-
*/
|
|
1050
|
-
function propertyName(typescript, property) {
|
|
1051
|
-
const name = property.name;
|
|
1052
|
-
if (name === void 0) return void 0;
|
|
1053
|
-
return typescript.isIdentifier(name) || typescript.isStringLiteral(name) || typescript.isNumericLiteral(name) ? name.text : void 0;
|
|
1054
|
-
}
|
|
1055
|
-
/**
|
|
1056
1322
|
* The value of a named property of an object literal; for a shorthand, the name itself.
|
|
1057
1323
|
*
|
|
1058
1324
|
* @param typescript - The TypeScript module.
|
|
@@ -1062,7 +1328,7 @@ function propertyName(typescript, property) {
|
|
|
1062
1328
|
*/
|
|
1063
1329
|
function propertyValue(typescript, object, name) {
|
|
1064
1330
|
for (const property of object.properties) {
|
|
1065
|
-
if (
|
|
1331
|
+
if (propertyNameOf(typescript, property) !== name) continue;
|
|
1066
1332
|
if (typescript.isPropertyAssignment(property)) return property.initializer;
|
|
1067
1333
|
if (typescript.isShorthandPropertyAssignment(property)) return property.name;
|
|
1068
1334
|
}
|
|
@@ -1182,7 +1448,7 @@ function readTable(reading, call, flow) {
|
|
|
1182
1448
|
unresolve(reading, `defineFlow "${flow}" nodes has a spread`);
|
|
1183
1449
|
continue;
|
|
1184
1450
|
}
|
|
1185
|
-
const name =
|
|
1451
|
+
const name = propertyNameOf(typescript, property);
|
|
1186
1452
|
if (name === void 0) {
|
|
1187
1453
|
unresolve(reading, `defineFlow "${flow}" node name is not a literal`);
|
|
1188
1454
|
continue;
|
|
@@ -1204,7 +1470,8 @@ function readTable(reading, call, flow) {
|
|
|
1204
1470
|
});
|
|
1205
1471
|
}
|
|
1206
1472
|
/**
|
|
1207
|
-
* Reads a definer that takes its id as the first argument; a flow also brings its table.
|
|
1473
|
+
* Reads a definer that takes its id as the first argument; a flow also brings its table. A
|
|
1474
|
+
* component is bound to its const, and the key takes the literal id.
|
|
1208
1475
|
*
|
|
1209
1476
|
* @param reading - The file being read.
|
|
1210
1477
|
* @param call - The call.
|
|
@@ -1251,7 +1518,7 @@ function readTextStyles(reading, call) {
|
|
|
1251
1518
|
}
|
|
1252
1519
|
const binding = bindingOf(typescript, call);
|
|
1253
1520
|
for (const property of table.properties) {
|
|
1254
|
-
const key =
|
|
1521
|
+
const key = propertyNameOf(typescript, property);
|
|
1255
1522
|
if (key === void 0) {
|
|
1256
1523
|
unresolve(reading, "defineTextStyles key is not a literal");
|
|
1257
1524
|
continue;
|
|
@@ -1271,24 +1538,79 @@ function readTextStyles(reading, call) {
|
|
|
1271
1538
|
}
|
|
1272
1539
|
}
|
|
1273
1540
|
/**
|
|
1274
|
-
*
|
|
1541
|
+
* Adds a style key; the same key and anchor twice in one file is one anchor, since `find`
|
|
1542
|
+
* answers every style call of a factory.
|
|
1543
|
+
*
|
|
1544
|
+
* @param reading - The file being read.
|
|
1545
|
+
* @param key - The key.
|
|
1546
|
+
* @param anchor - The anchor.
|
|
1547
|
+
*/
|
|
1548
|
+
function addStyle(reading, key, anchor) {
|
|
1549
|
+
const identity = JSON.stringify({
|
|
1550
|
+
key,
|
|
1551
|
+
anchor
|
|
1552
|
+
});
|
|
1553
|
+
if (reading.styles.has(identity)) return;
|
|
1554
|
+
reading.styles.add(identity);
|
|
1555
|
+
reading.definitions.push({
|
|
1556
|
+
key,
|
|
1557
|
+
anchor
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* The key and anchor of a style built in a module-level function: `style:<path>#<function>`, or
|
|
1562
|
+
* `style:<path>#<function>.<property>` when the call is the value of an object property there.
|
|
1563
|
+
*
|
|
1564
|
+
* @param reading - The file being read.
|
|
1565
|
+
* @param call - The `defineStyle` call.
|
|
1566
|
+
* @param factory - The name of the function the call sits in.
|
|
1567
|
+
* @returns The key and its anchor.
|
|
1568
|
+
*/
|
|
1569
|
+
function styleKeyIn(reading, call, factory) {
|
|
1570
|
+
const { typescript, path } = reading;
|
|
1571
|
+
const property = propertyOfValue(typescript, call);
|
|
1572
|
+
if (property === void 0) return {
|
|
1573
|
+
key: `style:${path}#${factory}`,
|
|
1574
|
+
anchor: {
|
|
1575
|
+
path,
|
|
1576
|
+
binding: factory
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
return {
|
|
1580
|
+
key: `style:${path}#${factory}.${property}`,
|
|
1581
|
+
anchor: {
|
|
1582
|
+
path,
|
|
1583
|
+
binding: factory,
|
|
1584
|
+
key: property
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Reads a style. One bound to a module const is `style:<path>#<binding>`. One built in a
|
|
1590
|
+
* module-level function, at any depth, is `style:<path>#<function>`, or
|
|
1591
|
+
* `style:<path>#<function>.<property>` when it is the value of an object property there. Any
|
|
1592
|
+
* other style is unresolved.
|
|
1275
1593
|
*
|
|
1276
1594
|
* @param reading - The file being read.
|
|
1277
1595
|
* @param call - The call.
|
|
1278
1596
|
*/
|
|
1279
1597
|
function readStyle(reading, call) {
|
|
1280
|
-
const
|
|
1598
|
+
const { typescript, path } = reading;
|
|
1599
|
+
const binding = bindingOf(typescript, call);
|
|
1281
1600
|
if (binding !== void 0) {
|
|
1282
|
-
reading
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
path: reading.path,
|
|
1286
|
-
binding
|
|
1287
|
-
}
|
|
1601
|
+
addStyle(reading, `style:${path}#${binding}`, {
|
|
1602
|
+
path,
|
|
1603
|
+
binding
|
|
1288
1604
|
});
|
|
1289
1605
|
return;
|
|
1290
1606
|
}
|
|
1291
|
-
const
|
|
1607
|
+
const factory = enclosingFunction(typescript, call);
|
|
1608
|
+
if (factory !== void 0) {
|
|
1609
|
+
const style = styleKeyIn(reading, call, factory);
|
|
1610
|
+
addStyle(reading, style.key, style.anchor);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
const around = enclosingName(typescript, call);
|
|
1292
1614
|
unresolve(reading, `defineStyle${around === void 0 ? "" : ` in "${around}"`} is not bound to a module-level const`);
|
|
1293
1615
|
}
|
|
1294
1616
|
/**
|
|
@@ -1336,7 +1658,8 @@ function readJsx(reading, source) {
|
|
|
1336
1658
|
* @param source - The parsed file.
|
|
1337
1659
|
* @param path - The root-relative path of the file.
|
|
1338
1660
|
* @param definers - The definers the file can call, from `definersOf`.
|
|
1339
|
-
* @returns The definitions, the node tables and the unresolved items
|
|
1661
|
+
* @returns The definitions, the node tables and the unresolved items in source order, and the
|
|
1662
|
+
* component names the file renders.
|
|
1340
1663
|
*/
|
|
1341
1664
|
function extractFile(typescript, source, path, definers) {
|
|
1342
1665
|
const reading = {
|
|
@@ -1345,7 +1668,8 @@ function extractFile(typescript, source, path, definers) {
|
|
|
1345
1668
|
definers,
|
|
1346
1669
|
definitions: [],
|
|
1347
1670
|
tables: [],
|
|
1348
|
-
unresolved: []
|
|
1671
|
+
unresolved: [],
|
|
1672
|
+
styles: /* @__PURE__ */ new Set()
|
|
1349
1673
|
};
|
|
1350
1674
|
const visit = (node) => {
|
|
1351
1675
|
const definer = typescript.isCallExpression(node) ? definerOfCall(typescript, node, definers) : void 0;
|
|
@@ -1353,11 +1677,13 @@ function extractFile(typescript, source, path, definers) {
|
|
|
1353
1677
|
typescript.forEachChild(node, visit);
|
|
1354
1678
|
};
|
|
1355
1679
|
if (definers.size > 0) visit(source);
|
|
1680
|
+
reading.definitions.push(...readComponents(typescript, source, path));
|
|
1356
1681
|
readJsx(reading, source);
|
|
1357
1682
|
return {
|
|
1358
1683
|
definitions: reading.definitions,
|
|
1359
1684
|
tables: reading.tables,
|
|
1360
|
-
unresolved: reading.unresolved
|
|
1685
|
+
unresolved: reading.unresolved,
|
|
1686
|
+
rendered: renderedOf(typescript, source)
|
|
1361
1687
|
};
|
|
1362
1688
|
}
|
|
1363
1689
|
//#endregion
|
|
@@ -1541,6 +1867,8 @@ function buildIndex(catalog, manifest) {
|
|
|
1541
1867
|
}
|
|
1542
1868
|
//#endregion
|
|
1543
1869
|
//#region src/project/locate.ts
|
|
1870
|
+
/** The kind of a style key. */
|
|
1871
|
+
const STYLE_KIND = "style";
|
|
1544
1872
|
/**
|
|
1545
1873
|
* The place of a node: the line of one node and the range of another.
|
|
1546
1874
|
*
|
|
@@ -1581,18 +1909,6 @@ function declares(typescript, statement, binding) {
|
|
|
1581
1909
|
});
|
|
1582
1910
|
}
|
|
1583
1911
|
/**
|
|
1584
|
-
* The name of a property-like node, when it is written literally.
|
|
1585
|
-
*
|
|
1586
|
-
* @param typescript - The TypeScript module.
|
|
1587
|
-
* @param node - Any node.
|
|
1588
|
-
* @returns The name of a property, a shorthand or a method; `undefined` for anything else.
|
|
1589
|
-
*/
|
|
1590
|
-
function propertyNameOf(typescript, node) {
|
|
1591
|
-
if (!(typescript.isPropertyAssignment(node) || typescript.isShorthandPropertyAssignment(node) || typescript.isMethodDeclaration(node))) return void 0;
|
|
1592
|
-
const name = node.name;
|
|
1593
|
-
return typescript.isIdentifier(name) || typescript.isStringLiteral(name) || typescript.isNumericLiteral(name) ? name.text : void 0;
|
|
1594
|
-
}
|
|
1595
|
-
/**
|
|
1596
1912
|
* The shallowest property of a key inside a declaration, the first one at that depth.
|
|
1597
1913
|
*
|
|
1598
1914
|
* @param typescript - The TypeScript module.
|
|
@@ -1628,20 +1944,61 @@ function writtenAt(typescript, source, key) {
|
|
|
1628
1944
|
return placeOf(source, found, typescript.isPropertyAssignment(parent) || typescript.isCallExpression(parent) ? parent : found);
|
|
1629
1945
|
}
|
|
1630
1946
|
/**
|
|
1947
|
+
* The style calls a function makes, at any depth, in source order. A call that is the value of a
|
|
1948
|
+
* property is left out: it has a key of its own.
|
|
1949
|
+
*
|
|
1950
|
+
* @param typescript - The TypeScript module.
|
|
1951
|
+
* @param factory - The function.
|
|
1952
|
+
* @param definers - The definers of the file.
|
|
1953
|
+
* @returns The calls.
|
|
1954
|
+
*/
|
|
1955
|
+
function styleCallsIn(typescript, factory, definers) {
|
|
1956
|
+
const calls = [];
|
|
1957
|
+
const visit = (node) => {
|
|
1958
|
+
if (typescript.isCallExpression(node) && definerOfCall(typescript, node, definers) === "defineStyle" && propertyOfValue(typescript, node) === void 0) calls.push(node);
|
|
1959
|
+
typescript.forEachChild(node, visit);
|
|
1960
|
+
};
|
|
1961
|
+
visit(factory);
|
|
1962
|
+
return calls;
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Reads a style built in a module-level function: every style call of the function, or, with a
|
|
1966
|
+
* key, the property of that key with the call as the range.
|
|
1967
|
+
*
|
|
1968
|
+
* @param typescript - The TypeScript module.
|
|
1969
|
+
* @param source - The parsed file.
|
|
1970
|
+
* @param anchor - The anchor, bound to the function.
|
|
1971
|
+
* @param context - What the caller knows: the definers of the file.
|
|
1972
|
+
* @returns The places; none when the binding is not a function or holds no such call.
|
|
1973
|
+
*/
|
|
1974
|
+
function locateFactory(typescript, source, anchor, context) {
|
|
1975
|
+
const factory = source.statements.flatMap((statement) => declaredFunctions(typescript, statement)).find((item) => item.name === anchor.binding);
|
|
1976
|
+
if (factory === void 0) return [];
|
|
1977
|
+
if (anchor.key !== void 0) {
|
|
1978
|
+
const property = propertyIn(typescript, factory.node, anchor.key);
|
|
1979
|
+
if (property === void 0 || !typescript.isPropertyAssignment(property)) return [];
|
|
1980
|
+
return [placeOf(source, property, unwrap(typescript, property.initializer))];
|
|
1981
|
+
}
|
|
1982
|
+
const definers = context.definers?.() ?? /* @__PURE__ */ new Map();
|
|
1983
|
+
return styleCallsIn(typescript, factory.node, definers).map((call) => placeOf(source, call, call));
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1631
1986
|
* Reads an anchor on one parse of its file.
|
|
1632
1987
|
*
|
|
1633
1988
|
* @param typescript - The TypeScript module.
|
|
1634
1989
|
* @param source - The parsed file.
|
|
1635
1990
|
* @param anchor - The anchor.
|
|
1636
|
-
* @param
|
|
1991
|
+
* @param context - The kind of its key, the JSX hits of the parse and the definers of the file.
|
|
1637
1992
|
* @returns Every place of the anchor in the file; none when the file no longer holds it.
|
|
1638
1993
|
*/
|
|
1639
|
-
function locate(typescript, source, anchor,
|
|
1640
|
-
if (anchor.kind !== void 0) return jsxHits().filter((hit) => hit.key === anchor.key && hit.kind === anchor.kind && hit.component === anchor.component).map((hit) => placeOf(source, hit.attribute, hit.element));
|
|
1994
|
+
function locate(typescript, source, anchor, context = {}) {
|
|
1995
|
+
if (anchor.kind !== void 0) return (context.jsxHits?.() ?? collectJsx(typescript, source)).filter((hit) => hit.key === anchor.key && hit.kind === anchor.kind && hit.component === anchor.component && hit.prop === anchor.prop).map((hit) => placeOf(source, hit.attribute, hit.element));
|
|
1641
1996
|
if (anchor.binding === void 0) {
|
|
1642
1997
|
const place = anchor.key === void 0 ? void 0 : writtenAt(typescript, source, anchor.key);
|
|
1643
1998
|
return place === void 0 ? [] : [place];
|
|
1644
1999
|
}
|
|
2000
|
+
const built = context.keyKind === STYLE_KIND ? locateFactory(typescript, source, anchor, context) : [];
|
|
2001
|
+
if (built.length > 0) return built;
|
|
1645
2002
|
const binding = anchor.binding;
|
|
1646
2003
|
const statement = source.statements.find((item) => declares(typescript, item, binding));
|
|
1647
2004
|
if (statement === void 0) return [];
|
|
@@ -1798,35 +2155,12 @@ async function readInside(root, file) {
|
|
|
1798
2155
|
if (real !== void 0) return await readFile(real);
|
|
1799
2156
|
} catch {}
|
|
1800
2157
|
}
|
|
1801
|
-
/**
|
|
1802
|
-
* The anchors of the JSX patterns and of the literal `id=` props of an index.
|
|
1803
|
-
*
|
|
1804
|
-
* @param index - The index.
|
|
1805
|
-
* @returns The `{id}` patterns, the `*` patterns and the `id=` props.
|
|
1806
|
-
*/
|
|
1807
|
-
function jsxShapes(index) {
|
|
1808
|
-
const shapes = {
|
|
1809
|
-
idPatterns: [],
|
|
1810
|
-
wildPatterns: [],
|
|
1811
|
-
idAttributes: []
|
|
1812
|
-
};
|
|
1813
|
-
for (const [key, entry] of Object.entries(index.symbols)) {
|
|
1814
|
-
if (!key.startsWith("jsx:")) continue;
|
|
1815
|
-
for (const anchor of entry.def) {
|
|
1816
|
-
const pattern = anchor.key ?? "";
|
|
1817
|
-
if (anchor.kind === "idProp") shapes.idAttributes.push(anchor);
|
|
1818
|
-
else if (pattern.includes("{id}")) shapes.idPatterns.push(anchor);
|
|
1819
|
-
else if (pattern.includes("*")) shapes.wildPatterns.push(anchor);
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
return shapes;
|
|
1823
|
-
}
|
|
1824
2158
|
//#endregion
|
|
1825
2159
|
//#region src/project/find.ts
|
|
1826
2160
|
/**
|
|
1827
2161
|
* Whether a key matches a pattern whose `*` holes stand for any text.
|
|
1828
2162
|
*
|
|
1829
|
-
* @param pattern - A pattern,
|
|
2163
|
+
* @param pattern - A pattern, its prop holes already filled.
|
|
1830
2164
|
* @param key - The key without its prefix.
|
|
1831
2165
|
* @returns True when the key reads as the pattern.
|
|
1832
2166
|
* @example
|
|
@@ -1854,18 +2188,21 @@ function shapesOf(session) {
|
|
|
1854
2188
|
return shapes;
|
|
1855
2189
|
}
|
|
1856
2190
|
/**
|
|
1857
|
-
* Whether
|
|
1858
|
-
*
|
|
2191
|
+
* Whether a literal prop fills the holes of its name in a pattern into the wanted key: `id=` fills
|
|
2192
|
+
* `{id}`, `amountKey=` fills `{amountKey}`. A pattern written in a component takes only the props
|
|
2193
|
+
* of that component; one written in a helper takes any.
|
|
1859
2194
|
*
|
|
1860
|
-
* @param pattern - The anchor of the
|
|
1861
|
-
* @param
|
|
2195
|
+
* @param pattern - The anchor of the pattern.
|
|
2196
|
+
* @param filler - The anchor of the prop: an `idProp` anchor.
|
|
1862
2197
|
* @param wanted - The runtime key, without its prefix.
|
|
1863
2198
|
* @returns True when the filled pattern reads as the key.
|
|
1864
2199
|
*/
|
|
1865
|
-
function fills(pattern,
|
|
1866
|
-
|
|
1867
|
-
const
|
|
1868
|
-
|
|
2200
|
+
function fills(pattern, filler, wanted) {
|
|
2201
|
+
if (pattern.component !== void 0 && pattern.component !== filler.component) return false;
|
|
2202
|
+
const key = pattern.key ?? "";
|
|
2203
|
+
const hole = holeOf(filler.prop ?? "id");
|
|
2204
|
+
if (!key.includes(hole)) return false;
|
|
2205
|
+
return matchesPattern(key.replaceAll(hole, filler.key ?? ""), wanted);
|
|
1869
2206
|
}
|
|
1870
2207
|
/**
|
|
1871
2208
|
* How much literal text a pattern holds: a pattern with more of it says more about a key.
|
|
@@ -1878,7 +2215,7 @@ function fills(pattern, idAttribute, wanted) {
|
|
|
1878
2215
|
* ```
|
|
1879
2216
|
*/
|
|
1880
2217
|
function specificityOf(anchor) {
|
|
1881
|
-
return (anchor.key ?? "").replaceAll(
|
|
2218
|
+
return (anchor.key ?? "").replaceAll(HOLE, "").replaceAll("*", "").length;
|
|
1882
2219
|
}
|
|
1883
2220
|
/**
|
|
1884
2221
|
* Orders patterns from the most specific to the least, keeping their order otherwise.
|
|
@@ -1891,25 +2228,25 @@ function bySpecificity(first, second) {
|
|
|
1891
2228
|
return specificityOf(second) - specificityOf(first);
|
|
1892
2229
|
}
|
|
1893
2230
|
/**
|
|
1894
|
-
* The anchors a runtime JSX key reaches through patterns: each
|
|
1895
|
-
* prop (the pattern, then the prop), then each `*` pattern as a wildcard.
|
|
1896
|
-
* pattern with more literal text comes first: `card*Picture` before `card*`.
|
|
2231
|
+
* The anchors a runtime JSX key reaches through patterns: each prop-hole pattern filled with a
|
|
2232
|
+
* literal prop of its name (the pattern, then the prop), then each `*` pattern as a wildcard.
|
|
2233
|
+
* Within each tier the pattern with more literal text comes first: `card*Picture` before `card*`.
|
|
1897
2234
|
*
|
|
1898
2235
|
* @param shapes - The JSX shapes of the index.
|
|
1899
2236
|
* @param wanted - The runtime key, without its prefix.
|
|
1900
2237
|
* @returns The anchors, in that order.
|
|
1901
2238
|
*/
|
|
1902
2239
|
function patternAnchors(shapes, wanted) {
|
|
1903
|
-
const {
|
|
2240
|
+
const { holePatterns, wildPatterns, idProps } = shapes;
|
|
1904
2241
|
const reached = [];
|
|
1905
|
-
for (const pattern of
|
|
2242
|
+
for (const pattern of holePatterns.toSorted(bySpecificity)) for (const filler of idProps.filter((item) => fills(pattern, item, wanted))) reached.push(pattern, filler);
|
|
1906
2243
|
const wild = wildPatterns.filter((pattern) => matchesPattern(pattern.key ?? "", wanted));
|
|
1907
2244
|
reached.push(...wild.toSorted(bySpecificity));
|
|
1908
2245
|
return reached;
|
|
1909
2246
|
}
|
|
1910
2247
|
/**
|
|
1911
2248
|
* The anchors of a key, in the order `find` answers them: the exact key, then, for a JSX key as
|
|
1912
|
-
* the game reports it (no `*`, no
|
|
2249
|
+
* the game reports it (no `*`, no prop hole), the patterns that read as it.
|
|
1913
2250
|
*
|
|
1914
2251
|
* @param session - The open project.
|
|
1915
2252
|
* @param key - The key.
|
|
@@ -1917,7 +2254,7 @@ function patternAnchors(shapes, wanted) {
|
|
|
1917
2254
|
*/
|
|
1918
2255
|
function anchorsOf(session, key) {
|
|
1919
2256
|
const exact = session.index.symbols[key]?.def ?? [];
|
|
1920
|
-
const reached = key.startsWith("jsx:") && !key.includes("*") && !key
|
|
2257
|
+
const reached = key.startsWith("jsx:") && !key.includes("*") && !hasHole(key) ? patternAnchors(shapesOf(session), key.slice(4)) : [];
|
|
1921
2258
|
return [.../* @__PURE__ */ new Set([...exact, ...reached])];
|
|
1922
2259
|
}
|
|
1923
2260
|
/**
|
|
@@ -1965,6 +2302,33 @@ async function viewOf(session, file) {
|
|
|
1965
2302
|
};
|
|
1966
2303
|
}
|
|
1967
2304
|
/**
|
|
2305
|
+
* The definers a file can call, recognised by binding on the module facts of the catalog, as
|
|
2306
|
+
* extraction does.
|
|
2307
|
+
*
|
|
2308
|
+
* @param catalog - The catalog of the project.
|
|
2309
|
+
* @param file - The root-relative path.
|
|
2310
|
+
* @returns The callee text to the definer.
|
|
2311
|
+
*/
|
|
2312
|
+
function definersAt(catalog, file) {
|
|
2313
|
+
const modules = /* @__PURE__ */ new Map();
|
|
2314
|
+
for (const [path, record] of catalog.records) if (record.good !== void 0) modules.set(path, record.good.module);
|
|
2315
|
+
return definersOf(modules, file);
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* The kind of a key: the text before its first colon.
|
|
2319
|
+
*
|
|
2320
|
+
* @param key - A key.
|
|
2321
|
+
* @returns The kind, or the whole key when it has no colon.
|
|
2322
|
+
* @example
|
|
2323
|
+
* ```ts
|
|
2324
|
+
* kindOf("style:features/ui/kit.tsx#boardStyle"); // "style"
|
|
2325
|
+
* ```
|
|
2326
|
+
*/
|
|
2327
|
+
function kindOf(key) {
|
|
2328
|
+
const colon = key.indexOf(":");
|
|
2329
|
+
return colon === -1 ? key : key.slice(0, colon);
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
1968
2332
|
* The identity of an answer: two anchors that land on the same element with the same key answer
|
|
1969
2333
|
* once.
|
|
1970
2334
|
*
|
|
@@ -1989,11 +2353,16 @@ async function findKey(session, key) {
|
|
|
1989
2353
|
const views = /* @__PURE__ */ new Map();
|
|
1990
2354
|
const seen = /* @__PURE__ */ new Set();
|
|
1991
2355
|
const found = [];
|
|
2356
|
+
const keyKind = kindOf(key);
|
|
1992
2357
|
for (const anchor of anchorsOf(session, key)) {
|
|
1993
2358
|
if (!views.has(anchor.path)) views.set(anchor.path, await viewOf(session, anchor.path));
|
|
1994
2359
|
const view = views.get(anchor.path);
|
|
1995
2360
|
if (view === void 0) continue;
|
|
1996
|
-
const answers = locate(session.catalog.typescript, view.source, anchor,
|
|
2361
|
+
const answers = locate(session.catalog.typescript, view.source, anchor, {
|
|
2362
|
+
keyKind,
|
|
2363
|
+
jsxHits: view.jsx,
|
|
2364
|
+
definers: () => definersAt(session.catalog, anchor.path)
|
|
2365
|
+
}).map((place) => ({
|
|
1997
2366
|
...anchor,
|
|
1998
2367
|
...place,
|
|
1999
2368
|
hash: view.hash,
|