@moku-labs/game 0.5.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.
@@ -0,0 +1,3113 @@
1
+ import { createBrandConsole } from "@moku-labs/common/cli";
2
+ import { readFile, readdir, realpath, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { watch } from "node:fs";
6
+ //#region src/project/extract/scope.ts
7
+ /**
8
+ * Unwraps the expressions that change no value: parentheses, `as`, `satisfies`, `!` and `<T>`.
9
+ *
10
+ * @param typescript - The TypeScript module.
11
+ * @param node - An expression.
12
+ * @returns The expression inside.
13
+ */
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;
18
+ }
19
+ /**
20
+ * The text of a property name written literally: an identifier, a string or a number.
21
+ *
22
+ * @param typescript - The TypeScript module.
23
+ * @param name - A property name.
24
+ * @returns The text, or `undefined` for a computed or a private name.
25
+ */
26
+ function literalNameOf(typescript, name) {
27
+ return typescript.isIdentifier(name) || typescript.isStringLiteral(name) || typescript.isNumericLiteral(name) ? name.text : void 0;
28
+ }
29
+ /**
30
+ * The name of a property-like node, when it is written literally: a property, a shorthand, a
31
+ * method or an accessor.
32
+ *
33
+ * @param typescript - The TypeScript module.
34
+ * @param node - Any node.
35
+ * @returns The name, or `undefined` for a spread, a computed name or a node without a name.
36
+ */
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;
39
+ }
40
+ /**
41
+ * Whether a node holds statements: a block, a source file, a module block or a case clause.
42
+ *
43
+ * @param node - A node.
44
+ * @returns True when it has a `statements` list.
45
+ */
46
+ function holdsStatements(node) {
47
+ return "statements" in node && Array.isArray(node.statements);
48
+ }
49
+ /**
50
+ * Whether a node is a function whose parameters a name can be.
51
+ *
52
+ * @param typescript - The TypeScript module.
53
+ * @param node - A node.
54
+ * @returns True for function declarations, expressions, arrows and methods.
55
+ */
56
+ function isFunctionScope(typescript, node) {
57
+ return typescript.isFunctionDeclaration(node) || typescript.isFunctionExpression(node) || typescript.isArrowFunction(node) || typescript.isMethodDeclaration(node);
58
+ }
59
+ /**
60
+ * The meaning of a `const` declarator: a function when its initializer is one.
61
+ *
62
+ * @param typescript - The TypeScript module.
63
+ * @param initializer - The initializer of the const.
64
+ * @returns The meaning.
65
+ */
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
+ };
75
+ }
76
+ /**
77
+ * Looks for a name among the statements of one block.
78
+ *
79
+ * @param typescript - The TypeScript module.
80
+ * @param holder - The block.
81
+ * @param name - The name.
82
+ * @returns What the name means there, or `undefined`.
83
+ */
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
+ }
95
+ }
96
+ /**
97
+ * Looks for a name among the names of a destructured parameter (`({ id }) => …`).
98
+ *
99
+ * @param typescript - The TypeScript module.
100
+ * @param pattern - The destructure.
101
+ * @param name - The name.
102
+ * @returns The parameter meaning with the property it takes, or `undefined`.
103
+ */
104
+ function destructuredIn(typescript, pattern, name) {
105
+ for (const element of pattern.elements) {
106
+ if (!(typescript.isBindingElement(element) && typescript.isIdentifier(element.name) && element.name.text === name)) continue;
107
+ const property = element.propertyName;
108
+ return {
109
+ kind: "param",
110
+ property: property !== void 0 && typescript.isIdentifier(property) ? property.text : name
111
+ };
112
+ }
113
+ }
114
+ /**
115
+ * Looks for a name among the parameters of one function: a plain parameter, or a property of a
116
+ * destructured one.
117
+ *
118
+ * @param typescript - The TypeScript module.
119
+ * @param scope - The function.
120
+ * @param name - The name.
121
+ * @returns The parameter meaning, or `undefined`.
122
+ */
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;
130
+ }
131
+ }
132
+ /**
133
+ * Says what a name means at the place it is used, walking up from there.
134
+ *
135
+ * @param typescript - The TypeScript module.
136
+ * @param identifier - The name where it is used.
137
+ * @returns What it means, or `undefined` when the file does not declare it.
138
+ */
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
+ }
147
+ }
148
+ /**
149
+ * The statement directly under the file that a node sits in.
150
+ *
151
+ * @param typescript - The TypeScript module.
152
+ * @param node - Any node.
153
+ * @returns The statement, or the file itself for the file.
154
+ */
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;
159
+ }
160
+ /**
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.
167
+ */
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
+ }
175
+ /**
176
+ * The function a const declarator holds, through parentheses, `as` and `satisfies`.
177
+ *
178
+ * @param typescript - The TypeScript module.
179
+ * @param declaration - The declarator.
180
+ * @returns The arrow or function expression, or `undefined` for any other value.
181
+ */
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;
185
+ }
186
+ /**
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.
190
+ *
191
+ * @param typescript - The TypeScript module.
192
+ * @param statement - A statement directly under the file.
193
+ * @returns The functions, in source order.
194
+ */
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
+ });
213
+ }
214
+ /**
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.
218
+ *
219
+ * @param typescript - The TypeScript module.
220
+ * @param node - Any node.
221
+ * @returns The name of the function, or `undefined`.
222
+ */
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;
225
+ }
226
+ /**
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`.
229
+ *
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.
233
+ */
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);
240
+ }
241
+ /**
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`.
245
+ */
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;
251
+ /**
252
+ * The hole a prop of a component leaves in a key.
253
+ *
254
+ * @param property - The name of the prop.
255
+ * @returns The hole: `{amountKey}` for `amountKey`.
256
+ * @example
257
+ * ```ts
258
+ * holeOf("amountKey"); // "{amountKey}"
259
+ * ```
260
+ */
261
+ function holeOf(property) {
262
+ return `{${property}}`;
263
+ }
264
+ /**
265
+ * Whether a pattern holds a hole a prop fills.
266
+ *
267
+ * @param pattern - A pattern or a key.
268
+ * @returns True when it holds `{id}`, `{amountKey}` or another prop hole.
269
+ * @example
270
+ * ```ts
271
+ * hasHole("{id}Close"); // true
272
+ * ```
273
+ */
274
+ function hasHole(pattern) {
275
+ return pattern.search(HOLE) !== -1;
276
+ }
277
+ /**
278
+ * The hole a prop of a parameter object leaves: its own hole for a key-carrying prop, `*` else.
279
+ *
280
+ * @param property - The name of the prop, when the parameter is read through one.
281
+ * @returns The hole.
282
+ * @example
283
+ * ```ts
284
+ * propertyHole("tab"); // "*"
285
+ * ```
286
+ */
287
+ function propertyHole(property) {
288
+ return property !== void 0 && KEY_PROP.test(property) ? holeOf(property) : "*";
289
+ }
290
+ /**
291
+ * The one expression a function returns, when it has exactly one.
292
+ *
293
+ * @param typescript - The TypeScript module.
294
+ * @param node - The function.
295
+ * @returns The returned expression, or `undefined`.
296
+ */
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);
305
+ };
306
+ typescript.forEachChild(body, collect);
307
+ return returns.length === 1 ? returns[0]?.expression : void 0;
308
+ }
309
+ /**
310
+ * Reads a name: a bound parameter, a const, or a destructured key-carrying prop.
311
+ *
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.
317
+ */
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 "*";
325
+ }
326
+ /**
327
+ * Reads `props.id` or `props.amountKey`: a key-carrying prop of a parameter object becomes its
328
+ * hole, any other prop `*`.
329
+ *
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.
334
+ */
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) : "*";
338
+ }
339
+ /**
340
+ * Reads a call of a same-file function: its one returned expression, with each parameter bound
341
+ * to the pattern of its argument.
342
+ *
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.
348
+ */
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));
360
+ }
361
+ return read(typescript, returned, inner, depth + 1);
362
+ }
363
+ /**
364
+ * Reads a template: its literal parts stay, each span is read into a pattern.
365
+ *
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.
371
+ */
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("");
375
+ }
376
+ /**
377
+ * Reads one expression into a pattern.
378
+ *
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.
384
+ */
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 "*";
395
+ }
396
+ /**
397
+ * Reduces a key expression to its pattern, with neighbouring `*` holes merged into one.
398
+ *
399
+ * @param typescript - The TypeScript module.
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.
403
+ */
404
+ function patternOf(typescript, expression) {
405
+ return read(typescript, expression, /* @__PURE__ */ new Map(), 0).replaceAll(/\*+/g, "*");
406
+ }
407
+ /**
408
+ * The literal head of a pattern: the text before its first hole.
409
+ *
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
+ * ```
416
+ */
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));
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]/;
427
+ /**
428
+ * The expression a JSX attribute holds: the string of `key="x"` or the code of `key={x}`.
429
+ *
430
+ * @param typescript - The TypeScript module.
431
+ * @param attribute - The attribute.
432
+ * @returns The expression, or `undefined` for `<a key />` and `key={}`.
433
+ */
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;
439
+ }
440
+ /**
441
+ * How a key expression is written.
442
+ *
443
+ * @param typescript - The TypeScript module.
444
+ * @param expression - The key expression.
445
+ * @returns `literal` for a string, `template` for a template with holes, `ident` for the rest.
446
+ */
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";
451
+ }
452
+ /**
453
+ * The upper-case top-level declaration a node sits in: the component a `{prop}` pattern belongs to.
454
+ *
455
+ * @param typescript - The TypeScript module.
456
+ * @param node - The attribute.
457
+ * @returns The component name, or `undefined` inside a lower-case helper.
458
+ */
459
+ function componentAround(typescript, node) {
460
+ const name = enclosingName(typescript, node);
461
+ return name !== void 0 && COMPONENT_TAG.test(name) ? name : void 0;
462
+ }
463
+ /**
464
+ * Quotes a key expression for an unresolved reason: one line, cut to a readable length.
465
+ *
466
+ * @param text - The expression as written.
467
+ * @returns The quote.
468
+ * @example
469
+ * ```ts
470
+ * quoteOf("card.key"); // "card.key"
471
+ * ```
472
+ */
473
+ function quoteOf(text) {
474
+ const line = text.replaceAll(/\s+/g, " ");
475
+ return line.length > QUOTE_LENGTH ? `${line.slice(0, QUOTE_LENGTH)}…` : line;
476
+ }
477
+ /**
478
+ * The hit of a `key` attribute.
479
+ *
480
+ * @param typescript - The TypeScript module.
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.
486
+ */
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
+ };
504
+ }
505
+ /**
506
+ * The hit of a literal key-carrying prop on a component: `id="settingsBoard"`,
507
+ * `amountKey="giftReward"`.
508
+ *
509
+ * @param typescript - The TypeScript module.
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.
516
+ */
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
+ };
530
+ }
531
+ /**
532
+ * The hits of the attributes of one opening or self-closing element.
533
+ *
534
+ * @param typescript - The TypeScript module.
535
+ * @param opening - The opening or self-closing element.
536
+ * @returns Its hits.
537
+ */
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;
554
+ }
555
+ /**
556
+ * Collects every JSX key of a file, in source order.
557
+ *
558
+ * @param typescript - The TypeScript module.
559
+ * @param source - The parsed file.
560
+ * @returns The hits; a key that reads `*` is among them.
561
+ */
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);
567
+ };
568
+ if (source.languageVariant === typescript.LanguageVariant.JSX) visit(source);
569
+ return hits;
570
+ }
571
+ /**
572
+ * Whether a hit holds nothing literal: its key reads `*` alone.
573
+ *
574
+ * @param hit - A hit.
575
+ * @returns True for a key nothing in the file can read.
576
+ */
577
+ function isOpaque(hit) {
578
+ return hit.key === "*";
579
+ }
580
+ /**
581
+ * The anchor of a hit.
582
+ *
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.
586
+ */
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
+ };
596
+ }
597
+ /**
598
+ * The unresolved reason of a key that reads `*`.
599
+ *
600
+ * @param hit - The hit.
601
+ * @returns The reason: `JSX key "card.key" on <row> resolves to "*"`.
602
+ */
603
+ function opaqueReason(hit) {
604
+ return `JSX key "${hit.written}" on <${hit.tag}> resolves to "*"`;
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";
612
+ /**
613
+ * Reads the plain components of a file: the module-level upper-case functions of a `.tsx` file.
614
+ *
615
+ * @param typescript - The TypeScript module.
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
+ }));
629
+ }
630
+ /**
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.
633
+ *
634
+ * @param typescript - The TypeScript module.
635
+ * @param tag - The tag name of an element.
636
+ * @returns The name, or `undefined` for an element of the engine and for `this`.
637
+ */
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;
641
+ }
642
+ /**
643
+ * The component names a file renders, each once.
644
+ *
645
+ * @param typescript - The TypeScript module.
646
+ * @param source - The parsed file.
647
+ * @returns The names, sorted; none for a file without JSX.
648
+ */
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();
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
+ ];
670
+ /**
671
+ * Whether a name is a definer whose calls make keys.
672
+ *
673
+ * @param name - A name.
674
+ * @returns True for the definers of `DEFINERS`.
675
+ * @example
676
+ * ```ts
677
+ * isDefiner("defineScene"); // true
678
+ * ```
679
+ */
680
+ function isDefiner(name) {
681
+ return DEFINERS.includes(name);
682
+ }
683
+ /**
684
+ * Whether a statement carries a modifier.
685
+ *
686
+ * @param typescript - The TypeScript module.
687
+ * @param node - The statement.
688
+ * @param kind - `ExportKeyword` or `DefaultKeyword`.
689
+ * @returns True when the modifier is there.
690
+ */
691
+ function hasModifier(typescript, node, kind) {
692
+ if (!typescript.canHaveModifiers(node)) return false;
693
+ return typescript.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false;
694
+ }
695
+ /**
696
+ * The names a binding declares: the identifier, or every name of a destructure.
697
+ *
698
+ * @param typescript - The TypeScript module.
699
+ * @param name - The binding name.
700
+ * @returns The declared names.
701
+ */
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) : []);
705
+ }
706
+ /**
707
+ * Records the value imports of one import statement.
708
+ *
709
+ * @param typescript - The TypeScript module.
710
+ * @param statement - The import statement.
711
+ * @param draft - The facts so far.
712
+ */
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.
1169
+ */
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
+ }
1178
+ }
1179
+ /**
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.
1182
+ *
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;
1194
+ }
1195
+ /**
1196
+ * The component names one file renders.
1197
+ *
1198
+ * @param record - The record of the file.
1199
+ * @returns The names; none for a broken file.
1200
+ */
1201
+ function renderedBy(record) {
1202
+ return record.good === void 0 ? [] : record.extracted?.result.rendered ?? [];
1203
+ }
1204
+ /**
1205
+ * Adds one use of a rendered binding to every component entry bound to it.
1206
+ *
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.
1210
+ */
1211
+ function useComponent(byBinding, file, binding) {
1212
+ for (const entry of byBinding.get(binding) ?? []) entry.uses.push({
1213
+ path: file,
1214
+ binding
1215
+ });
1216
+ }
1217
+ /**
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`.
1220
+ *
1221
+ * @param assembly - The records and module facts.
1222
+ * @param symbols - The keys so far.
1223
+ */
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);
1227
+ }
1228
+ /**
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.
1231
+ *
1232
+ * @param symbols - The keys as collected.
1233
+ * @returns The symbols of the index.
1234
+ */
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;
1247
+ }
1248
+ /**
1249
+ * Freezes a value and everything it holds.
1250
+ *
1251
+ * @param value - A plain value.
1252
+ * @returns The same value, frozen.
1253
+ */
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;
1259
+ }
1260
+ /**
1261
+ * Assembles the index of a catalog.
1262
+ *
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.
1266
+ */
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
+ });
1299
+ }
1300
+ //#endregion
1301
+ //#region src/project/extract/definitions.ts
1302
+ /** The key prefix of each definer that takes its id as the first argument. */
1303
+ const ID_PREFIXES = {
1304
+ defineFlow: "flow",
1305
+ defineFeature: "feature",
1306
+ defineScene: "scene",
1307
+ defineEmitter: "emitter",
1308
+ defineComponent: "component"
1309
+ };
1310
+ /**
1311
+ * The text of a string literal or a template without holes.
1312
+ *
1313
+ * @param typescript - The TypeScript module.
1314
+ * @param node - An expression, if any.
1315
+ * @returns The text, or `undefined` for anything else.
1316
+ */
1317
+ function literalOf(typescript, node) {
1318
+ const inner = node === void 0 ? void 0 : unwrap(typescript, node);
1319
+ return inner !== void 0 && typescript.isStringLiteralLike(inner) ? inner.text : void 0;
1320
+ }
1321
+ /**
1322
+ * The value of a named property of an object literal; for a shorthand, the name itself.
1323
+ *
1324
+ * @param typescript - The TypeScript module.
1325
+ * @param object - The object literal.
1326
+ * @param name - The property name.
1327
+ * @returns The value, or `undefined` when the object has no such property.
1328
+ */
1329
+ function propertyValue(typescript, object, name) {
1330
+ for (const property of object.properties) {
1331
+ if (propertyNameOf(typescript, property) !== name) continue;
1332
+ if (typescript.isPropertyAssignment(property)) return property.initializer;
1333
+ if (typescript.isShorthandPropertyAssignment(property)) return property.name;
1334
+ }
1335
+ }
1336
+ /**
1337
+ * The object literal an expression is, directly or through a same-file const.
1338
+ *
1339
+ * @param typescript - The TypeScript module.
1340
+ * @param node - An expression, if any.
1341
+ * @returns The object literal, or `undefined`.
1342
+ */
1343
+ function objectOf(typescript, node) {
1344
+ const inner = node === void 0 ? void 0 : unwrap(typescript, node);
1345
+ if (inner === void 0) return void 0;
1346
+ if (typescript.isObjectLiteralExpression(inner)) return inner;
1347
+ if (!typescript.isIdentifier(inner)) return void 0;
1348
+ const meaning = meaningOf(typescript, inner);
1349
+ const value = meaning?.kind === "const" ? unwrap(typescript, meaning.initializer) : void 0;
1350
+ return value !== void 0 && typescript.isObjectLiteralExpression(value) ? value : void 0;
1351
+ }
1352
+ /**
1353
+ * The const a call is bound to: `export const boardFlow = defineFlow(…)` at the top of the file.
1354
+ *
1355
+ * @param typescript - The TypeScript module.
1356
+ * @param call - The definer call.
1357
+ * @returns The binding, or `undefined` when the call is not the initializer of a module const.
1358
+ */
1359
+ function bindingOf(typescript, call) {
1360
+ let node = call.parent;
1361
+ while (typescript.isExpression(node)) node = node.parent;
1362
+ if (!typescript.isVariableDeclaration(node) || !typescript.isIdentifier(node.name)) return void 0;
1363
+ if (node.initializer === void 0 || unwrap(typescript, node.initializer) !== call) return void 0;
1364
+ const list = node.parent;
1365
+ return (list.flags & typescript.NodeFlags.Const) !== 0 && typescript.isVariableStatement(list.parent) && typescript.isSourceFile(list.parent.parent) ? node.name.text : void 0;
1366
+ }
1367
+ /**
1368
+ * The definer a call calls, by its callee as written: `defineFlow(…)` or `kit.defineFlow(…)`.
1369
+ *
1370
+ * @param typescript - The TypeScript module.
1371
+ * @param call - The call.
1372
+ * @param definers - The definers of the file.
1373
+ * @returns The definer, or `undefined` for any other call.
1374
+ */
1375
+ function definerOfCall(typescript, call, definers) {
1376
+ const callee = call.expression;
1377
+ if (typescript.isIdentifier(callee)) return definers.get(callee.text);
1378
+ if (!typescript.isPropertyAccessExpression(callee) || !typescript.isIdentifier(callee.expression)) return;
1379
+ return definers.get(`${callee.expression.text}.${callee.name.text}`);
1380
+ }
1381
+ /**
1382
+ * Adds a key bound to the const of a call, or anchored by its id when the call has no const.
1383
+ *
1384
+ * @param reading - The file being read.
1385
+ * @param call - The definer call.
1386
+ * @param key - The key.
1387
+ * @param id - The literal id, the anchor key when there is no binding.
1388
+ * @param bound - What the anchor adds to the binding: the `name` key of a projection.
1389
+ */
1390
+ function define(reading, call, key, id, bound = {}) {
1391
+ const binding = bindingOf(reading.typescript, call);
1392
+ const anchor = binding === void 0 ? {
1393
+ path: reading.path,
1394
+ key: id
1395
+ } : {
1396
+ path: reading.path,
1397
+ binding,
1398
+ ...bound
1399
+ };
1400
+ reading.definitions.push({
1401
+ key,
1402
+ anchor
1403
+ });
1404
+ }
1405
+ /**
1406
+ * Notes something the file holds that cannot be read.
1407
+ *
1408
+ * @param reading - The file being read.
1409
+ * @param reason - What and why.
1410
+ */
1411
+ function unresolve(reading, reason) {
1412
+ reading.unresolved.push({
1413
+ path: reading.path,
1414
+ reason
1415
+ });
1416
+ }
1417
+ /**
1418
+ * The plain name a table entry's value is: `merge` for `{ merge }` and for `{ merge: merge }`.
1419
+ *
1420
+ * @param typescript - The TypeScript module.
1421
+ * @param property - The table entry.
1422
+ * @returns The name, or `undefined` for a slot, an inline node or anything else.
1423
+ */
1424
+ function refOf(typescript, property) {
1425
+ if (typescript.isShorthandPropertyAssignment(property)) return property.name.text;
1426
+ if (!typescript.isPropertyAssignment(property)) return void 0;
1427
+ const value = unwrap(typescript, property.initializer);
1428
+ return typescript.isIdentifier(value) ? value.text : void 0;
1429
+ }
1430
+ /**
1431
+ * Reads the `nodes` table of a flow into its entries.
1432
+ *
1433
+ * @param reading - The file being read.
1434
+ * @param call - The `defineFlow` call.
1435
+ * @param flow - The flow id.
1436
+ */
1437
+ function readTable(reading, call, flow) {
1438
+ const { typescript } = reading;
1439
+ const spec = objectOf(typescript, call.arguments[1]);
1440
+ const nodes = spec === void 0 ? void 0 : objectOf(typescript, propertyValue(typescript, spec, "nodes"));
1441
+ if (nodes === void 0) {
1442
+ unresolve(reading, `defineFlow "${flow}" nodes is not an object literal`);
1443
+ return;
1444
+ }
1445
+ const entries = [];
1446
+ for (const property of nodes.properties) {
1447
+ if (typescript.isSpreadAssignment(property)) {
1448
+ unresolve(reading, `defineFlow "${flow}" nodes has a spread`);
1449
+ continue;
1450
+ }
1451
+ const name = propertyNameOf(typescript, property);
1452
+ if (name === void 0) {
1453
+ unresolve(reading, `defineFlow "${flow}" node name is not a literal`);
1454
+ continue;
1455
+ }
1456
+ const ref = refOf(typescript, property);
1457
+ entries.push(ref === void 0 ? { name } : {
1458
+ name,
1459
+ ref
1460
+ });
1461
+ }
1462
+ const binding = bindingOf(typescript, call);
1463
+ reading.tables.push(binding === void 0 ? {
1464
+ flow,
1465
+ entries
1466
+ } : {
1467
+ flow,
1468
+ binding,
1469
+ entries
1470
+ });
1471
+ }
1472
+ /**
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.
1475
+ *
1476
+ * @param reading - The file being read.
1477
+ * @param call - The call.
1478
+ * @param definer - The definer.
1479
+ * @param prefix - The key prefix.
1480
+ */
1481
+ function readIdCall(reading, call, definer, prefix) {
1482
+ const id = literalOf(reading.typescript, call.arguments[0]);
1483
+ if (id === void 0) {
1484
+ unresolve(reading, `${definer} id is not a string literal`);
1485
+ return;
1486
+ }
1487
+ define(reading, call, `${prefix}:${id}`, id);
1488
+ if (definer === "defineFlow") readTable(reading, call, id);
1489
+ }
1490
+ /**
1491
+ * Reads a projection: its key is its `name`, its line the `name` property.
1492
+ *
1493
+ * @param reading - The file being read.
1494
+ * @param call - The call.
1495
+ */
1496
+ function readProjection(reading, call) {
1497
+ const { typescript } = reading;
1498
+ const spec = objectOf(typescript, call.arguments[0]);
1499
+ const name = spec === void 0 ? void 0 : literalOf(typescript, propertyValue(typescript, spec, "name"));
1500
+ if (name === void 0) {
1501
+ unresolve(reading, "projection name is not a string literal");
1502
+ return;
1503
+ }
1504
+ define(reading, call, `projection:${name}`, name, { key: "name" });
1505
+ }
1506
+ /**
1507
+ * Reads a text style table: every literal key is a key.
1508
+ *
1509
+ * @param reading - The file being read.
1510
+ * @param call - The call.
1511
+ */
1512
+ function readTextStyles(reading, call) {
1513
+ const { typescript } = reading;
1514
+ const table = call.arguments[0] === void 0 ? void 0 : unwrap(typescript, call.arguments[0]);
1515
+ if (table === void 0 || !typescript.isObjectLiteralExpression(table)) {
1516
+ unresolve(reading, "defineTextStyles argument is not an object literal");
1517
+ return;
1518
+ }
1519
+ const binding = bindingOf(typescript, call);
1520
+ for (const property of table.properties) {
1521
+ const key = propertyNameOf(typescript, property);
1522
+ if (key === void 0) {
1523
+ unresolve(reading, "defineTextStyles key is not a literal");
1524
+ continue;
1525
+ }
1526
+ const anchor = binding === void 0 ? {
1527
+ path: reading.path,
1528
+ key
1529
+ } : {
1530
+ path: reading.path,
1531
+ binding,
1532
+ key
1533
+ };
1534
+ reading.definitions.push({
1535
+ key: `textStyle:${key}`,
1536
+ anchor
1537
+ });
1538
+ }
1539
+ }
1540
+ /**
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.
1593
+ *
1594
+ * @param reading - The file being read.
1595
+ * @param call - The call.
1596
+ */
1597
+ function readStyle(reading, call) {
1598
+ const { typescript, path } = reading;
1599
+ const binding = bindingOf(typescript, call);
1600
+ if (binding !== void 0) {
1601
+ addStyle(reading, `style:${path}#${binding}`, {
1602
+ path,
1603
+ binding
1604
+ });
1605
+ return;
1606
+ }
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);
1614
+ unresolve(reading, `defineStyle${around === void 0 ? "" : ` in "${around}"`} is not bound to a module-level const`);
1615
+ }
1616
+ /**
1617
+ * Reads one definer call.
1618
+ *
1619
+ * @param reading - The file being read.
1620
+ * @param call - The call.
1621
+ * @param definer - The definer it calls.
1622
+ */
1623
+ function readCall(reading, call, definer) {
1624
+ const prefix = ID_PREFIXES[definer];
1625
+ if (prefix !== void 0) readIdCall(reading, call, definer, prefix);
1626
+ if (definer === "projection") readProjection(reading, call);
1627
+ if (definer === "defineTextStyles") readTextStyles(reading, call);
1628
+ if (definer === "defineStyle") readStyle(reading, call);
1629
+ }
1630
+ /**
1631
+ * Adds the JSX keys of the file; the same key twice in one file is one anchor, since `find`
1632
+ * answers every place of an anchor in its file.
1633
+ *
1634
+ * @param reading - The file being read.
1635
+ * @param source - The parsed file.
1636
+ */
1637
+ function readJsx(reading, source) {
1638
+ const seen = /* @__PURE__ */ new Set();
1639
+ for (const hit of collectJsx(reading.typescript, source)) {
1640
+ if (isOpaque(hit)) {
1641
+ unresolve(reading, opaqueReason(hit));
1642
+ continue;
1643
+ }
1644
+ const anchor = anchorOfHit(reading.path, hit);
1645
+ const identity = JSON.stringify(anchor);
1646
+ if (seen.has(identity)) continue;
1647
+ seen.add(identity);
1648
+ reading.definitions.push({
1649
+ key: `jsx:${hit.key}`,
1650
+ anchor
1651
+ });
1652
+ }
1653
+ }
1654
+ /**
1655
+ * Reads the keys one file defines.
1656
+ *
1657
+ * @param typescript - The TypeScript module.
1658
+ * @param source - The parsed file.
1659
+ * @param path - The root-relative path of the file.
1660
+ * @param definers - The definers the file can call, from `definersOf`.
1661
+ * @returns The definitions, the node tables and the unresolved items in source order, and the
1662
+ * component names the file renders.
1663
+ */
1664
+ function extractFile(typescript, source, path, definers) {
1665
+ const reading = {
1666
+ typescript,
1667
+ path,
1668
+ definers,
1669
+ definitions: [],
1670
+ tables: [],
1671
+ unresolved: [],
1672
+ styles: /* @__PURE__ */ new Set()
1673
+ };
1674
+ const visit = (node) => {
1675
+ const definer = typescript.isCallExpression(node) ? definerOfCall(typescript, node, definers) : void 0;
1676
+ if (definer !== void 0 && typescript.isCallExpression(node)) readCall(reading, node, definer);
1677
+ typescript.forEachChild(node, visit);
1678
+ };
1679
+ if (definers.size > 0) visit(source);
1680
+ reading.definitions.push(...readComponents(typescript, source, path));
1681
+ readJsx(reading, source);
1682
+ return {
1683
+ definitions: reading.definitions,
1684
+ tables: reading.tables,
1685
+ unresolved: reading.unresolved,
1686
+ rendered: renderedOf(typescript, source)
1687
+ };
1688
+ }
1689
+ //#endregion
1690
+ //#region src/project/typescript.ts
1691
+ /**
1692
+ * Loads the optional peer `typescript`.
1693
+ *
1694
+ * @param load - The import; the real dynamic import by default.
1695
+ * @returns The TypeScript module.
1696
+ * @throws {Error} When the package is not installed.
1697
+ */
1698
+ async function loadTypeScript(load = () => import("typescript")) {
1699
+ try {
1700
+ const loaded = await load();
1701
+ return loaded.default ?? loaded;
1702
+ } catch {
1703
+ throw new Error("[game] The project index needs the \"typescript\" package.\n Install it as a dev dependency of the game.");
1704
+ }
1705
+ }
1706
+ /**
1707
+ * Formats the first parse error of a file as `path:line:col message`, 1-based.
1708
+ *
1709
+ * @param typescript - The TypeScript module.
1710
+ * @param source - The parsed file.
1711
+ * @param diagnostic - The parse error.
1712
+ * @returns The message.
1713
+ */
1714
+ function describeError(typescript, source, diagnostic) {
1715
+ const { line, character } = source.getLineAndCharacterOfPosition(diagnostic.start);
1716
+ const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, " ");
1717
+ return `${source.fileName}:${line + 1}:${character + 1} ${message}`;
1718
+ }
1719
+ /**
1720
+ * Parses one file with parent links, as TSX for a `.tsx` path and as TS otherwise.
1721
+ *
1722
+ * @param typescript - The TypeScript module.
1723
+ * @param path - The root-relative path; it names the file in the error.
1724
+ * @param text - The source text.
1725
+ * @returns The tree and the first parse error.
1726
+ */
1727
+ function parseFile(typescript, path, text) {
1728
+ const kind = path.endsWith(".tsx") ? typescript.ScriptKind.TSX : typescript.ScriptKind.TS;
1729
+ const source = typescript.createSourceFile(path, text, typescript.ScriptTarget.Latest, true, kind);
1730
+ const [first] = source.parseDiagnostics ?? [];
1731
+ if (first === void 0) return { source };
1732
+ return {
1733
+ source,
1734
+ error: describeError(typescript, source, first)
1735
+ };
1736
+ }
1737
+ //#endregion
1738
+ //#region src/project/catalog.ts
1739
+ /**
1740
+ * @file project — the catalog: one record per indexed file and a cache of parses. A file is
1741
+ * parsed once per version of its bytes; a file that stops parsing keeps its last good parse, so
1742
+ * its keys stay in the index while it is broken. Building the index reads the module facts of
1743
+ * every good file, re-extracts only the files whose bytes or reachable definers changed, and
1744
+ * assembles the result.
1745
+ */
1746
+ /** How many parses the cache keeps; the oldest goes first. */
1747
+ const PARSE_CACHE_SIZE = 512;
1748
+ /**
1749
+ * Creates an empty catalog.
1750
+ *
1751
+ * @param typescript - The TypeScript module.
1752
+ * @returns The catalog.
1753
+ */
1754
+ function createCatalog(typescript) {
1755
+ return {
1756
+ typescript,
1757
+ records: /* @__PURE__ */ new Map(),
1758
+ parses: /* @__PURE__ */ new Map()
1759
+ };
1760
+ }
1761
+ /**
1762
+ * Parses one version of a file, or takes it from the cache.
1763
+ *
1764
+ * @param catalog - The catalog.
1765
+ * @param path - The root-relative path.
1766
+ * @param hash - The sha1 of the bytes.
1767
+ * @param text - The text of the bytes.
1768
+ * @returns The parse.
1769
+ */
1770
+ function parseCached(catalog, path, hash, text) {
1771
+ const id = `${path}\0${hash}`;
1772
+ const cached = catalog.parses.get(id);
1773
+ catalog.parses.delete(id);
1774
+ const parsed = cached ?? parseFile(catalog.typescript, path, text);
1775
+ catalog.parses.set(id, parsed);
1776
+ for (const old of catalog.parses.keys()) {
1777
+ if (catalog.parses.size <= PARSE_CACHE_SIZE) break;
1778
+ catalog.parses.delete(old);
1779
+ }
1780
+ return parsed;
1781
+ }
1782
+ /**
1783
+ * Takes in the bytes of a file. Unchanged bytes change nothing; bytes that parse replace the
1784
+ * good version; bytes that do not parse mark the file broken and keep the good version.
1785
+ *
1786
+ * @param catalog - The catalog.
1787
+ * @param path - The root-relative path.
1788
+ * @param bytes - The bytes on disk.
1789
+ * @returns True when the bytes differ from the ones last read.
1790
+ */
1791
+ function putFile(catalog, path, bytes) {
1792
+ const hash = sha1(bytes);
1793
+ const record = catalog.records.get(path);
1794
+ if (record?.hash === hash) return false;
1795
+ const text = new TextDecoder().decode(bytes);
1796
+ const parsed = parseCached(catalog, path, hash, text);
1797
+ if (parsed.error === void 0) {
1798
+ const good = {
1799
+ hash,
1800
+ text,
1801
+ module: readModule(catalog.typescript, parsed.source)
1802
+ };
1803
+ catalog.records.set(path, {
1804
+ hash,
1805
+ state: "ok",
1806
+ good
1807
+ });
1808
+ return true;
1809
+ }
1810
+ catalog.records.set(path, {
1811
+ hash,
1812
+ state: "broken",
1813
+ error: parsed.error,
1814
+ ...record?.good === void 0 ? {} : { good: record.good },
1815
+ ...record?.extracted === void 0 ? {} : { extracted: record.extracted }
1816
+ });
1817
+ return true;
1818
+ }
1819
+ /**
1820
+ * Drops a file that is gone.
1821
+ *
1822
+ * @param catalog - The catalog.
1823
+ * @param path - The root-relative path.
1824
+ * @returns True when the catalog held the file.
1825
+ */
1826
+ function dropFile(catalog, path) {
1827
+ return catalog.records.delete(path);
1828
+ }
1829
+ /**
1830
+ * Brings the keys of every good file up to date: a file is read again when its good version or
1831
+ * the definers it reaches changed since it was last read.
1832
+ *
1833
+ * @param catalog - The catalog.
1834
+ * @param modules - The module facts of every good file.
1835
+ */
1836
+ function refreshExtractions(catalog, modules) {
1837
+ for (const [path, record] of catalog.records) {
1838
+ const good = record.good;
1839
+ if (good === void 0) continue;
1840
+ const definers = definersOf(modules, path);
1841
+ const signature = [...definers].map(([callee, definer]) => `${callee}=${definer}`).join(";");
1842
+ const stamp = `${good.hash}|${signature}`;
1843
+ if (record.extracted?.definers === stamp) continue;
1844
+ const { source } = parseCached(catalog, path, good.hash, good.text);
1845
+ record.extracted = {
1846
+ definers: stamp,
1847
+ result: extractFile(catalog.typescript, source, path, definers)
1848
+ };
1849
+ }
1850
+ }
1851
+ /**
1852
+ * Builds the index of everything the catalog holds.
1853
+ *
1854
+ * @param catalog - The catalog.
1855
+ * @param manifest - The root-relative manifest path when the file exists.
1856
+ * @returns The index, frozen.
1857
+ */
1858
+ function buildIndex(catalog, manifest) {
1859
+ const files = [...catalog.records].toSorted(([first], [second]) => first < second ? -1 : 1);
1860
+ const modules = /* @__PURE__ */ new Map();
1861
+ for (const [path, record] of files) if (record.good !== void 0) modules.set(path, record.good.module);
1862
+ refreshExtractions(catalog, modules);
1863
+ return assembleIndex({
1864
+ files,
1865
+ modules
1866
+ }, manifest);
1867
+ }
1868
+ //#endregion
1869
+ //#region src/project/locate.ts
1870
+ /** The kind of a style key. */
1871
+ const STYLE_KIND = "style";
1872
+ /**
1873
+ * The place of a node: the line of one node and the range of another.
1874
+ *
1875
+ * @param source - The parsed file.
1876
+ * @param lineNode - The node whose start line is the line.
1877
+ * @param rangeNode - The node whose span is the range.
1878
+ * @returns The place.
1879
+ */
1880
+ function placeOf(source, lineNode, rangeNode) {
1881
+ const line = source.getLineAndCharacterOfPosition(lineNode.getStart(source)).line + 1;
1882
+ const start = source.getLineAndCharacterOfPosition(rangeNode.getStart(source));
1883
+ const end = source.getLineAndCharacterOfPosition(rangeNode.getEnd());
1884
+ return {
1885
+ line,
1886
+ range: [
1887
+ start.line + 1,
1888
+ start.character + 1,
1889
+ end.line + 1,
1890
+ end.character + 1
1891
+ ]
1892
+ };
1893
+ }
1894
+ /**
1895
+ * Whether a top-level statement declares a name.
1896
+ *
1897
+ * @param typescript - The TypeScript module.
1898
+ * @param statement - The statement.
1899
+ * @param binding - The name.
1900
+ * @returns True for a const, a destructured const, a function or a class of that name.
1901
+ */
1902
+ function declares(typescript, statement, binding) {
1903
+ if (typescript.isFunctionDeclaration(statement) || typescript.isClassDeclaration(statement)) return statement.name?.text === binding;
1904
+ if (!typescript.isVariableStatement(statement)) return false;
1905
+ return statement.declarationList.declarations.some((declaration) => {
1906
+ const name = declaration.name;
1907
+ if (typescript.isIdentifier(name)) return name.text === binding;
1908
+ return name.elements.some((element) => typescript.isBindingElement(element) && element.name.getText() === binding);
1909
+ });
1910
+ }
1911
+ /**
1912
+ * The shallowest property of a key inside a declaration, the first one at that depth.
1913
+ *
1914
+ * @param typescript - The TypeScript module.
1915
+ * @param root - The declaration statement.
1916
+ * @param key - The property name.
1917
+ * @returns The property, or `undefined`.
1918
+ */
1919
+ function propertyIn(typescript, root, key) {
1920
+ const queue = [root];
1921
+ for (let next = queue.shift(); next !== void 0; next = queue.shift()) {
1922
+ if (propertyNameOf(typescript, next) === key) return next;
1923
+ typescript.forEachChild(next, (child) => {
1924
+ queue.push(child);
1925
+ });
1926
+ }
1927
+ }
1928
+ /**
1929
+ * The first place a key is written: a string literal of that text, or a property of that name.
1930
+ *
1931
+ * @param typescript - The TypeScript module.
1932
+ * @param source - The parsed file.
1933
+ * @param key - The key.
1934
+ * @returns The place, or `undefined`.
1935
+ */
1936
+ function writtenAt(typescript, source, key) {
1937
+ const visit = (node) => {
1938
+ if (typescript.isStringLiteralLike(node) && node.text === key || propertyNameOf(typescript, node) === key) return node;
1939
+ return typescript.forEachChild(node, visit);
1940
+ };
1941
+ const found = visit(source);
1942
+ if (found === void 0) return void 0;
1943
+ const parent = found.parent;
1944
+ return placeOf(source, found, typescript.isPropertyAssignment(parent) || typescript.isCallExpression(parent) ? parent : found);
1945
+ }
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
+ /**
1986
+ * Reads an anchor on one parse of its file.
1987
+ *
1988
+ * @param typescript - The TypeScript module.
1989
+ * @param source - The parsed file.
1990
+ * @param anchor - The anchor.
1991
+ * @param context - The kind of its key, the JSX hits of the parse and the definers of the file.
1992
+ * @returns Every place of the anchor in the file; none when the file no longer holds it.
1993
+ */
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));
1996
+ if (anchor.binding === void 0) {
1997
+ const place = anchor.key === void 0 ? void 0 : writtenAt(typescript, source, anchor.key);
1998
+ return place === void 0 ? [] : [place];
1999
+ }
2000
+ const built = context.keyKind === STYLE_KIND ? locateFactory(typescript, source, anchor, context) : [];
2001
+ if (built.length > 0) return built;
2002
+ const binding = anchor.binding;
2003
+ const statement = source.statements.find((item) => declares(typescript, item, binding));
2004
+ if (statement === void 0) return [];
2005
+ const node = (anchor.key === void 0 ? void 0 : propertyIn(typescript, statement, anchor.key)) ?? statement;
2006
+ return [placeOf(source, node, node)];
2007
+ }
2008
+ //#endregion
2009
+ //#region src/project/paths.ts
2010
+ /**
2011
+ * @file project — which files the index reads, and the guard on every path it reads. Paths in and
2012
+ * out are root-relative POSIX. A read resolves the path against the real root with `realpath`, so
2013
+ * a `..` or a symlink that leaves the root is refused by name instead of read.
2014
+ */
2015
+ /** The folders a game never edits by hand: dependencies, builds, generated code and tests. */
2016
+ const SKIPPED_FOLDER = /^(?:node_modules|dist|generated|\.moku|\.git|__tests__|tests)$/;
2017
+ /** The source files the index reads. */
2018
+ const SOURCE_FILE = /\.tsx?$/;
2019
+ /** Test files, skipped wherever they sit. */
2020
+ const TEST_FILE = /\.(?:test|spec)\.tsx?$/;
2021
+ /** The hint under every root error. */
2022
+ const ROOT_HINT = "Pass the folder of the game as root.";
2023
+ /**
2024
+ * Whether a folder name is one the index never walks into.
2025
+ *
2026
+ * @param name - One folder name.
2027
+ * @returns True for `node_modules`, `dist`, `generated`, `.moku`, `.git`, `__tests__` and `tests`.
2028
+ * @example
2029
+ * ```ts
2030
+ * isSkippedFolder("generated"); // true
2031
+ * ```
2032
+ */
2033
+ function isSkippedFolder(name) {
2034
+ return SKIPPED_FOLDER.test(name);
2035
+ }
2036
+ /**
2037
+ * Whether a root-relative path sits under a skipped folder.
2038
+ *
2039
+ * @param file - A path with `/` or `\` separators.
2040
+ * @returns True when one of its folders is skipped.
2041
+ * @example
2042
+ * ```ts
2043
+ * isSkippedPath("node_modules/pixi.js/index.ts"); // true
2044
+ * ```
2045
+ */
2046
+ function isSkippedPath(file) {
2047
+ return file.split(/[/\\]/).slice(0, -1).some((folder) => isSkippedFolder(folder));
2048
+ }
2049
+ /**
2050
+ * Whether the index reads a root-relative path: a `.ts` or `.tsx` file outside the skipped
2051
+ * folders that is not a test.
2052
+ *
2053
+ * @param file - A root-relative POSIX path.
2054
+ * @returns True for a source file of the index.
2055
+ * @example
2056
+ * ```ts
2057
+ * isIndexedFile("features/ui/kit.tsx"); // true
2058
+ * ```
2059
+ */
2060
+ function isIndexedFile(file) {
2061
+ return SOURCE_FILE.test(file) && !TEST_FILE.test(file) && !isSkippedPath(file);
2062
+ }
2063
+ /**
2064
+ * Turns a path into its root-relative POSIX form.
2065
+ *
2066
+ * @param file - A path with any separators.
2067
+ * @returns The path with `/` separators, `.` segments resolved.
2068
+ * @example
2069
+ * ```ts
2070
+ * toPosix("./features\\ui\\kit.tsx"); // "features/ui/kit.tsx"
2071
+ * ```
2072
+ */
2073
+ function toPosix(file) {
2074
+ return path.posix.normalize(file.replaceAll("\\", "/"));
2075
+ }
2076
+ /**
2077
+ * Whether a file system error says nothing is at the path.
2078
+ *
2079
+ * @param error - What was thrown.
2080
+ * @returns True for `ENOENT` and `ENOTDIR`.
2081
+ */
2082
+ function isMissing(error) {
2083
+ const code = error instanceof Error && "code" in error ? error.code : void 0;
2084
+ return code === "ENOENT" || code === "ENOTDIR";
2085
+ }
2086
+ /**
2087
+ * Whether an absolute path lies inside a folder.
2088
+ *
2089
+ * @param folder - The absolute folder.
2090
+ * @param file - The absolute path.
2091
+ * @returns True for the folder itself and anything below it.
2092
+ */
2093
+ function isInside(folder, file) {
2094
+ const relative = path.relative(folder, file);
2095
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
2096
+ }
2097
+ /**
2098
+ * The error of a path that leaves the root.
2099
+ *
2100
+ * @param file - The path as it was given.
2101
+ * @param root - The real root.
2102
+ * @returns The error to throw.
2103
+ */
2104
+ function leavesRoot(file, root) {
2105
+ return /* @__PURE__ */ new Error(`[game] The path "${file}" leaves the project root.\n Pass a path inside "${root}", relative to it.`);
2106
+ }
2107
+ /**
2108
+ * Checks the root of a project and resolves it: it exists, it is a directory, and its real path
2109
+ * is the one every read is held to.
2110
+ *
2111
+ * @param root - The root as given, absolute or relative to the working directory.
2112
+ * @returns The real absolute path of the root.
2113
+ * @throws {Error} When the root does not exist or is not a directory.
2114
+ */
2115
+ async function openRoot(root) {
2116
+ const absolute = path.resolve(root);
2117
+ if (!(await stat(absolute).catch((error) => {
2118
+ if (!isMissing(error)) throw error;
2119
+ throw new Error(`[game] The project root "${absolute}" does not exist.\n ${ROOT_HINT}`);
2120
+ })).isDirectory()) throw new Error(`[game] The project root "${absolute}" is not a directory.\n ${ROOT_HINT}`);
2121
+ return realpath(absolute);
2122
+ }
2123
+ /**
2124
+ * Resolves a root-relative path to the real file, refusing any path that leaves the root.
2125
+ *
2126
+ * @param root - The real root.
2127
+ * @param file - The root-relative path.
2128
+ * @returns The real absolute path, or `undefined` when nothing is there.
2129
+ * @throws {Error} When the path leaves the root, by `..`, as an absolute path or by a symlink.
2130
+ */
2131
+ async function resolveInside(root, file) {
2132
+ const absolute = path.resolve(root, file);
2133
+ if (!isInside(root, absolute)) throw leavesRoot(file, root);
2134
+ let real;
2135
+ try {
2136
+ real = await realpath(absolute);
2137
+ } catch (error) {
2138
+ if (isMissing(error)) return;
2139
+ throw error;
2140
+ }
2141
+ if (!isInside(root, real)) throw leavesRoot(file, root);
2142
+ return real;
2143
+ }
2144
+ /**
2145
+ * Reads the bytes of a root-relative file through the guard. A path that leaves the root reads as
2146
+ * nothing here; `changed` refuses it by name with `resolveInside` first.
2147
+ *
2148
+ * @param root - The real root.
2149
+ * @param file - The root-relative path.
2150
+ * @returns The bytes, or `undefined` when the file is gone or lies outside the root.
2151
+ */
2152
+ async function readInside(root, file) {
2153
+ try {
2154
+ const real = await resolveInside(root, file);
2155
+ if (real !== void 0) return await readFile(real);
2156
+ } catch {}
2157
+ }
2158
+ //#endregion
2159
+ //#region src/project/find.ts
2160
+ /**
2161
+ * Whether a key matches a pattern whose `*` holes stand for any text.
2162
+ *
2163
+ * @param pattern - A pattern, its prop holes already filled.
2164
+ * @param key - The key without its prefix.
2165
+ * @returns True when the key reads as the pattern.
2166
+ * @example
2167
+ * ```ts
2168
+ * matchesPattern("card*Picture", "card2Picture"); // true
2169
+ * ```
2170
+ */
2171
+ function matchesPattern(pattern, key) {
2172
+ if (!pattern.includes("*")) return pattern === key;
2173
+ const parts = pattern.split("*").map((part) => part.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw`\$&`));
2174
+ return new RegExp(`^${parts.join(".*")}$`).test(key);
2175
+ }
2176
+ /**
2177
+ * The JSX shapes of the current index, read once per index: an index is frozen, and an update
2178
+ * replaces it with a new object.
2179
+ *
2180
+ * @param session - The open project.
2181
+ * @returns The shapes of `session.index`.
2182
+ */
2183
+ function shapesOf(session) {
2184
+ const cached = session.shapes.get(session.index);
2185
+ if (cached !== void 0) return cached;
2186
+ const shapes = jsxShapes(session.index);
2187
+ session.shapes.set(session.index, shapes);
2188
+ return shapes;
2189
+ }
2190
+ /**
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.
2194
+ *
2195
+ * @param pattern - The anchor of the pattern.
2196
+ * @param filler - The anchor of the prop: an `idProp` anchor.
2197
+ * @param wanted - The runtime key, without its prefix.
2198
+ * @returns True when the filled pattern reads as the key.
2199
+ */
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);
2206
+ }
2207
+ /**
2208
+ * How much literal text a pattern holds: a pattern with more of it says more about a key.
2209
+ *
2210
+ * @param anchor - The anchor of a pattern.
2211
+ * @returns The length of the pattern without its holes.
2212
+ * @example
2213
+ * ```ts
2214
+ * specificityOf({ path: "strip.tsx", key: "card*Picture" }); // 11
2215
+ * ```
2216
+ */
2217
+ function specificityOf(anchor) {
2218
+ return (anchor.key ?? "").replaceAll(HOLE, "").replaceAll("*", "").length;
2219
+ }
2220
+ /**
2221
+ * Orders patterns from the most specific to the least, keeping their order otherwise.
2222
+ *
2223
+ * @param first - One pattern anchor.
2224
+ * @param second - The other.
2225
+ * @returns A negative number when the first holds more literal text.
2226
+ */
2227
+ function bySpecificity(first, second) {
2228
+ return specificityOf(second) - specificityOf(first);
2229
+ }
2230
+ /**
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*`.
2234
+ *
2235
+ * @param shapes - The JSX shapes of the index.
2236
+ * @param wanted - The runtime key, without its prefix.
2237
+ * @returns The anchors, in that order.
2238
+ */
2239
+ function patternAnchors(shapes, wanted) {
2240
+ const { holePatterns, wildPatterns, idProps } = shapes;
2241
+ const reached = [];
2242
+ for (const pattern of holePatterns.toSorted(bySpecificity)) for (const filler of idProps.filter((item) => fills(pattern, item, wanted))) reached.push(pattern, filler);
2243
+ const wild = wildPatterns.filter((pattern) => matchesPattern(pattern.key ?? "", wanted));
2244
+ reached.push(...wild.toSorted(bySpecificity));
2245
+ return reached;
2246
+ }
2247
+ /**
2248
+ * The anchors of a key, in the order `find` answers them: the exact key, then, for a JSX key as
2249
+ * the game reports it (no `*`, no prop hole), the patterns that read as it.
2250
+ *
2251
+ * @param session - The open project.
2252
+ * @param key - The key.
2253
+ * @returns The anchors, each once.
2254
+ */
2255
+ function anchorsOf(session, key) {
2256
+ const exact = session.index.symbols[key]?.def ?? [];
2257
+ const reached = key.startsWith("jsx:") && !key.includes("*") && !hasHole(key) ? patternAnchors(shapesOf(session), key.slice(4)) : [];
2258
+ return [.../* @__PURE__ */ new Set([...exact, ...reached])];
2259
+ }
2260
+ /**
2261
+ * The JSX hits of one parse, collected on the first call and kept for the next ones.
2262
+ *
2263
+ * @param typescript - The TypeScript module.
2264
+ * @param source - The parse.
2265
+ * @returns A function that answers the hits.
2266
+ */
2267
+ function jsxHitsOnce(typescript, source) {
2268
+ let hits;
2269
+ return () => {
2270
+ hits ??= collectJsx(typescript, source);
2271
+ return hits;
2272
+ };
2273
+ }
2274
+ /**
2275
+ * The parse a file answers from now: the bytes on disk when they parse, else the last good parse
2276
+ * of the file, marked broken.
2277
+ *
2278
+ * @param session - The open project.
2279
+ * @param file - The root-relative path.
2280
+ * @returns The view, or `undefined` when the file is gone or never parsed.
2281
+ */
2282
+ async function viewOf(session, file) {
2283
+ const { catalog } = session;
2284
+ const good = catalog.records.get(file)?.good;
2285
+ const bytes = await readInside(session.root, file);
2286
+ if (bytes === void 0) return void 0;
2287
+ const hash = sha1(bytes);
2288
+ const fresh = parseCached(catalog, file, hash, hash === good?.hash ? good.text : new TextDecoder().decode(bytes));
2289
+ if (fresh.error === void 0) return {
2290
+ source: fresh.source,
2291
+ hash,
2292
+ broken: false,
2293
+ jsx: jsxHitsOnce(catalog.typescript, fresh.source)
2294
+ };
2295
+ if (good === void 0) return void 0;
2296
+ const { source } = parseCached(catalog, file, good.hash, good.text);
2297
+ return {
2298
+ source,
2299
+ hash: good.hash,
2300
+ broken: true,
2301
+ jsx: jsxHitsOnce(catalog.typescript, source)
2302
+ };
2303
+ }
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
+ /**
2332
+ * The identity of an answer: two anchors that land on the same element with the same key answer
2333
+ * once.
2334
+ *
2335
+ * @param answer - One answer of `find`.
2336
+ * @returns The path, the range, the kind and the key, joined.
2337
+ * @example
2338
+ * ```ts
2339
+ * identityOf({ path: "a.tsx", key: "row", kind: "literal", line: 3, range: [3, 1, 3, 9], hash: "h" }); // "a.tsx|3,1,3,9|literal|row"
2340
+ * ```
2341
+ */
2342
+ function identityOf(answer) {
2343
+ return `${answer.path}|${answer.range.join(",")}|${answer.kind ?? ""}|${answer.key ?? ""}`;
2344
+ }
2345
+ /**
2346
+ * Answers where a key is defined, with lines read from the files on disk now.
2347
+ *
2348
+ * @param session - The open project.
2349
+ * @param key - The key.
2350
+ * @returns One entry per place; `[]` for an unknown key.
2351
+ */
2352
+ async function findKey(session, key) {
2353
+ const views = /* @__PURE__ */ new Map();
2354
+ const seen = /* @__PURE__ */ new Set();
2355
+ const found = [];
2356
+ const keyKind = kindOf(key);
2357
+ for (const anchor of anchorsOf(session, key)) {
2358
+ if (!views.has(anchor.path)) views.set(anchor.path, await viewOf(session, anchor.path));
2359
+ const view = views.get(anchor.path);
2360
+ if (view === void 0) continue;
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) => ({
2366
+ ...anchor,
2367
+ ...place,
2368
+ hash: view.hash,
2369
+ ...view.broken ? { broken: true } : {}
2370
+ }));
2371
+ for (const answer of answers) {
2372
+ const identity = identityOf(answer);
2373
+ if (seen.has(identity)) continue;
2374
+ seen.add(identity);
2375
+ found.push(answer);
2376
+ }
2377
+ }
2378
+ return found;
2379
+ }
2380
+ //#endregion
2381
+ //#region src/project/change.ts
2382
+ /**
2383
+ * The files that define a key, each once, in the order of its anchors.
2384
+ *
2385
+ * @param entry - The symbol entry.
2386
+ * @param entry.def - Its definitions.
2387
+ * @returns The paths.
2388
+ */
2389
+ function pathsOf(entry) {
2390
+ return [...new Set(entry.def.map((anchor) => anchor.path))];
2391
+ }
2392
+ /**
2393
+ * Compares the index before a batch with the index after it.
2394
+ *
2395
+ * @param before - The index before the batch.
2396
+ * @param after - The index after the batch.
2397
+ * @param files - The paths whose bytes changed, appeared or vanished.
2398
+ * @returns The change: the new revision, the files sorted, the moves and the removed keys.
2399
+ */
2400
+ function diffIndexes(before, after, files) {
2401
+ const moved = [];
2402
+ const removed = [];
2403
+ for (const [key, entry] of Object.entries(before.symbols)) {
2404
+ const now = after.symbols[key];
2405
+ if (now === void 0) {
2406
+ removed.push(key);
2407
+ continue;
2408
+ }
2409
+ const was = pathsOf(entry);
2410
+ const is = pathsOf(now);
2411
+ const left = was.filter((file) => !is.includes(file));
2412
+ const arrived = is.filter((file) => !was.includes(file));
2413
+ for (const [index, from] of left.entries()) {
2414
+ const to = arrived[index];
2415
+ if (to !== void 0) moved.push({
2416
+ key,
2417
+ from,
2418
+ to
2419
+ });
2420
+ }
2421
+ }
2422
+ return {
2423
+ revision: after.revision,
2424
+ files: files.toSorted(),
2425
+ moved,
2426
+ removed
2427
+ };
2428
+ }
2429
+ //#endregion
2430
+ //#region src/project/walk.ts
2431
+ /**
2432
+ * Reads the stamp of a file.
2433
+ *
2434
+ * @param file - The absolute path.
2435
+ * @returns The stamp, or `undefined` when the file is gone.
2436
+ */
2437
+ async function stampOf(file) {
2438
+ try {
2439
+ const stats = await stat(file, { bigint: true });
2440
+ return {
2441
+ ino: stats.ino,
2442
+ size: stats.size,
2443
+ mtimeNs: stats.mtimeNs,
2444
+ ctimeNs: stats.ctimeNs
2445
+ };
2446
+ } catch {
2447
+ return;
2448
+ }
2449
+ }
2450
+ /**
2451
+ * Whether two stamps describe the same write.
2452
+ *
2453
+ * @param first - One stamp.
2454
+ * @param second - The other.
2455
+ * @returns True when inode, size and both time stamps are equal.
2456
+ * @example
2457
+ * ```ts
2458
+ * const stamp = { ino: 1n, size: 10n, mtimeNs: 5n, ctimeNs: 5n };
2459
+ * sameStamp(stamp, { ...stamp, size: 11n }); // false
2460
+ * ```
2461
+ */
2462
+ function sameStamp(first, second) {
2463
+ return first.ino === second.ino && first.size === second.size && first.mtimeNs === second.mtimeNs && first.ctimeNs === second.ctimeNs;
2464
+ }
2465
+ /**
2466
+ * Lists one folder, empty when it vanished during the walk.
2467
+ *
2468
+ * @param folder - The absolute folder.
2469
+ * @returns Its entries.
2470
+ */
2471
+ async function entriesOf(folder) {
2472
+ return readdir(folder, { withFileTypes: true }).catch(() => []);
2473
+ }
2474
+ /**
2475
+ * Reads one folder of the walk: its sub-folders join the queue, its source files join the list
2476
+ * with their stamps.
2477
+ *
2478
+ * @param root - The real root.
2479
+ * @param folder - The folder, root-relative; `""` is the root.
2480
+ * @param walk - The files so far, and the queue of folders still to read.
2481
+ * @param walk.files - The files so far.
2482
+ * @param walk.pending - The folders still to read.
2483
+ */
2484
+ async function readFolder(root, folder, walk) {
2485
+ for (const entry of await entriesOf(path.join(root, folder))) {
2486
+ const relative = folder === "" ? entry.name : `${folder}/${entry.name}`;
2487
+ if (entry.isDirectory() && !isSkippedFolder(entry.name)) walk.pending.push(relative);
2488
+ if (!entry.isFile() || !isIndexedFile(relative)) continue;
2489
+ const stamp = await stampOf(path.join(root, relative));
2490
+ if (stamp !== void 0) walk.files.set(relative, stamp);
2491
+ }
2492
+ }
2493
+ /**
2494
+ * Walks a root: every indexed source file with its stamp and every folder outside the skipped
2495
+ * ones. Symlinks are skipped, files and folders alike.
2496
+ *
2497
+ * @param root - The real root.
2498
+ * @returns The files and the folders, root-relative POSIX.
2499
+ */
2500
+ async function walkRoot(root) {
2501
+ const files = /* @__PURE__ */ new Map();
2502
+ const folders = [];
2503
+ const pending = [""];
2504
+ for (let folder = pending.pop(); folder !== void 0; folder = pending.pop()) {
2505
+ folders.push(folder);
2506
+ await readFolder(root, folder, {
2507
+ files,
2508
+ pending
2509
+ });
2510
+ }
2511
+ return {
2512
+ files,
2513
+ folders
2514
+ };
2515
+ }
2516
+ //#endregion
2517
+ //#region src/project/session.ts
2518
+ /**
2519
+ * @file project — an open project: the real root, the catalog of its files, the stamps of the last
2520
+ * walk, the current index and a queue that runs one update at a time. `syncWithDisk` is the batch
2521
+ * walk of a watch batch: it walks the root, hashes the files whose stamp moved and every new one,
2522
+ * drops the vanished ones and rebuilds the index when bytes changed. `reindexPath` reads one path
2523
+ * now for `changed`.
2524
+ */
2525
+ /** The manifest the asset scanner writes by default. */
2526
+ const MANIFEST_FILE = "manifest.json";
2527
+ /**
2528
+ * The manifest path to report, when the file exists.
2529
+ *
2530
+ * @param root - The real root.
2531
+ * @param manifest - The root-relative manifest path.
2532
+ * @returns The path, or `undefined` when nothing is there.
2533
+ */
2534
+ async function presentManifest(root, manifest) {
2535
+ try {
2536
+ if (await resolveInside(root, manifest) !== void 0) return manifest;
2537
+ } catch {}
2538
+ }
2539
+ /**
2540
+ * Reads one file into the catalog, or drops it when it is gone or cannot be read.
2541
+ *
2542
+ * @param session - The project.
2543
+ * @param file - The root-relative path.
2544
+ * @returns True when the catalog changed.
2545
+ */
2546
+ async function readIntoCatalog(session, file) {
2547
+ const bytes = await readInside(session.root, file);
2548
+ if (bytes === void 0) {
2549
+ session.stamps.delete(file);
2550
+ return dropFile(session.catalog, file);
2551
+ }
2552
+ return putFile(session.catalog, file, bytes);
2553
+ }
2554
+ /**
2555
+ * Rebuilds the index after files changed and says what changed.
2556
+ *
2557
+ * @param session - The project.
2558
+ * @param files - The paths whose bytes changed, appeared or vanished.
2559
+ * @returns The change.
2560
+ */
2561
+ async function rebuild(session, files) {
2562
+ const before = session.index;
2563
+ const manifest = await presentManifest(session.root, session.manifest);
2564
+ session.index = buildIndex(session.catalog, manifest);
2565
+ return diffIndexes(before, session.index, files);
2566
+ }
2567
+ /**
2568
+ * Opens a project: checks the root, loads TypeScript, reads every source file and builds the
2569
+ * index.
2570
+ *
2571
+ * @param options - The root, the manifest and the debounce.
2572
+ * @returns The open project.
2573
+ * @throws {Error} When the root is missing or not a directory, the manifest path leaves the root,
2574
+ * or TypeScript is not installed.
2575
+ */
2576
+ async function openSession(options) {
2577
+ const root = await openRoot(options.root);
2578
+ const typescript = await loadTypeScript();
2579
+ const manifest = toPosix(options.manifest ?? MANIFEST_FILE);
2580
+ await resolveInside(root, manifest);
2581
+ const walk = await walkRoot(root);
2582
+ const catalog = createCatalog(typescript);
2583
+ const session = {
2584
+ root,
2585
+ manifest,
2586
+ catalog,
2587
+ stamps: walk.files,
2588
+ folders: walk.folders,
2589
+ index: buildIndex(catalog),
2590
+ shapes: /* @__PURE__ */ new WeakMap(),
2591
+ queue: Promise.resolve()
2592
+ };
2593
+ for (const file of [...walk.files.keys()].toSorted()) await readIntoCatalog(session, file);
2594
+ session.index = buildIndex(catalog, await presentManifest(root, manifest));
2595
+ return session;
2596
+ }
2597
+ /**
2598
+ * Runs an update after the ones before it, so two never interleave.
2599
+ *
2600
+ * @param session - The project.
2601
+ * @param task - The update.
2602
+ * @returns What the update returns.
2603
+ */
2604
+ function serialize(session, task) {
2605
+ const result = session.queue.then(task);
2606
+ session.queue = result.catch(() => {});
2607
+ return result;
2608
+ }
2609
+ /**
2610
+ * The batch walk: walks the root, hashes each file whose stamp moved and each new one, drops the
2611
+ * vanished ones, and rebuilds the index when bytes changed. A save with the same bytes changes
2612
+ * nothing.
2613
+ *
2614
+ * @param session - The project.
2615
+ * @returns The change, or `undefined` when the index stayed the same.
2616
+ */
2617
+ async function syncWithDisk(session) {
2618
+ const walk = await walkRoot(session.root);
2619
+ const files = [];
2620
+ for (const file of session.catalog.records.keys()) if (!walk.files.has(file) && dropFile(session.catalog, file)) files.push(file);
2621
+ const previous = session.stamps;
2622
+ session.stamps = walk.files;
2623
+ session.folders = walk.folders;
2624
+ for (const [file, stamp] of walk.files) {
2625
+ const known = previous.get(file);
2626
+ if (!(known !== void 0 && sameStamp(known, stamp)) && await readIntoCatalog(session, file)) files.push(file);
2627
+ }
2628
+ const manifest = await presentManifest(session.root, session.manifest);
2629
+ if (files.length === 0 && manifest === session.index.manifest) return void 0;
2630
+ return rebuild(session, files);
2631
+ }
2632
+ /**
2633
+ * Reads one root-relative path now: a changed or new source file is parsed, a deleted one is
2634
+ * dropped, and anything the index does not read changes nothing.
2635
+ *
2636
+ * @param session - The project.
2637
+ * @param file - The root-relative path.
2638
+ * @returns The index after the read.
2639
+ * @throws {Error} When the path leaves the root.
2640
+ */
2641
+ async function reindexPath(session, file) {
2642
+ const relative = toPosix(file);
2643
+ const real = await resolveInside(session.root, relative);
2644
+ const isLinked = real !== void 0 && real !== path.join(session.root, relative);
2645
+ if (path.isAbsolute(relative) || isLinked || !isIndexedFile(relative)) return session.index;
2646
+ const stamp = real === void 0 ? void 0 : await stampOf(real);
2647
+ if (stamp !== void 0) session.stamps.set(relative, stamp);
2648
+ if (!await readIntoCatalog(session, relative)) return session.index;
2649
+ await rebuild(session, [relative]);
2650
+ return session.index;
2651
+ }
2652
+ //#endregion
2653
+ //#region src/project/watch.ts
2654
+ /**
2655
+ * @file project — the watcher of an open project. A platform event is only a trigger: Bun on macOS
2656
+ * reports every edit as `rename`, drops the destination of a move and names the `.tmp` of an
2657
+ * atomic save instead of the file. So events only poke a batcher; when the quiet period ends, the
2658
+ * batch walks the root (`syncWithDisk`) and finds what really changed. The watcher can also miss a
2659
+ * write made as it starts, so the handle walks once when watching starts and then every two
2660
+ * seconds as a backstop. The root is watched recursively; where the platform cannot, every folder
2661
+ * of the last walk gets its own watcher, re-armed after each walk. Every caller of `watch` shares
2662
+ * the one watcher of the handle.
2663
+ */
2664
+ /** The longest a burst of events holds a batch back, at the least. */
2665
+ const LONGEST_WAIT_MS = 1e3;
2666
+ /** The longest wait as a multiple of the quiet period, when that is longer. */
2667
+ const LONGEST_WAIT_FACTOR = 10;
2668
+ /**
2669
+ * How often a watching handle walks the root with no event: the platform watcher can miss a write
2670
+ * made as it starts, and drop events under load. A walk that finds the same bytes calls nobody.
2671
+ */
2672
+ const BACKSTOP_MS = 2e3;
2673
+ /** The error `watch` throws on a closed handle. */
2674
+ const CLOSED = "[game] The project is closed.\n Open it again with openProject().";
2675
+ /**
2676
+ * Watches every folder of the last walk with its own watcher, for a platform without recursive
2677
+ * watching.
2678
+ *
2679
+ * @param root - The real root.
2680
+ * @param listener - Gets every event.
2681
+ * @param onError - Called when a folder watcher errors.
2682
+ * @param watchFunction - `fs.watch`.
2683
+ * @returns The tree watcher.
2684
+ */
2685
+ function watchFolders(root, listener, onError, watchFunction) {
2686
+ const watchers = /* @__PURE__ */ new Map();
2687
+ const arm = (folder) => {
2688
+ try {
2689
+ const handle = watchFunction(path.join(root, folder), { recursive: false }, listener);
2690
+ handle.on("error", () => {
2691
+ handle.close();
2692
+ if (watchers.get(folder) === handle) watchers.delete(folder);
2693
+ onError();
2694
+ });
2695
+ watchers.set(folder, handle);
2696
+ } catch {}
2697
+ };
2698
+ return {
2699
+ rearm: (folders) => {
2700
+ for (const [folder, handle] of watchers) {
2701
+ if (folders.includes(folder)) continue;
2702
+ handle.close();
2703
+ watchers.delete(folder);
2704
+ }
2705
+ for (const folder of folders) if (!watchers.has(folder)) arm(folder);
2706
+ },
2707
+ close: () => {
2708
+ for (const handle of watchers.values()) handle.close();
2709
+ watchers.clear();
2710
+ }
2711
+ };
2712
+ }
2713
+ /**
2714
+ * Watches a root: recursively when the platform can, else one watcher per folder. Events under
2715
+ * the skipped folders are ignored.
2716
+ *
2717
+ * @param root - The real root.
2718
+ * @param onEvent - Called on every event that may matter.
2719
+ * @param watchFunction - `fs.watch` by default.
2720
+ * @returns The tree watcher.
2721
+ */
2722
+ function watchTree(root, onEvent, watchFunction = watch) {
2723
+ const listener = (_event, file) => {
2724
+ if (file === null || !isSkippedPath(`${file}/`)) onEvent();
2725
+ };
2726
+ try {
2727
+ const handle = watchFunction(root, { recursive: true }, listener);
2728
+ handle.on("error", () => onEvent());
2729
+ return {
2730
+ rearm: () => {},
2731
+ close: () => handle.close()
2732
+ };
2733
+ } catch {
2734
+ return watchFolders(root, listener, onEvent, watchFunction);
2735
+ }
2736
+ }
2737
+ /**
2738
+ * Creates the debounce of a watcher: a batch runs once the events stop for `quietMs`, or after the
2739
+ * longest wait during an endless burst. One batch runs at a time; events during a batch queue one
2740
+ * more.
2741
+ *
2742
+ * @param quietMs - The quiet period that closes a batch.
2743
+ * @param run - The batch.
2744
+ * @returns The batcher.
2745
+ */
2746
+ function createBatcher(quietMs, run) {
2747
+ const longestMs = Math.max(LONGEST_WAIT_MS, quietMs * LONGEST_WAIT_FACTOR);
2748
+ let quiet;
2749
+ let longest;
2750
+ let running = false;
2751
+ let pending = false;
2752
+ let cancelled = false;
2753
+ const clear = () => {
2754
+ clearTimeout(quiet);
2755
+ clearTimeout(longest);
2756
+ quiet = void 0;
2757
+ longest = void 0;
2758
+ };
2759
+ const fire = () => {
2760
+ clear();
2761
+ if (running) {
2762
+ pending = true;
2763
+ return;
2764
+ }
2765
+ const settle = () => {
2766
+ running = false;
2767
+ if (pending && !cancelled) poke();
2768
+ pending = false;
2769
+ };
2770
+ running = true;
2771
+ run().then(settle, settle);
2772
+ };
2773
+ const poke = () => {
2774
+ if (cancelled) return;
2775
+ clearTimeout(quiet);
2776
+ quiet = setTimeout(fire, quietMs);
2777
+ longest ??= setTimeout(fire, longestMs);
2778
+ };
2779
+ return {
2780
+ poke,
2781
+ cancel: () => {
2782
+ cancelled = true;
2783
+ clear();
2784
+ }
2785
+ };
2786
+ }
2787
+ /**
2788
+ * Creates the callers' hub of one handle: the watcher starts with the first caller and stops with
2789
+ * the last; each batch that changed the index calls every caller once.
2790
+ *
2791
+ * @param session - The open project.
2792
+ * @param debounceMs - The quiet period.
2793
+ * @param watchFunction - `fs.watch` by default.
2794
+ * @param backstopMs - The period of the walk with no event, two seconds by default.
2795
+ * @returns The hub.
2796
+ */
2797
+ function createHub(session, debounceMs, watchFunction = watch, backstopMs = BACKSTOP_MS) {
2798
+ const listeners = /* @__PURE__ */ new Set();
2799
+ let tree;
2800
+ let batcher;
2801
+ let backstop;
2802
+ let closed = false;
2803
+ const batch = async () => {
2804
+ const change = await serialize(session, () => syncWithDisk(session));
2805
+ tree?.rearm(session.folders);
2806
+ if (change === void 0 || closed) return;
2807
+ for (const { listener } of [...listeners]) try {
2808
+ listener(session.index, change);
2809
+ } catch {}
2810
+ };
2811
+ const stop = () => {
2812
+ clearInterval(backstop);
2813
+ tree?.close();
2814
+ batcher?.cancel();
2815
+ backstop = void 0;
2816
+ tree = void 0;
2817
+ batcher = void 0;
2818
+ };
2819
+ const start = () => {
2820
+ const started = createBatcher(debounceMs, batch);
2821
+ batcher = started;
2822
+ tree = watchTree(session.root, () => started.poke(), watchFunction);
2823
+ tree.rearm(session.folders);
2824
+ started.poke();
2825
+ backstop = setInterval(() => started.poke(), backstopMs);
2826
+ };
2827
+ return {
2828
+ add: (listener) => {
2829
+ if (closed) throw new Error(CLOSED);
2830
+ const entry = { listener };
2831
+ listeners.add(entry);
2832
+ if (tree === void 0) start();
2833
+ return () => {
2834
+ listeners.delete(entry);
2835
+ if (listeners.size === 0) stop();
2836
+ };
2837
+ },
2838
+ close: () => {
2839
+ closed = true;
2840
+ listeners.clear();
2841
+ stop();
2842
+ }
2843
+ };
2844
+ }
2845
+ //#endregion
2846
+ //#region src/project/open.ts
2847
+ /**
2848
+ * @file project — `openProject`, the one function of the project index a caller starts from. It
2849
+ * opens the root, builds the index and hands back the handle: the current index, `find`, `watch`,
2850
+ * `changed` and `close`.
2851
+ */
2852
+ /** The quiet period that closes a watch batch, when the options name none. */
2853
+ const DEFAULT_DEBOUNCE_MS = 75;
2854
+ /**
2855
+ * Opens the project index of a game: checks the root (it exists, it is a directory, its real
2856
+ * path), loads TypeScript, parses every source file and builds the index. Nothing is written to
2857
+ * disk and nothing is inferred from the working directory but the root itself.
2858
+ *
2859
+ * @param options - The game root, the manifest path and the quiet period of `watch`.
2860
+ * @returns The handle; close it when the game closes.
2861
+ * @throws {Error} When the root is missing or not a directory, or the `typescript` package is not
2862
+ * installed: `[game] The project index needs the "typescript" package.`
2863
+ * @example
2864
+ * ```ts
2865
+ * // The editor opens the fixture game and answers where a node and a text style live.
2866
+ * const project = await openProject({ root: "tests/integration/merge-game" });
2867
+ * Object.keys(project.index.files).length; // 111
2868
+ * (await project.find("textStyle:ui.title"))[0]?.line; // 39, in "features/ui/styles.ts"
2869
+ * project.close();
2870
+ * ```
2871
+ */
2872
+ async function openProject(options) {
2873
+ const session = await openSession(options);
2874
+ const hub = createHub(session, options.debounceMs ?? DEFAULT_DEBOUNCE_MS);
2875
+ return {
2876
+ /**
2877
+ * The current index of the session; a batch or `changed` replaces it.
2878
+ *
2879
+ * @returns The index.
2880
+ */
2881
+ get index() {
2882
+ return session.index;
2883
+ },
2884
+ find: (key) => findKey(session, key),
2885
+ watch: (onIndex) => hub.add(onIndex),
2886
+ changed: (file) => serialize(session, () => reindexPath(session, file)),
2887
+ close: () => hub.close()
2888
+ };
2889
+ }
2890
+ //#endregion
2891
+ //#region src/project/cli.ts
2892
+ /**
2893
+ * @file project — the command line `moku-game-index`. It opens the project of `--root` and runs one
2894
+ * command: `--json` prints the index, `where <key>` prints one `path:line` per place, `--check`
2895
+ * reports broken files and conflicts (exit 1) and lists the unresolved items as info. Output goes
2896
+ * through the branded console of `@moku-labs/common`; `runCli` returns the exit code instead of
2897
+ * taking it.
2898
+ */
2899
+ /** The usage line under every flag error. */
2900
+ const USAGE = "Run: moku-game-index --root <dir> [--manifest <path>] --json | --check | where <key>.";
2901
+ /** Pretty JSON indent of `--json`. */
2902
+ const JSON_INDENT = 2;
2903
+ /**
2904
+ * Wraps a command line problem in the message shape of the framework.
2905
+ *
2906
+ * @param message - One sentence naming the flag.
2907
+ * @returns The error to throw.
2908
+ */
2909
+ function problem(message) {
2910
+ return /* @__PURE__ */ new Error(`[game] index: ${message}\n ${USAGE}`);
2911
+ }
2912
+ /**
2913
+ * Reads the message of anything that was thrown.
2914
+ *
2915
+ * @param failure - What the `catch` caught.
2916
+ * @returns The message.
2917
+ */
2918
+ function messageOf(failure) {
2919
+ return failure instanceof Error ? failure.message : String(failure);
2920
+ }
2921
+ /**
2922
+ * The value after a flag that takes one.
2923
+ *
2924
+ * @param argv - The arguments.
2925
+ * @param at - The index of the flag.
2926
+ * @param what - What the flag needs, for the error.
2927
+ * @returns The value.
2928
+ * @throws {Error} When the value is missing.
2929
+ */
2930
+ function valueAfter(argv, at, what) {
2931
+ const flag = argv[at] ?? "";
2932
+ const value = argv[at + 1];
2933
+ if (value === void 0 || value.startsWith("--")) throw problem(`"${flag}" needs ${what}.`);
2934
+ return value;
2935
+ }
2936
+ /**
2937
+ * Reads one flag or command into the flags.
2938
+ *
2939
+ * @param argv - The arguments.
2940
+ * @param at - The index of the flag.
2941
+ * @param flags - The flags so far.
2942
+ * @returns How many arguments the flag took.
2943
+ * @throws {Error} When the flag is unknown or misses its value.
2944
+ */
2945
+ function readFlag(argv, at, flags) {
2946
+ const flag = argv[at];
2947
+ switch (flag) {
2948
+ case "--json":
2949
+ flags.commands.push({ kind: "json" });
2950
+ return 1;
2951
+ case "--check":
2952
+ flags.commands.push({ kind: "check" });
2953
+ return 1;
2954
+ case "where":
2955
+ flags.commands.push({
2956
+ kind: "where",
2957
+ key: valueAfter(argv, at, "a key, such as node:board/merge")
2958
+ });
2959
+ return 2;
2960
+ case "--root":
2961
+ flags.root = valueAfter(argv, at, "a path");
2962
+ return 2;
2963
+ case "--manifest":
2964
+ flags.manifest = valueAfter(argv, at, "a path");
2965
+ return 2;
2966
+ default: throw problem(`unknown option "${String(flag)}".`);
2967
+ }
2968
+ }
2969
+ /**
2970
+ * Reads the arguments.
2971
+ *
2972
+ * @param argv - The arguments after the script name.
2973
+ * @returns The root, the manifest and the one command.
2974
+ * @throws {Error} When a flag is unknown, a value is missing, `--root` is missing or the commands
2975
+ * are not exactly one.
2976
+ */
2977
+ function parseArgv(argv) {
2978
+ const flags = { commands: [] };
2979
+ for (let at = 0; at < argv.length;) at += readFlag(argv, at, flags);
2980
+ if (flags.root === void 0) throw new Error("[game] index: \"--root\" is required.\n Name the game folder: moku-game-index --root <dir> where <key>.");
2981
+ const [command] = flags.commands;
2982
+ if (command === void 0 || flags.commands.length > 1) throw problem("name one command: --json, --check or where <key>.");
2983
+ return {
2984
+ root: flags.root,
2985
+ command,
2986
+ ...flags.manifest === void 0 ? {} : { manifest: flags.manifest }
2987
+ };
2988
+ }
2989
+ /**
2990
+ * One line of a `where` answer.
2991
+ *
2992
+ * @param found - One place of the key.
2993
+ * @returns `path:line`, with ` (broken)` when the line comes from the last good parse.
2994
+ * @example
2995
+ * ```ts
2996
+ * whereLine({ path: "nodes/merge.ts", line: 17, range: [17, 1, 38, 4], hash: "ab" }); // "nodes/merge.ts:17"
2997
+ * ```
2998
+ */
2999
+ function whereLine(found) {
3000
+ return `${found.path}:${found.line}${found.broken === true ? " (broken)" : ""}`;
3001
+ }
3002
+ /**
3003
+ * Counts a thing in words, so the summary reads as a sentence.
3004
+ *
3005
+ * @param count - How many.
3006
+ * @param word - The singular.
3007
+ * @returns The count and the word.
3008
+ * @example
3009
+ * ```ts
3010
+ * plural(1, "key"); // "1 key"
3011
+ * ```
3012
+ */
3013
+ function plural(count, word) {
3014
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
3015
+ }
3016
+ /**
3017
+ * The `--check` report: a summary, every broken file and conflict as an error, every unresolved
3018
+ * item as info.
3019
+ *
3020
+ * @param index - The index.
3021
+ * @param ui - Where the lines go.
3022
+ * @returns `1` when a file is broken or a key is in conflict, else `0`.
3023
+ */
3024
+ function printCheck(index, ui) {
3025
+ const files = Object.entries(index.files);
3026
+ const symbols = Object.entries(index.symbols);
3027
+ const broken = files.filter(([, file]) => file.state === "broken");
3028
+ const conflicts = symbols.filter(([, symbol]) => symbol.conflict === true);
3029
+ ui.info(`${plural(files.length, "file")}, ${plural(symbols.length, "key")}: ${broken.length} broken, ${conflicts.length} in conflict, ${index.unresolved.length} unresolved.`);
3030
+ for (const [file, entry] of broken) ui.error(`broken ${file}: ${entry.error ?? ""}`);
3031
+ for (const [key, symbol] of conflicts) ui.error(`conflict ${key}: ${symbol.def.map((anchor) => anchor.path).join(", ")}`);
3032
+ for (const item of index.unresolved) ui.info(`unresolved ${item.path}: ${item.reason}`);
3033
+ return broken.length + conflicts.length > 0 ? 1 : 0;
3034
+ }
3035
+ /**
3036
+ * The `where` answer: one line per place of the key.
3037
+ *
3038
+ * @param project - The open project.
3039
+ * @param key - The key.
3040
+ * @param root - The root as given, for the message.
3041
+ * @param ui - Where the lines go.
3042
+ * @returns `1` when the key is unknown, else `0`.
3043
+ */
3044
+ async function printWhere(project, key, root, ui) {
3045
+ const found = await project.find(key);
3046
+ if (found.length === 0) {
3047
+ ui.error(`[game] The key "${key}" is not in the index of "${root}".\n Run "moku-game-index --root ${root} --json" to list the keys.`);
3048
+ return 1;
3049
+ }
3050
+ for (const place of found) ui.line(whereLine(place));
3051
+ return 0;
3052
+ }
3053
+ /**
3054
+ * Runs the one command of the line on an open project.
3055
+ *
3056
+ * @param project - The open project.
3057
+ * @param options - What the flags asked for.
3058
+ * @param ui - Where the lines go.
3059
+ * @returns The exit code.
3060
+ */
3061
+ async function runCommand(project, options, ui) {
3062
+ const { command } = options;
3063
+ if (command.kind === "where") return printWhere(project, command.key, options.root, ui);
3064
+ if (command.kind === "check") return printCheck(project.index, ui);
3065
+ ui.line(JSON.stringify(project.index, void 0, JSON_INDENT));
3066
+ return 0;
3067
+ }
3068
+ /**
3069
+ * Runs `moku-game-index`: opens the project of `--root` and runs one command. Nothing here calls
3070
+ * `process.exit`; the caller does.
3071
+ *
3072
+ * @param argv - The arguments after the script name.
3073
+ * @param ui - Where the lines go. The branded console by default.
3074
+ * @returns The exit code: `1` for a bad flag, a root that cannot be opened, an unknown key, or a
3075
+ * `--check` that found a broken file or a conflict; else `0`.
3076
+ * @example
3077
+ * ```ts
3078
+ * // An agent asks where a node of the fixture game lives.
3079
+ * await runCli(["--root", "tests/integration/merge-game", "where", "node:board/merge"]);
3080
+ * // 0, and the console printed "nodes/merge.ts:17"
3081
+ * ```
3082
+ */
3083
+ async function runCli(argv, ui = createBrandConsole()) {
3084
+ try {
3085
+ const options = parseArgv(argv);
3086
+ const project = await openProject({
3087
+ root: options.root,
3088
+ ...options.manifest === void 0 ? {} : { manifest: options.manifest }
3089
+ });
3090
+ try {
3091
+ return await runCommand(project, options, ui);
3092
+ } finally {
3093
+ project.close();
3094
+ }
3095
+ } catch (error) {
3096
+ ui.error(messageOf(error));
3097
+ return 1;
3098
+ }
3099
+ }
3100
+ //#endregion
3101
+ //#region src/project.ts
3102
+ /**
3103
+ * @file The project door of the engine (subpath `./project`, node and bun only): the static index
3104
+ * of a game's sources. `openProject` parses the game with TypeScript (an optional peer, loaded on
3105
+ * the first open) and maps every engine id (flows, nodes, features, scenes, projections, emitters,
3106
+ * text styles, styles and JSX keys) to anchors; `find` reads the line of a key from the file on
3107
+ * disk at the call, `watch` keeps the index fresh while agents edit files. The package bin
3108
+ * `moku-game-index` runs `runCli`. Re-exports only: the code lives in `project/`, and this is the
3109
+ * one door to it in `src/`.
3110
+ */
3111
+ if (import.meta.main) process.exitCode = await runCli(process.argv.slice(2));
3112
+ //#endregion
3113
+ export { openProject, runCli };