@moku-labs/game 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- /** The definers whose calls make keys. */
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
- * Whether a name is a definer whose calls make keys.
8
+ * Unwraps the expressions that change no value: parentheses, `as`, `satisfies`, `!` and `<T>`.
18
9
  *
19
- * @param name - A name.
20
- * @returns True for the definers of `DEFINERS`.
21
- * @example
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 isDefiner(name) {
27
- return DEFINERS.includes(name);
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
- * Whether a statement carries a modifier.
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 node - The statement.
34
- * @param kind - `ExportKeyword` or `DefaultKeyword`.
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 hasModifier(typescript, node, kind) {
38
- if (!typescript.canHaveModifiers(node)) return false;
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 names a binding declares: the identifier, or every name of a destructure.
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 name - The binding name.
46
- * @returns The declared names.
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 namesOf(typescript, name) {
49
- if (typescript.isIdentifier(name)) return [name.text];
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
- * Records the value imports of one import statement.
41
+ * Whether a node holds statements: a block, a source file, a module block or a case clause.
54
42
  *
55
- * @param typescript - The TypeScript module.
56
- * @param statement - The import statement.
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 readImport(typescript, statement, draft) {
60
- const clause = statement.importClause;
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
- * Records one export statement: a re-export from another module, a star, or a list of local
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 statement - The export statement.
92
- * @param draft - The facts so far.
53
+ * @param node - A node.
54
+ * @returns True for function declarations, expressions, arrows and methods.
93
55
  */
94
- function readExport(typescript, statement, draft) {
95
- const source = statement.moduleSpecifier;
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
- * Whether an import brings a name of the engine package.
60
+ * The meaning of a `const` declarator: a function when its initializer is one.
122
61
  *
123
- * @param ref - The import, if any.
124
- * @param name - The name taken from the package, `*` for a namespace.
125
- * @returns True for that import of `@moku-labs/game`.
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 isGameImport(ref, name) {
132
- return ref?.specifier === "@moku-labs/game" && ref.imported === name;
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
- * Whether an expression calls `defineGame` of the engine: by a named import, renamed or not, or
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 expression - The initializer of a destructure.
140
- * @param imports - The value imports of the file.
141
- * @returns True for the kit call.
80
+ * @param holder - The block.
81
+ * @param name - The name.
82
+ * @returns What the name means there, or `undefined`.
142
83
  */
143
- function callsDefineGame(typescript, expression, imports) {
144
- if (expression === void 0 || !typescript.isCallExpression(expression)) return false;
145
- const callee = expression.expression;
146
- if (typescript.isIdentifier(callee)) return isGameImport(imports.get(callee.text), "defineGame");
147
- return typescript.isPropertyAccessExpression(callee) && typescript.isIdentifier(callee.expression) && callee.name.text === "defineGame" && isGameImport(imports.get(callee.expression.text), "*");
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
- * Records the definers a `defineGame` destructure hands out, under their local names.
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 draft - The facts so far.
101
+ * @param name - The name.
102
+ * @returns The parameter meaning with the property it takes, or `undefined`.
155
103
  */
156
- function readKit(typescript, pattern, draft) {
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
- const handed = property !== void 0 && typescript.isIdentifier(property) ? property.text : element.name.text;
161
- if (isDefiner(handed)) draft.kit.set(element.name.text, handed);
108
+ return {
109
+ kind: "param",
110
+ property: property !== void 0 && typescript.isIdentifier(property) ? property.text : name
111
+ };
162
112
  }
163
113
  }
164
114
  /**
165
- * Records one top-level variable statement: its names, its exports and a kit destructure.
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 statement - The statement.
169
- * @param draft - The facts so far.
119
+ * @param scope - The function.
120
+ * @param name - The name.
121
+ * @returns The parameter meaning, or `undefined`.
170
122
  */
171
- function readVariables(typescript, statement, draft) {
172
- const exported = hasModifier(typescript, statement, typescript.SyntaxKind.ExportKeyword);
173
- for (const declaration of statement.declarationList.declarations) {
174
- for (const name of namesOf(typescript, declaration.name)) {
175
- draft.declared.add(name);
176
- if (exported) draft.exports.set(name, { local: name });
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
- * Records a top-level function or class: its name, and its export.
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 statement - The declaration.
187
- * @param draft - The facts so far.
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 readNamed(typescript, statement, draft) {
190
- const name = statement.name?.text;
191
- if (name === void 0) return;
192
- draft.declared.add(name);
193
- if (!hasModifier(typescript, statement, typescript.SyntaxKind.ExportKeyword)) return;
194
- const isDefault = hasModifier(typescript, statement, typescript.SyntaxKind.DefaultKeyword);
195
- draft.exports.set(isDefault ? "default" : name, { local: name });
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
- * Reads the module facts of one parsed file. Imports come first, so a kit destructure anywhere
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 source - The parsed file.
203
- * @returns The facts.
152
+ * @param node - Any node.
153
+ * @returns The statement, or the file itself for the file.
204
154
  */
205
- function readModule(typescript, source) {
206
- const draft = {
207
- imports: /* @__PURE__ */ new Map(),
208
- exports: /* @__PURE__ */ new Map(),
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
- * @file project, extract — following a name across files with the module facts only: an import
226
- * to the module it names, a re-export to the module behind it, a star export to the first module
227
- * that has the name, until the name is declared, handed out by the kit, or comes from a package.
228
- * This is how a definer is recognised by binding (a renamed import still is the definer) and how
229
- * a node of a flow table reaches the file that declares it.
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
- /** How many imports and re-exports a name is followed through before giving up. */
232
- const MAX_HOPS = 12;
233
- /** The extensions a TypeScript import may name instead of the source file. */
234
- const SCRIPT_EXTENSION = /\.(?:[cm]?js|jsx)$/;
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
- * Whether a module specifier is relative to the importing file.
176
+ * The function a const declarator holds, through parentheses, `as` and `satisfies`.
237
177
  *
238
- * @param specifier - The module specifier.
239
- * @returns True for `./…` and `../…`.
240
- * @example
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 isRelative(specifier) {
246
- return specifier === "." || specifier === ".." || specifier.startsWith("./") || specifier.startsWith("../");
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
- * Resolves a relative module specifier to an indexed file, the way a bundler does: the path as
250
- * written, then with `.ts`, `.tsx`, `/index.ts` and `/index.tsx`.
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 modules - The module facts by path.
253
- * @param from - The importing file.
254
- * @param specifier - The module specifier.
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 resolveModulePath(modules, from, specifier) {
258
- if (!isRelative(specifier)) return void 0;
259
- const base = path.posix.normalize(path.posix.join(path.posix.dirname(from), specifier));
260
- const stem = base.replace(SCRIPT_EXTENSION, "");
261
- return [
262
- base,
263
- `${stem}.ts`,
264
- `${stem}.tsx`,
265
- `${stem}/index.ts`,
266
- `${stem}/index.tsx`
267
- ].find((candidate) => modules.has(candidate));
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
- * Follows an imported name into the module that the specifier names.
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 modules - The module facts by path.
273
- * @param from - The importing file.
274
- * @param ref - The specifier and the name.
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 followImport(modules, from, ref, hops) {
279
- if (!isRelative(ref.specifier)) return {
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
- * Follows an exported name of a module: a local binding, a re-export, or a star export.
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 modules - The module facts by path.
291
- * @param file - The module.
292
- * @param name - The exported name.
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 resolveExport(modules, file, name, hops = 0) {
297
- const facts = modules.get(file);
298
- if (facts === void 0 || hops > MAX_HOPS) return void 0;
299
- const ref = facts.exports.get(name);
300
- if (ref !== void 0 && "local" in ref) return resolveBinding(modules, file, ref.local, hops + 1);
301
- if (ref !== void 0) return followImport(modules, file, ref, hops);
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
- * Follows a name used in a file to where it ends up: the kit hands it out, the file declares it,
312
- * or an import brings it.
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
- function resolveBinding(modules, file, local, hops = 0) {
321
- const facts = modules.get(file);
322
- if (facts === void 0 || hops > MAX_HOPS) return void 0;
323
- const definer = facts.kit.get(local);
324
- if (definer !== void 0) return {
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 definer a target is, if any: one the kit hands out, or one imported from the engine.
252
+ * The hole a prop of a component leaves in a key.
338
253
  *
339
- * @param target - Where a name ends up.
340
- * @returns The definer, or `undefined`.
254
+ * @param property - The name of the prop.
255
+ * @returns The hole: `{amountKey}` for `amountKey`.
341
256
  * @example
342
257
  * ```ts
343
- * definerOf({ kind: "package", specifier: "@moku-labs/game", imported: "defineStyle" }); // "defineStyle"
258
+ * holeOf("amountKey"); // "{amountKey}"
344
259
  * ```
345
260
  */
346
- function definerOf(target) {
347
- if (target?.kind === "definer") return target.definer;
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
- * Hashes bytes or text with sha1.
265
+ * Whether a pattern holds a hole a prop fills.
401
266
  *
402
- * @param bytes - The file bytes, or a text.
403
- * @returns The hex digest.
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
- * sha1(""); // "da39a3ee5e6b4b0d3255bfef95601890afd80709"
271
+ * hasHole("{id}Close"); // true
407
272
  * ```
408
273
  */
409
- function sha1(bytes) {
410
- return createHash("sha1").update(bytes).digest("hex");
274
+ function hasHole(pattern) {
275
+ return pattern.search(HOLE) !== -1;
411
276
  }
412
277
  /**
413
- * The revision of an index: the sha1 of the lines `${path}\0${hash}\n`, sorted by path.
278
+ * The hole a prop of a parameter object leaves: its own hole for a key-carrying prop, `*` else.
414
279
  *
415
- * @param files - Every indexed file with its hash.
416
- * @returns The hex digest.
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
- * revisionOf({}); // "da39a3ee5e6b4b0d3255bfef95601890afd80709"
284
+ * propertyHole("tab"); // "*"
420
285
  * ```
421
286
  */
422
- function revisionOf(files) {
423
- return sha1(Object.entries(files).map(([file, entry]) => `${file}\0${entry.hash}\n`).toSorted().join(""));
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 entry of a key, made on first use.
291
+ * The one expression a function returns, when it has exactly one.
429
292
  *
430
- * @param symbols - The keys so far.
431
- * @param key - The key.
432
- * @returns Its entry.
293
+ * @param typescript - The TypeScript module.
294
+ * @param node - The function.
295
+ * @returns The returned expression, or `undefined`.
433
296
  */
434
- function entryOf(symbols, key) {
435
- const existing = symbols.get(key);
436
- if (existing !== void 0) return existing;
437
- const entry = {
438
- def: [],
439
- uses: []
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
- symbols.set(key, entry);
442
- return entry;
306
+ typescript.forEachChild(body, collect);
307
+ return returns.length === 1 ? returns[0]?.expression : void 0;
443
308
  }
444
309
  /**
445
- * Adds the `node:` keys of one flow table. A plain name is followed to the file that declares
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 assembly - The records and module facts.
450
- * @param symbols - The keys so far.
451
- * @param unresolved - The unresolved list so far.
452
- * @param file - The flow file.
453
- * @param table - The table.
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 addTable(assembly, symbols, unresolved, file, table) {
456
- const use = table.binding === void 0 ? { path: file } : {
457
- path: file,
458
- binding: table.binding
459
- };
460
- for (const { name, ref } of table.entries) {
461
- const entry = entryOf(symbols, `node:${table.flow}/${name}`);
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
- * Adds, to every style key, the files that import its binding by name.
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 assembly - The records and module facts.
489
- * @param symbols - The keys so far.
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 addStyleUses(assembly, symbols) {
492
- for (const [file, record] of assembly.files) for (const [local, ref] of record.good?.module.imports ?? []) {
493
- const target = ref.imported === "*" ? void 0 : resolveBinding(assembly.modules, file, local);
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
- * The symbols as the index holds them: sorted by key, `uses` only when there are some, and
502
- * `conflict` on a key other than `jsx:` defined twice.
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 symbols - The keys as collected.
505
- * @returns The symbols of the index.
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 finishSymbols(symbols) {
508
- const finished = {};
509
- const sorted = [...symbols].toSorted(([first], [second]) => first < second ? -1 : 1);
510
- for (const [key, { def, uses }] of sorted) {
511
- const isConflict = !key.startsWith("jsx:") && def.length > 1;
512
- finished[key] = {
513
- def,
514
- ...uses.length === 0 ? {} : { uses },
515
- ...isConflict ? { conflict: true } : {}
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 finished;
361
+ return read(typescript, returned, inner, depth + 1);
519
362
  }
520
363
  /**
521
- * Freezes a value and everything it holds.
364
+ * Reads a template: its literal parts stay, each span is read into a pattern.
522
365
  *
523
- * @param value - A plain value.
524
- * @returns The same value, frozen.
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 deepFreeze(value) {
527
- if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
528
- Object.freeze(value);
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
- * Assembles the index of a catalog.
377
+ * Reads one expression into a pattern.
534
378
  *
535
- * @param assembly - The records sorted by path, and the module facts of the good files.
536
- * @param manifest - The root-relative manifest path when the file exists.
537
- * @returns The index, frozen.
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 assembleIndex(assembly, manifest) {
540
- const symbols = /* @__PURE__ */ new Map();
541
- const files = {};
542
- const unresolved = [];
543
- for (const [file, record] of assembly.files) {
544
- files[file] = {
545
- hash: record.hash,
546
- state: record.state,
547
- ...record.error === void 0 ? {} : { error: record.error }
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
- * Unwraps the expressions that change no value: parentheses, `as`, `satisfies`, `!` and `<T>`.
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 node - An expression.
578
- * @returns The expression inside.
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 unwrap(typescript, node) {
581
- let inner = node;
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
- * Whether a node holds statements: a block, a source file, a module block or a case clause.
408
+ * The literal head of a pattern: the text before its first hole.
587
409
  *
588
- * @param node - A node.
589
- * @returns True when it has a `statements` list.
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 holdsStatements(node) {
592
- return "statements" in node && Array.isArray(node.statements);
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
- * Whether a node is a function whose parameters a name can be.
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 node - A node.
599
- * @returns True for function declarations, expressions, arrows and methods.
431
+ * @param attribute - The attribute.
432
+ * @returns The expression, or `undefined` for `<a key />` and `key={}`.
600
433
  */
601
- function isFunctionScope(typescript, node) {
602
- return typescript.isFunctionDeclaration(node) || typescript.isFunctionExpression(node) || typescript.isArrowFunction(node) || typescript.isMethodDeclaration(node);
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
- * The meaning of a `const` declarator: a function when its initializer is one.
441
+ * How a key expression is written.
606
442
  *
607
443
  * @param typescript - The TypeScript module.
608
- * @param initializer - The initializer of the const.
609
- * @returns The meaning.
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 constMeaning(typescript, initializer) {
612
- const inner = unwrap(typescript, initializer);
613
- return typescript.isArrowFunction(inner) || typescript.isFunctionExpression(inner) ? {
614
- kind: "function",
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
- * Looks for a name among the statements of one block.
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 holder - The block.
626
- * @param name - The name.
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 declaredIn(typescript, holder, name) {
630
- for (const statement of holder.statements) {
631
- if (typescript.isFunctionDeclaration(statement) && statement.name?.text === name) return {
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
- * Looks for a name among the names of a destructured parameter (`({ id }) => …`).
464
+ * Quotes a key expression for an unresolved reason: one line, cut to a readable length.
643
465
  *
644
- * @param typescript - The TypeScript module.
645
- * @param pattern - The destructure.
646
- * @param name - The name.
647
- * @returns The parameter meaning with the property it takes, or `undefined`.
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 destructuredIn(typescript, pattern, name) {
650
- for (const element of pattern.elements) {
651
- if (!(typescript.isBindingElement(element) && typescript.isIdentifier(element.name) && element.name.text === name)) continue;
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
- * Looks for a name among the parameters of one function: a plain parameter, or a property of a
661
- * destructured one.
478
+ * The hit of a `key` attribute.
662
479
  *
663
480
  * @param typescript - The TypeScript module.
664
- * @param scope - The function.
665
- * @param name - The name.
666
- * @returns The parameter meaning, or `undefined`.
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 parameterIn(typescript, scope, name) {
669
- for (const parameter of scope.parameters) {
670
- const binding = parameter.name;
671
- if (typescript.isIdentifier(binding) && binding.text === name) return { kind: "param" };
672
- if (typescript.isIdentifier(binding)) continue;
673
- const meaning = destructuredIn(typescript, binding, name);
674
- if (meaning !== void 0) return meaning;
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
- * Says what a name means at the place it is used, walking up from there.
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 identifier - The name where it is used.
682
- * @returns What it means, or `undefined` when the file does not declare it.
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 meaningOf(typescript, identifier) {
685
- const name = identifier.text;
686
- for (let node = identifier.parent; node !== void 0; node = node.parent) {
687
- const parameter = isFunctionScope(typescript, node) ? parameterIn(typescript, node, name) : void 0;
688
- if (parameter !== void 0) return parameter;
689
- const declared = holdsStatements(node) ? declaredIn(typescript, node, name) : void 0;
690
- if (declared !== void 0) return declared;
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 name of the top-level declaration a node sits in: the function, class or first const of
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 node - Any node.
699
- * @returns The declared name, or `undefined` for a statement that declares none.
535
+ * @param opening - The opening or self-closing element.
536
+ * @returns Its hits.
700
537
  */
701
- function enclosingName(typescript, node) {
702
- let statement = node;
703
- while (statement.parent !== void 0 && !typescript.isSourceFile(statement.parent)) statement = statement.parent;
704
- if (typescript.isFunctionDeclaration(statement) || typescript.isClassDeclaration(statement)) return statement.name?.text;
705
- if (!typescript.isVariableStatement(statement)) return void 0;
706
- const [first] = statement.declarationList.declarations;
707
- return first !== void 0 && typescript.isIdentifier(first.name) ? first.name.text : void 0;
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
- * The one expression a function returns, when it has exactly one.
556
+ * Collects every JSX key of a file, in source order.
715
557
  *
716
558
  * @param typescript - The TypeScript module.
717
- * @param node - The function.
718
- * @returns The returned expression, or `undefined`.
559
+ * @param source - The parsed file.
560
+ * @returns The hits; a key that reads `*` is among them.
719
561
  */
720
- function returnedBy(typescript, node) {
721
- const body = node.body;
722
- if (body === void 0) return void 0;
723
- if (!typescript.isBlock(body)) return body;
724
- const returns = [];
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.forEachChild(body, collect);
730
- return returns.length === 1 ? returns[0]?.expression : void 0;
568
+ if (source.languageVariant === typescript.LanguageVariant.JSX) visit(source);
569
+ return hits;
731
570
  }
732
571
  /**
733
- * Reads a name: a bound parameter, a const, or a destructured `id` prop.
572
+ * Whether a hit holds nothing literal: its key reads `*` alone.
734
573
  *
735
- * @param typescript - The TypeScript module.
736
- * @param identifier - The name.
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 readName(typescript, identifier, bindings, depth) {
742
- const bound = bindings.get(identifier.text);
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
- * Reads `props.id`: the `id` of a parameter object becomes `{id}`.
581
+ * The anchor of a hit.
751
582
  *
752
- * @param typescript - The TypeScript module.
753
- * @param access - The property access.
754
- * @param bindings - The parameters bound by the call being read.
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 readAccess(typescript, access, bindings) {
758
- const owner = access.expression;
759
- return access.name.text === "id" && typescript.isIdentifier(owner) && !bindings.has(owner.text) && meaningOf(typescript, owner)?.kind === "param" ? ID : "*";
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
- * Reads a call of a same-file function: its one returned expression, with each parameter bound
763
- * to the pattern of its argument.
598
+ * The unresolved reason of a key that reads `*`.
764
599
  *
765
- * @param typescript - The TypeScript module.
766
- * @param call - The call.
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 readCall$1(typescript, call, bindings, depth) {
772
- const callee = call.expression;
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 a template: its literal parts stay, each span is read into a pattern.
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 template - The template expression.
790
- * @param bindings - The parameters bound by the call being read.
791
- * @param depth - How deep the reading is.
792
- * @returns The pattern.
793
- */
794
- function readTemplate(typescript, template, bindings, depth) {
795
- const spans = template.templateSpans.map((span) => read(typescript, span.expression, bindings, depth + 1) + span.literal.text);
796
- return template.head.text + spans.join("");
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
- * Reads one expression into a pattern.
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 node - The expression.
803
- * @param bindings - The parameters bound by the call being read.
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 read(typescript, node, bindings, depth) {
808
- const expression = unwrap(typescript, node);
809
- if (depth > MAX_DEPTH) return "*";
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
- * Reduces a key expression to its pattern, with neighbouring `*` holes merged into one.
643
+ * The component names a file renders, each once.
820
644
  *
821
645
  * @param typescript - The TypeScript module.
822
- * @param expression - The expression of a JSX `key` attribute.
823
- * @returns The pattern: `card*Picture`, `{id}Close`, `hudRow`, or `*` when nothing is literal.
646
+ * @param source - The parsed file.
647
+ * @returns The names, sorted; none for a file without JSX.
824
648
  */
825
- function patternOf(typescript, expression) {
826
- return read(typescript, expression, /* @__PURE__ */ new Map(), 0).replaceAll(/\*+/g, "*");
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
- * The literal head of a pattern: the text before its first hole.
671
+ * Whether a name is a definer whose calls make keys.
830
672
  *
831
- * @param pattern - A pattern.
832
- * @returns The head, empty when the pattern starts with a hole.
673
+ * @param name - A name.
674
+ * @returns True for the definers of `DEFINERS`.
833
675
  * @example
834
676
  * ```ts
835
- * stemOf("card*Picture"); // "card"
677
+ * isDefiner("defineScene"); // true
836
678
  * ```
837
679
  */
838
- function stemOf(pattern) {
839
- const holes = [pattern.indexOf("*"), pattern.indexOf(ID)].filter((at) => at >= 0);
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
- * The expression a JSX attribute holds: the string of `key="x"` or the code of `key={x}`.
684
+ * Whether a statement carries a modifier.
850
685
  *
851
686
  * @param typescript - The TypeScript module.
852
- * @param attribute - The attribute.
853
- * @returns The expression, or `undefined` for `<a key />` and `key={}`.
687
+ * @param node - The statement.
688
+ * @param kind - `ExportKeyword` or `DefaultKeyword`.
689
+ * @returns True when the modifier is there.
854
690
  */
855
- function attributeValue(typescript, attribute) {
856
- const initializer = attribute.initializer;
857
- if (initializer === void 0) return void 0;
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
- * How a key expression is written.
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 expression - The key expression.
866
- * @returns `literal` for a string, `template` for a template with holes, `ident` for the rest.
699
+ * @param name - The binding name.
700
+ * @returns The declared names.
867
701
  */
868
- function kindOf(typescript, expression) {
869
- const inner = unwrap(typescript, expression);
870
- if (typescript.isStringLiteralLike(inner)) return "literal";
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
- * The upper-case top-level declaration a node sits in: the component an `{id}` pattern belongs to.
707
+ * Records the value imports of one import statement.
875
708
  *
876
709
  * @param typescript - The TypeScript module.
877
- * @param node - The attribute.
878
- * @returns The component name, or `undefined` inside a lower-case helper.
710
+ * @param statement - The import statement.
711
+ * @param draft - The facts so far.
879
712
  */
880
- function componentAround(typescript, node) {
881
- const name = enclosingName(typescript, node);
882
- return name !== void 0 && COMPONENT_TAG.test(name) ? name : void 0;
883
- }
884
- /**
885
- * Quotes a key expression for an unresolved reason: one line, cut to a readable length.
886
- *
887
- * @param text - The expression as written.
888
- * @returns The quote.
889
- * @example
890
- * ```ts
891
- * quoteOf("card.key"); // "card.key"
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 quoteOf(text) {
895
- const line = text.replaceAll(/\s+/g, " ");
896
- return line.length > QUOTE_LENGTH ? `${line.slice(0, QUOTE_LENGTH)}…` : line;
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 hit of a `key` attribute.
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 typescript - The TypeScript module.
902
- * @param attribute - The attribute.
903
- * @param place - The element and its tag.
904
- * @param place.element - The whole element.
905
- * @param place.tag - The tag as written.
906
- * @returns The hit, or `undefined` for a key without a value.
907
- */
908
- function keyHit(typescript, attribute, place) {
909
- const expression = attributeValue(typescript, attribute);
910
- if (expression === void 0) return void 0;
911
- const kind = kindOf(typescript, expression);
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 hit of a literal `id` prop on a component.
1196
+ * The component names one file renders.
928
1197
  *
929
- * @param typescript - The TypeScript module.
930
- * @param attribute - The attribute.
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 idHit(typescript, attribute, place) {
937
- const expression = attributeValue(typescript, attribute);
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
- * The hits of the attributes of one opening or self-closing element.
1205
+ * Adds one use of a rendered binding to every component entry bound to it.
951
1206
  *
952
- * @param typescript - The TypeScript module.
953
- * @param opening - The opening or self-closing element.
954
- * @returns Its hits.
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 hitsOf(typescript, opening) {
957
- const place = {
958
- element: typescript.isJsxOpeningElement(opening) ? opening.parent : opening,
959
- tag: opening.tagName.getText()
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
- * Collects every JSX key of a file, in source order.
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 typescript - The TypeScript module.
977
- * @param source - The parsed file.
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 collectJsx(typescript, source) {
981
- const hits = [];
982
- const visit = (node) => {
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
- * Whether a hit holds nothing literal: its key reads `*` alone.
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 hit - A hit.
993
- * @returns True for a key nothing in the file can read.
1232
+ * @param symbols - The keys as collected.
1233
+ * @returns The symbols of the index.
994
1234
  */
995
- function isOpaque(hit) {
996
- return hit.key === "*";
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
- * The anchor of a hit.
1249
+ * Freezes a value and everything it holds.
1000
1250
  *
1001
- * @param file - The root-relative path of the file.
1002
- * @param hit - The hit.
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 anchorOfHit(file, hit) {
1006
- return {
1007
- path: file,
1008
- key: hit.key,
1009
- kind: hit.kind,
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
- * The unresolved reason of a key that reads `*`.
1261
+ * Assembles the index of a catalog.
1016
1262
  *
1017
- * @param hit - The hit.
1018
- * @returns The reason: `JSX key "card.key" on <row> resolves to "*"`.
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 opaqueReason(hit) {
1021
- return `JSX key "${hit.written}" on <${hit.tag}> resolves to "*"`;
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 (propertyName(typescript, property) !== name) continue;
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 = propertyName(typescript, property);
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 = propertyName(typescript, property);
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
- * Reads a style: only one bound to a module const has a key, `style:<path>#<binding>`.
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 binding = bindingOf(reading.typescript, call);
1598
+ const { typescript, path } = reading;
1599
+ const binding = bindingOf(typescript, call);
1281
1600
  if (binding !== void 0) {
1282
- reading.definitions.push({
1283
- key: `style:${reading.path}#${binding}`,
1284
- anchor: {
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 around = enclosingName(reading.typescript, call);
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, in source order.
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 jsxHits - The JSX hits of the parse; collected on this call by default.
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, jsxHits = () => collectJsx(typescript, source)) {
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, `{id}` already filled.
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 an `id=` prop fills an `{id}` pattern into the wanted key. A pattern written in a
1858
- * component takes only the props of that component; one written in a helper takes any.
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 `{id}` pattern.
1861
- * @param idAttribute - The anchor of the `id=` prop.
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, idAttribute, wanted) {
1866
- const isSameComponent = pattern.component === void 0 || pattern.component === idAttribute.component;
1867
- const filled = (pattern.key ?? "").replaceAll(ID, idAttribute.key ?? "");
1868
- return isSameComponent && matchesPattern(filled, wanted);
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(ID, "").replaceAll("*", "").length;
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 `{id}` pattern filled with an `id=`
1895
- * prop (the pattern, then the prop), then each `*` pattern as a wildcard. Within each tier the
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 { idPatterns, wildPatterns, idAttributes } = shapes;
2240
+ const { holePatterns, wildPatterns, idProps } = shapes;
1904
2241
  const reached = [];
1905
- for (const pattern of idPatterns.toSorted(bySpecificity)) for (const idAttribute of idAttributes.filter((item) => fills(pattern, item, wanted))) reached.push(pattern, idAttribute);
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 `{id}`), the patterns that read as it.
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.includes("{id}") ? patternAnchors(shapesOf(session), key.slice(4)) : [];
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, view.jsx).map((place) => ({
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,