@moku-labs/game 0.13.0 → 0.13.2

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.
Files changed (39) hide show
  1. package/README.md +3 -3
  2. package/dist/app/page.d.mts +2 -1
  3. package/dist/app/page.mjs +1 -0
  4. package/dist/app/system.d.mts +3 -3
  5. package/dist/app.d.mts +2 -2
  6. package/dist/app.mjs +1 -1
  7. package/dist/assets.d.mts +1 -1
  8. package/dist/assets.mjs +1 -1
  9. package/dist/cli.d.mts +69 -3
  10. package/dist/cli.mjs +482 -57
  11. package/dist/{commands-DvFr8Uwp.d.mts → commands-BrgeMP5y.d.mts} +5 -5
  12. package/dist/control.d.mts +4 -4
  13. package/dist/{define-CCc0ozKe.d.mts → define-ZmWbWxon.d.mts} +2 -2
  14. package/dist/{headless-G4nuZ-xz.d.mts → headless-Dgxvp6Ym.d.mts} +1 -1
  15. package/dist/hot.d.mts +3 -3
  16. package/dist/hot.mjs +11 -9
  17. package/dist/{index-rnOYAriI.d.mts → index-CdbE_jGs.d.mts} +12 -4
  18. package/dist/index.d.mts +5 -5
  19. package/dist/index.mjs +1 -1
  20. package/dist/inspect.d.mts +7 -7
  21. package/dist/jsx-dev-runtime.d.mts +1 -1
  22. package/dist/jsx-runtime.d.mts +1 -1
  23. package/dist/{memory-DwDhfFWK.d.mts → memory-DJ2sPExv.d.mts} +1 -1
  24. package/dist/project.mjs +7 -2954
  25. package/dist/{runtime-B276Gg0s.d.mts → runtime-6qakboTm.d.mts} +1 -1
  26. package/dist/{src-SmT-XxVf.mjs → src-CltFFKtc.mjs} +955 -108
  27. package/dist/testing.d.mts +4 -4
  28. package/dist/testing.mjs +1 -1
  29. package/dist/{types-D_2G6Q68.d.mts → types-B3T2X6To.d.mts} +8 -4
  30. package/dist/{types-ZQLYXo5X.d.mts → types-C6y8EfEB.d.mts} +54 -5
  31. package/dist/{types-BrTpz9oL.d.mts → types-CmuwAjkV.d.mts} +19 -6
  32. package/dist/{types-DAdwEJMp.d.mts → types-D57SntUt.d.mts} +2 -2
  33. package/dist/{types-DLghaGEy.d.mts → types-DGIkfZB7.d.mts} +4 -4
  34. package/dist/{types-Bcdo5-2w.d.mts → types-zRGJdFGX.d.mts} +7 -3
  35. package/dist/visual.d.mts +6 -6
  36. package/dist/watch-CWI_on4T.mjs +2960 -0
  37. package/llms.txt +11 -7
  38. package/package.json +4 -2
  39. package/dist/{assets-DYiKxPBX.mjs → assets-BrKSg1O6.mjs} +1 -1
package/dist/project.mjs CHANGED
@@ -1,2383 +1,5 @@
1
- import { watch } from "node:fs";
2
- import path from "node:path";
1
+ import { S as readInside, _ as declaredFunctions, a as reindexPath, b as unwrap, c as definerOfCall, d as sha1, f as definersOf, g as holeOf, h as hasHole, i as openSession, l as JSX, m as HOLE, n as createHub, o as serialize, p as collectJsx, s as parseCached, u as jsxShapes, v as propertyNameOf, y as propertyOfValue } from "./watch-CWI_on4T.mjs";
3
2
  import { createBrandConsole } from "@moku-labs/common/cli";
4
- import { createHash } from "node:crypto";
5
- import { readFile, readdir, realpath, stat } from "node:fs/promises";
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/paths.ts
878
- /**
879
- * @file project — which files the index reads, and the guard on every path it reads. Paths in and
880
- * out are root-relative POSIX. A read resolves the path against the real root with `realpath`, so
881
- * a `..` or a symlink that leaves the root is refused by name instead of read.
882
- */
883
- /** The folders a game never edits by hand: dependencies, builds, generated code and tests. */
884
- const SKIPPED_FOLDER = /^(?:node_modules|dist|generated|\.moku|\.git|__tests__|tests)$/;
885
- /** The source files the index reads. */
886
- const SOURCE_FILE = /\.tsx?$/;
887
- /** Test files, skipped wherever they sit. */
888
- const TEST_FILE = /\.(?:test|spec)\.tsx?$/;
889
- /** The hint under every root error. */
890
- const ROOT_HINT = "Pass the folder of the game as root.";
891
- /**
892
- * Whether a folder name is one the index never walks into.
893
- *
894
- * @param name - One folder name.
895
- * @returns True for `node_modules`, `dist`, `generated`, `.moku`, `.git`, `__tests__` and `tests`.
896
- * @example
897
- * ```ts
898
- * isSkippedFolder("generated"); // true
899
- * ```
900
- */
901
- function isSkippedFolder(name) {
902
- return SKIPPED_FOLDER.test(name);
903
- }
904
- /**
905
- * Whether a root-relative path sits under a skipped folder.
906
- *
907
- * @param file - A path with `/` or `\` separators.
908
- * @returns True when one of its folders is skipped.
909
- * @example
910
- * ```ts
911
- * isSkippedPath("node_modules/pixi.js/index.ts"); // true
912
- * ```
913
- */
914
- function isSkippedPath(file) {
915
- return file.split(/[/\\]/).slice(0, -1).some((folder) => isSkippedFolder(folder));
916
- }
917
- /**
918
- * Whether the index reads a root-relative path: a `.ts` or `.tsx` file outside the skipped
919
- * folders that is not a test.
920
- *
921
- * @param file - A root-relative POSIX path.
922
- * @returns True for a source file of the index.
923
- * @example
924
- * ```ts
925
- * isIndexedFile("features/ui/kit.tsx"); // true
926
- * ```
927
- */
928
- function isIndexedFile(file) {
929
- return SOURCE_FILE.test(file) && !TEST_FILE.test(file) && !isSkippedPath(file);
930
- }
931
- /**
932
- * Turns a path into its root-relative POSIX form.
933
- *
934
- * @param file - A path with any separators.
935
- * @returns The path with `/` separators, `.` segments resolved.
936
- * @example
937
- * ```ts
938
- * toPosix("./features\\ui\\kit.tsx"); // "features/ui/kit.tsx"
939
- * ```
940
- */
941
- function toPosix(file) {
942
- return path.posix.normalize(file.replaceAll("\\", "/"));
943
- }
944
- /**
945
- * Whether a file system error says nothing is at the path.
946
- *
947
- * @param error - What was thrown.
948
- * @returns True for `ENOENT` and `ENOTDIR`.
949
- */
950
- function isMissing(error) {
951
- const code = error instanceof Error && "code" in error ? error.code : void 0;
952
- return code === "ENOENT" || code === "ENOTDIR";
953
- }
954
- /**
955
- * Whether an absolute path lies inside a folder.
956
- *
957
- * @param folder - The absolute folder.
958
- * @param file - The absolute path.
959
- * @returns True for the folder itself and anything below it.
960
- */
961
- function isInside(folder, file) {
962
- const relative = path.relative(folder, file);
963
- return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
964
- }
965
- /**
966
- * The error of a path that leaves the root.
967
- *
968
- * @param file - The path as it was given.
969
- * @param root - The real root.
970
- * @returns The error to throw.
971
- */
972
- function leavesRoot(file, root) {
973
- return /* @__PURE__ */ new Error(`[game] The path "${file}" leaves the project root.\n Pass a path inside "${root}", relative to it.`);
974
- }
975
- /**
976
- * Checks the root of a project and resolves it: it exists, it is a directory, and its real path
977
- * is the one every read is held to.
978
- *
979
- * @param root - The root as given, absolute or relative to the working directory.
980
- * @returns The real absolute path of the root.
981
- * @throws {Error} When the root does not exist or is not a directory.
982
- */
983
- async function openRoot(root) {
984
- const absolute = path.resolve(root);
985
- if (!(await stat(absolute).catch((error) => {
986
- if (!isMissing(error)) throw error;
987
- throw new Error(`[game] The project root "${absolute}" does not exist.\n ${ROOT_HINT}`);
988
- })).isDirectory()) throw new Error(`[game] The project root "${absolute}" is not a directory.\n ${ROOT_HINT}`);
989
- return realpath(absolute);
990
- }
991
- /**
992
- * Resolves a root-relative path to the real file, refusing any path that leaves the root.
993
- *
994
- * @param root - The real root.
995
- * @param file - The root-relative path.
996
- * @returns The real absolute path, or `undefined` when nothing is there.
997
- * @throws {Error} When the path leaves the root, by `..`, as an absolute path or by a symlink.
998
- */
999
- async function resolveInside(root, file) {
1000
- const absolute = path.resolve(root, file);
1001
- if (!isInside(root, absolute)) throw leavesRoot(file, root);
1002
- let real;
1003
- try {
1004
- real = await realpath(absolute);
1005
- } catch (error) {
1006
- if (isMissing(error)) return;
1007
- throw error;
1008
- }
1009
- if (!isInside(root, real)) throw leavesRoot(file, root);
1010
- return real;
1011
- }
1012
- /**
1013
- * Reads the bytes of a root-relative file through the guard. A path that leaves the root reads as
1014
- * nothing here; `changed` refuses it by name with `resolveInside` first.
1015
- *
1016
- * @param root - The real root.
1017
- * @param file - The root-relative path.
1018
- * @returns The bytes, or `undefined` when the file is gone or lies outside the root.
1019
- */
1020
- async function readInside(root, file) {
1021
- try {
1022
- const real = await resolveInside(root, file);
1023
- if (real !== void 0) return await readFile(real);
1024
- } catch {}
1025
- }
1026
- //#endregion
1027
- //#region src/project/aliases.ts
1028
- /**
1029
- * @file project — the tsconfig `paths` of a game, the one alias resolver of the index. A v15 game
1030
- * imports across its layers through aliases (`@core/kit`, `@features/home`, `@shared`). The
1031
- * tsconfig is read with TypeScript, so JSONC and the `extends` chain are TypeScript's own; a
1032
- * specifier is matched the way TypeScript matches it: an exact key first, then the `*` key with
1033
- * the longest prefix. The engine is a package by definition and is never matched. Paths in and
1034
- * out are root-relative POSIX.
1035
- */
1036
- /** A specifier relative to the importing file, or an absolute path: never an alias. */
1037
- const NOT_BARE = /^(?:\.{1,2}(?:\/|$)|\/)/;
1038
- /** The hint under a tsconfig that does not parse. */
1039
- const PARSE_HINT = "Fix the file, or name another one with the tsconfig option or --tsconfig.";
1040
- /** TypeScript numbers its syntax diagnostics from 1000 to 1999. */
1041
- const SYNTAX_CODES = {
1042
- from: 1e3,
1043
- below: 2e3
1044
- };
1045
- /**
1046
- * Whether a specifier names the engine, which is a package even when the paths map it.
1047
- *
1048
- * @param specifier - A module specifier.
1049
- * @returns True for `@moku-labs/game` and its subpaths.
1050
- * @example
1051
- * ```ts
1052
- * isEngine("@moku-labs/game/testing"); // true
1053
- * ```
1054
- */
1055
- function isEngine(specifier) {
1056
- return specifier === "@moku-labs/game" || specifier.startsWith(`@moku-labs/game/`);
1057
- }
1058
- /**
1059
- * How many `*` a key or a target holds.
1060
- *
1061
- * @param text - A key or a target of `paths`.
1062
- * @returns The count.
1063
- * @example
1064
- * ```ts
1065
- * starsOf("@features/*"); // 1
1066
- * ```
1067
- */
1068
- function starsOf(text) {
1069
- return text.split("*").length - 1;
1070
- }
1071
- /**
1072
- * Whether a diagnostic of a tsconfig says a file does not parse: a syntax error in the tsconfig
1073
- * or in a file it extends. TypeScript also reports an unknown option, a missing `extends` and
1074
- * the empty file list of a read that lists no files; none of them hides the `paths`.
1075
- *
1076
- * @param diagnostic - A diagnostic of the read.
1077
- * @returns True for a syntax error of a file.
1078
- */
1079
- function isSyntaxError(diagnostic) {
1080
- return diagnostic.file !== void 0 && diagnostic.code >= SYNTAX_CODES.from && diagnostic.code < SYNTAX_CODES.below;
1081
- }
1082
- /**
1083
- * The error of a tsconfig that does not parse, naming the file and the place as
1084
- * `path:line:col message`.
1085
- *
1086
- * @param typescript - The TypeScript module.
1087
- * @param root - The real root.
1088
- * @param file - The tsconfig, root-relative.
1089
- * @param diagnostic - The syntax error.
1090
- * @returns The error to throw.
1091
- */
1092
- function doesNotParse(typescript, root, file, diagnostic) {
1093
- const text = typescript.flattenDiagnosticMessageText(diagnostic.messageText, " ");
1094
- const message = text.endsWith(".") ? text : `${text}.`;
1095
- const source = diagnostic.file;
1096
- let place = "";
1097
- if (source !== void 0) {
1098
- const { line, character } = source.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
1099
- place = `${toPosix(path.relative(root, source.fileName))}:${line + 1}:${character + 1} `;
1100
- }
1101
- return /* @__PURE__ */ new Error(`[game] The tsconfig "${file}" does not parse: ${place}${message}\n ${PARSE_HINT}`);
1102
- }
1103
- /**
1104
- * The targets of one key of `paths`, root-relative: each string target with one `*` at most,
1105
- * resolved against the base folder. TypeScript does not check the shape of `paths`, so the value
1106
- * is read as the JSON it is.
1107
- *
1108
- * @param root - The real root.
1109
- * @param base - The absolute folder the targets are relative to.
1110
- * @param value - The value of the key.
1111
- * @returns The targets, `*` kept.
1112
- */
1113
- function targetsOf(root, base, value) {
1114
- if (!Array.isArray(value)) return [];
1115
- return value.filter((target) => typeof target === "string" && starsOf(target) <= 1).map((target) => toPosix(path.relative(root, path.resolve(base, target))));
1116
- }
1117
- /**
1118
- * The patterns of a `paths` block: one per key with one `*` at most and a target. A key or a
1119
- * target with two `*` never matches in TypeScript, so it is dropped here.
1120
- *
1121
- * @param root - The real root.
1122
- * @param base - The absolute folder the targets are relative to.
1123
- * @param paths - The `paths` of the merged options.
1124
- * @returns The patterns, in the order of the keys.
1125
- */
1126
- function patternsOf(root, base, paths) {
1127
- const patterns = [];
1128
- for (const [key, value] of Object.entries(paths)) {
1129
- const targets = targetsOf(root, base, value);
1130
- const star = key.indexOf("*");
1131
- if (targets.length === 0 || starsOf(key) > 1) continue;
1132
- patterns.push(star === -1 ? {
1133
- prefix: key,
1134
- targets
1135
- } : {
1136
- prefix: key.slice(0, star),
1137
- suffix: key.slice(star + 1),
1138
- targets
1139
- });
1140
- }
1141
- return patterns;
1142
- }
1143
- /**
1144
- * The files TypeScript read for a tsconfig that lie inside the root, outside `node_modules`,
1145
- * root-relative and each once. A file it tried and did not find is kept: its arrival changes the
1146
- * aliases.
1147
- *
1148
- * @param root - The real root.
1149
- * @param read - The absolute paths TypeScript asked for, in order.
1150
- * @returns The root-relative paths.
1151
- */
1152
- function sourcesInside(root, read) {
1153
- const inside = read.map((file) => toPosix(path.relative(root, file))).filter((file) => !file.startsWith("../") && !path.isAbsolute(file)).filter((file) => !file.split("/").includes("node_modules"));
1154
- return [...new Set(inside)];
1155
- }
1156
- /**
1157
- * Parses a tsconfig with TypeScript: comments and trailing commas included, its `extends` chain
1158
- * merged, and every file TypeScript reads for that chain noted. No file is listed, so the parse
1159
- * stays cheap.
1160
- *
1161
- * @param typescript - The TypeScript module.
1162
- * @param root - The real root.
1163
- * @param file - The tsconfig, root-relative POSIX.
1164
- * @param text - The text of the tsconfig.
1165
- * @returns The merged options and the files TypeScript read.
1166
- * @throws {Error} When the tsconfig or a file it extends does not parse.
1167
- */
1168
- function parseTsconfigOptions(typescript, root, file, text) {
1169
- const absolute = path.join(root, file);
1170
- const parsed = typescript.parseConfigFileTextToJson(absolute, text);
1171
- if (parsed.error !== void 0) throw doesNotParse(typescript, root, file, parsed.error);
1172
- const read = [];
1173
- const host = {
1174
- ...typescript.sys,
1175
- readDirectory: () => [],
1176
- readFile: (name) => {
1177
- read.push(name);
1178
- return typescript.sys.readFile(name);
1179
- }
1180
- };
1181
- const { options, errors } = typescript.parseJsonConfigFileContent(parsed.config, host, path.dirname(absolute), void 0, absolute);
1182
- const syntax = errors.find((diagnostic) => isSyntaxError(diagnostic));
1183
- if (syntax !== void 0) throw doesNotParse(typescript, root, file, syntax);
1184
- return {
1185
- options,
1186
- read
1187
- };
1188
- }
1189
- /**
1190
- * The folder TypeScript resolves `paths` against when no `baseUrl` is set: the folder of the
1191
- * config that declares them, which may be a file the tsconfig extends. TypeScript keeps it on the
1192
- * merged options as `pathsBasePath`, outside its published types.
1193
- *
1194
- * @param options - The merged options.
1195
- * @returns The absolute folder, or `undefined` when no config declares `paths`.
1196
- */
1197
- function pathsBaseOf(options) {
1198
- const base = options.pathsBasePath;
1199
- return typeof base === "string" ? base : void 0;
1200
- }
1201
- /**
1202
- * Reads the tsconfig `paths` of a game. TypeScript parses the file and merges its `extends`
1203
- * chain. The targets resolve against `baseUrl` when one is set, else against the folder of the
1204
- * config that declares `paths`, as TypeScript resolves them: the folder of the tsconfig, or of the
1205
- * file it extends that holds the block.
1206
- *
1207
- * @param typescript - The TypeScript module.
1208
- * @param root - The real root.
1209
- * @param file - The tsconfig, root-relative POSIX.
1210
- * @returns The aliases, or `undefined` when the file is missing. A tsconfig without `paths` has
1211
- * no patterns and still lists its sources, so a `paths` block added to a file it extends is seen.
1212
- * @throws {Error} When the tsconfig or a file it extends does not parse.
1213
- */
1214
- async function readAliases(typescript, root, file) {
1215
- const bytes = await readInside(root, file);
1216
- if (bytes === void 0) return void 0;
1217
- const { options, read } = parseTsconfigOptions(typescript, root, file, new TextDecoder().decode(bytes));
1218
- const patterns = patternsOf(root, options.baseUrl ?? pathsBaseOf(options) ?? path.dirname(path.join(root, file)), options.paths ?? {});
1219
- return {
1220
- file,
1221
- sources: [.../* @__PURE__ */ new Set([file, ...sourcesInside(root, read)])],
1222
- patterns
1223
- };
1224
- }
1225
- /**
1226
- * Whether a `*` key matches a specifier.
1227
- *
1228
- * @param pattern - A key with `*`.
1229
- * @param suffix - Its suffix.
1230
- * @param specifier - The module specifier.
1231
- * @returns True when the specifier starts with the prefix and ends with the suffix.
1232
- * @example
1233
- * ```ts
1234
- * matchesStar({ prefix: "@core/", suffix: "", targets: ["core/*"] }, "", "@core/kit"); // true
1235
- * ```
1236
- */
1237
- function matchesStar(pattern, suffix, specifier) {
1238
- return specifier.length >= pattern.prefix.length + suffix.length && specifier.startsWith(pattern.prefix) && specifier.endsWith(suffix);
1239
- }
1240
- /**
1241
- * Whether a specifier is never an alias: a path relative to the importing file, an absolute path,
1242
- * or the engine.
1243
- *
1244
- * @param specifier - The module specifier as written.
1245
- * @returns True when no key of `paths` may match it.
1246
- * @example
1247
- * ```ts
1248
- * isNeverAlias("@moku-labs/game/testing"); // true
1249
- * ```
1250
- */
1251
- function isNeverAlias(specifier) {
1252
- return NOT_BARE.test(specifier) || isEngine(specifier);
1253
- }
1254
- /**
1255
- * Whether a key of `paths` is a `*` key that matches a specifier with a longer prefix than the
1256
- * best match so far.
1257
- *
1258
- * @param pattern - A key of `paths`.
1259
- * @param best - The best `*` match so far, if any.
1260
- * @param specifier - The module specifier.
1261
- * @returns True when the key is the better match.
1262
- * @example
1263
- * ```ts
1264
- * isBetterMatch({ prefix: "@core/", suffix: "", targets: ["core/*"] }, undefined, "@core/kit"); // true
1265
- * ```
1266
- */
1267
- function isBetterMatch(pattern, best, specifier) {
1268
- const isLonger = best === void 0 || pattern.prefix.length > best.prefix.length;
1269
- return pattern.suffix !== void 0 && isLonger && matchesStar(pattern, pattern.suffix, specifier);
1270
- }
1271
- /**
1272
- * The targets a specifier maps to, the way TypeScript maps it: a key without `*` equal to the
1273
- * specifier wins; else the `*` key with the longest prefix, its `*` filled into every target. A
1274
- * relative or absolute specifier, the engine (`@moku-labs/game` and its subpaths) and a specifier
1275
- * no key matches map to nothing.
1276
- *
1277
- * @param aliases - The aliases of the game, if it declares some.
1278
- * @param specifier - The module specifier as written.
1279
- * @returns The root-relative targets in the order of the tsconfig; `[]` for no match.
1280
- * @example
1281
- * ```ts
1282
- * // map: the aliases of tests/fixtures/layout-game/tsconfig.json
1283
- * aliasTargets(map, "@features/orders"); // ["features/orders/index.ts"]
1284
- * aliasTargets(map, "@moku-labs/game/testing"); // []
1285
- * ```
1286
- */
1287
- function aliasTargets(aliases, specifier) {
1288
- if (aliases === void 0 || isNeverAlias(specifier)) return [];
1289
- const exact = aliases.patterns.find((pattern) => pattern.suffix === void 0 && pattern.prefix === specifier);
1290
- if (exact !== void 0) return [...exact.targets];
1291
- let best;
1292
- for (const pattern of aliases.patterns) if (isBetterMatch(pattern, best, specifier)) best = pattern;
1293
- if (best === void 0) return [];
1294
- const hole = specifier.slice(best.prefix.length, specifier.length - best.suffix.length);
1295
- return best.targets.map((target) => target.replace("*", () => hole));
1296
- }
1297
- /**
1298
- * Whether two keys of `paths` are the same key with the same targets.
1299
- *
1300
- * @param first - One pattern.
1301
- * @param second - The other, if any.
1302
- * @returns True when prefix, suffix and targets are equal.
1303
- * @example
1304
- * ```ts
1305
- * samePattern({ prefix: "@kit", targets: ["kit.ts"] }, { prefix: "@kit", targets: ["kit.ts"] }); // true
1306
- * ```
1307
- */
1308
- function samePattern(first, second) {
1309
- return second !== void 0 && first.prefix === second.prefix && first.suffix === second.suffix && first.targets.length === second.targets.length && first.targets.every((target, at) => target === second.targets[at]);
1310
- }
1311
- /**
1312
- * Whether two reads of the aliases resolve alike: the same tsconfig and the same patterns in the
1313
- * same order. The sources are left out: they feed the stamps, not the resolution.
1314
- *
1315
- * @param first - The aliases before, or `undefined` for none.
1316
- * @param second - The aliases after, or `undefined` for none.
1317
- * @returns True when the index built with either is the same.
1318
- * @example
1319
- * ```ts
1320
- * sameAliases(undefined, undefined); // true
1321
- * ```
1322
- */
1323
- function sameAliases(first, second) {
1324
- if (first === void 0 || second === void 0) return first === second;
1325
- return first.file === second.file && first.patterns.length === second.patterns.length && first.patterns.every((pattern, at) => samePattern(pattern, second.patterns[at]));
1326
- }
1327
- //#endregion
1328
- //#region src/project/extract/resolve.ts
1329
- /**
1330
- * @file project, extract — following a name across files with the module facts only: an import
1331
- * to the module it names, through a tsconfig alias when the game declares one, a re-export to the
1332
- * module behind it, a star export to the first module that has the name, until the name is
1333
- * declared, handed out by the kit, or comes from a package. This is how a definer is recognised
1334
- * by binding (a renamed import still is the definer) and how a node of a flow table reaches the
1335
- * file that declares it.
1336
- */
1337
- /** How many imports and re-exports a name is followed through before giving up. */
1338
- const MAX_HOPS = 12;
1339
- /** The extensions a TypeScript import may name instead of the source file. */
1340
- const SCRIPT_EXTENSION = /\.(?:[cm]?js|jsx)$/;
1341
- /**
1342
- * Whether a module specifier is relative to the importing file.
1343
- *
1344
- * @param specifier - The module specifier.
1345
- * @returns True for `./…` and `../…`.
1346
- * @example
1347
- * ```ts
1348
- * isRelative("../kit"); // true
1349
- * ```
1350
- */
1351
- function isRelative(specifier) {
1352
- return specifier === "." || specifier === ".." || specifier.startsWith("./") || specifier.startsWith("../");
1353
- }
1354
- /**
1355
- * The indexed file a root-relative path names, the way a bundler finds it: the path as written,
1356
- * then with `.ts`, `.tsx`, `/index.ts` and `/index.tsx`.
1357
- *
1358
- * @param modules - The module facts by path.
1359
- * @param base - A root-relative path.
1360
- * @returns The path the index holds, or `undefined`.
1361
- */
1362
- function indexedFile(modules, base) {
1363
- const normal = path.posix.normalize(base);
1364
- const stem = normal.replace(SCRIPT_EXTENSION, "");
1365
- return [
1366
- normal,
1367
- `${stem}.ts`,
1368
- `${stem}.tsx`,
1369
- `${stem}/index.ts`,
1370
- `${stem}/index.tsx`
1371
- ].find((candidate) => modules.has(candidate));
1372
- }
1373
- /**
1374
- * Resolves a module specifier to an indexed file: a relative one against the importing file, any
1375
- * other through the targets of the tsconfig alias it matches, the first target the index holds
1376
- * winning. Each candidate is tried as written, then with `.ts`, `.tsx`, `/index.ts` and
1377
- * `/index.tsx`.
1378
- *
1379
- * @param resolution - The module facts and the aliases.
1380
- * @param from - The importing file.
1381
- * @param specifier - The module specifier.
1382
- * @returns The root-relative path, or `undefined` for a package or a file the index does not hold.
1383
- */
1384
- function resolveModulePath(resolution, from, specifier) {
1385
- const { modules } = resolution;
1386
- if (isRelative(specifier)) return indexedFile(modules, path.posix.join(path.posix.dirname(from), specifier));
1387
- for (const target of aliasTargets(resolution.aliases, specifier)) {
1388
- const found = indexedFile(modules, target);
1389
- if (found !== void 0) return found;
1390
- }
1391
- }
1392
- /**
1393
- * Whether a specifier names a package: it is not relative and no tsconfig alias matches it.
1394
- *
1395
- * @param resolution - The module facts and the aliases.
1396
- * @param specifier - The module specifier.
1397
- * @returns True for a package.
1398
- */
1399
- function isPackage(resolution, specifier) {
1400
- return !isRelative(specifier) && aliasTargets(resolution.aliases, specifier).length === 0;
1401
- }
1402
- /**
1403
- * Follows an imported name into the module that the specifier names.
1404
- *
1405
- * @param resolution - The module facts and the aliases.
1406
- * @param from - The importing file.
1407
- * @param ref - The specifier and the name.
1408
- * @param hops - How many hops were taken so far.
1409
- * @returns Where the name ends up.
1410
- */
1411
- function followImport(resolution, from, ref, hops) {
1412
- if (isPackage(resolution, ref.specifier)) return {
1413
- kind: "package",
1414
- ...ref
1415
- };
1416
- if (ref.imported === "*") return void 0;
1417
- const target = resolveModulePath(resolution, from, ref.specifier);
1418
- return target === void 0 ? void 0 : resolveExport(resolution, target, ref.imported, hops + 1);
1419
- }
1420
- /**
1421
- * Follows an exported name of a module: a local binding, a re-export, or a star export.
1422
- *
1423
- * @param resolution - The module facts and the aliases.
1424
- * @param file - The module.
1425
- * @param name - The exported name.
1426
- * @param hops - How many hops were taken so far.
1427
- * @returns Where the name ends up.
1428
- */
1429
- function resolveExport(resolution, file, name, hops = 0) {
1430
- const facts = resolution.modules.get(file);
1431
- if (facts === void 0 || hops > MAX_HOPS) return void 0;
1432
- const ref = facts.exports.get(name);
1433
- if (ref !== void 0 && "local" in ref) return resolveBinding(resolution, file, ref.local, hops + 1);
1434
- if (ref !== void 0) return followImport(resolution, file, ref, hops);
1435
- for (const specifier of facts.stars) {
1436
- const found = followImport(resolution, file, {
1437
- specifier,
1438
- imported: name
1439
- }, hops);
1440
- if (found !== void 0) return found;
1441
- }
1442
- }
1443
- /**
1444
- * Follows a name used in a file to where it ends up: the kit hands it out, the file declares it,
1445
- * or an import brings it.
1446
- *
1447
- * @param resolution - The module facts and the aliases.
1448
- * @param file - The file that uses the name.
1449
- * @param local - The name as the file writes it.
1450
- * @param hops - How many hops were taken so far.
1451
- * @returns Where the name ends up, or `undefined` when it cannot be followed.
1452
- */
1453
- function resolveBinding(resolution, file, local, hops = 0) {
1454
- const facts = resolution.modules.get(file);
1455
- if (facts === void 0 || hops > MAX_HOPS) return void 0;
1456
- const definer = facts.kit.get(local);
1457
- if (definer !== void 0) return {
1458
- kind: "definer",
1459
- definer
1460
- };
1461
- if (facts.declared.has(local)) return {
1462
- kind: "declared",
1463
- path: file,
1464
- binding: local
1465
- };
1466
- const ref = facts.imports.get(local);
1467
- return ref === void 0 ? void 0 : followImport(resolution, file, ref, hops);
1468
- }
1469
- /**
1470
- * The alias a name of a file is imported through when that alias reaches no indexed file: a
1471
- * tsconfig key matched the specifier, and none of its targets is a file of the root.
1472
- *
1473
- * @param resolution - The module facts and the aliases.
1474
- * @param file - The file that imports the name.
1475
- * @param local - The name as the file writes it.
1476
- * @returns The specifier as written, or `undefined` when the import is not such an alias.
1477
- */
1478
- function missedAlias(resolution, file, local) {
1479
- const ref = resolution.modules.get(file)?.imports.get(local);
1480
- if (!(ref !== void 0 && !isRelative(ref.specifier) && !isPackage(resolution, ref.specifier))) return void 0;
1481
- return resolveModulePath(resolution, file, ref.specifier) === void 0 ? ref.specifier : void 0;
1482
- }
1483
- /**
1484
- * The definer a target is, if any: one the kit hands out, or one imported from the engine.
1485
- *
1486
- * @param target - Where a name ends up.
1487
- * @returns The definer, or `undefined`.
1488
- * @example
1489
- * ```ts
1490
- * definerOf({ kind: "package", specifier: "@moku-labs/game", imported: "defineStyle" }); // "defineStyle"
1491
- * ```
1492
- */
1493
- function definerOf(target) {
1494
- if (target?.kind === "definer") return target.definer;
1495
- if (target?.kind !== "package" || target.specifier !== "@moku-labs/game") return void 0;
1496
- return isDefiner(target.imported) ? target.imported : void 0;
1497
- }
1498
- /**
1499
- * Adds the definers a namespace import reaches as `ns.name`.
1500
- *
1501
- * @param resolution - The module facts and the aliases.
1502
- * @param file - The importing file.
1503
- * @param namespace - The local name of the namespace.
1504
- * @param specifier - The module it imports.
1505
- * @param definers - The map to add to.
1506
- */
1507
- function addNamespace(resolution, file, namespace, specifier, definers) {
1508
- if (specifier === "@moku-labs/game") {
1509
- for (const definer of DEFINERS) definers.set(`${namespace}.${definer}`, definer);
1510
- return;
1511
- }
1512
- const target = resolveModulePath(resolution, file, specifier);
1513
- const names = target === void 0 ? [] : [...resolution.modules.get(target)?.exports.keys() ?? []];
1514
- for (const name of names) {
1515
- const definer = definerOf(resolveExport(resolution, target ?? "", name));
1516
- if (definer !== void 0) definers.set(`${namespace}.${name}`, definer);
1517
- }
1518
- }
1519
- /**
1520
- * The definers a file can call, by the callee as the file writes it: `defineFlow`, a renamed
1521
- * `scene`, or `kit.defineScene` through a namespace.
1522
- *
1523
- * @param resolution - The module facts and the aliases.
1524
- * @param file - The file.
1525
- * @returns The callee text to the definer.
1526
- */
1527
- function definersOf(resolution, file) {
1528
- const facts = resolution.modules.get(file);
1529
- const definers = new Map(facts?.kit);
1530
- for (const [local, ref] of facts?.imports ?? []) {
1531
- if (ref.imported === "*") {
1532
- addNamespace(resolution, file, local, ref.specifier, definers);
1533
- continue;
1534
- }
1535
- const definer = definerOf(resolveBinding(resolution, file, local));
1536
- if (definer !== void 0) definers.set(local, definer);
1537
- }
1538
- return definers;
1539
- }
1540
- //#endregion
1541
- //#region src/project/hash.ts
1542
- /**
1543
- * @file project — the two hashes of the index: the sha1 of a file's bytes, the same value the
1544
- * editor uses as a file version, and the revision over every file of the index.
1545
- */
1546
- /**
1547
- * Hashes bytes or text with sha1.
1548
- *
1549
- * @param bytes - The file bytes, or a text.
1550
- * @returns The hex digest.
1551
- * @example
1552
- * ```ts
1553
- * sha1(""); // "da39a3ee5e6b4b0d3255bfef95601890afd80709"
1554
- * ```
1555
- */
1556
- function sha1(bytes) {
1557
- return createHash("sha1").update(bytes).digest("hex");
1558
- }
1559
- /**
1560
- * The revision of an index: the sha1 of the lines `${path}\0${hash}\n`, sorted by path.
1561
- *
1562
- * @param files - Every indexed file with its hash.
1563
- * @returns The hex digest.
1564
- * @example
1565
- * ```ts
1566
- * revisionOf({}); // "da39a3ee5e6b4b0d3255bfef95601890afd80709"
1567
- * ```
1568
- */
1569
- function revisionOf(files) {
1570
- return sha1(Object.entries(files).map(([file, entry]) => `${file}\0${entry.hash}\n`).toSorted().join(""));
1571
- }
1572
- /**
1573
- * The anchors of the JSX patterns and of the literal key-carrying props of an index.
1574
- *
1575
- * @param index - The index.
1576
- * @returns The prop-hole patterns, the `*` patterns and the `idProp` anchors.
1577
- */
1578
- function jsxShapes(index) {
1579
- const shapes = {
1580
- holePatterns: [],
1581
- wildPatterns: [],
1582
- idProps: []
1583
- };
1584
- for (const [key, entry] of Object.entries(index.symbols)) {
1585
- if (!key.startsWith("jsx:")) continue;
1586
- for (const anchor of entry.def) {
1587
- const pattern = anchor.key ?? "";
1588
- if (anchor.kind === "idProp") shapes.idProps.push(anchor);
1589
- else if (hasHole(pattern)) shapes.holePatterns.push(anchor);
1590
- else if (pattern.includes("*")) shapes.wildPatterns.push(anchor);
1591
- }
1592
- }
1593
- return shapes;
1594
- }
1595
- //#endregion
1596
- //#region src/project/assemble.ts
1597
- /** The prefix of a node key. */
1598
- const NODE = "node:";
1599
- /**
1600
- * The entry of a key, made on first use.
1601
- *
1602
- * @param symbols - The keys so far.
1603
- * @param key - The key.
1604
- * @returns Its entry.
1605
- */
1606
- function entryOf(symbols, key) {
1607
- const existing = symbols.get(key);
1608
- if (existing !== void 0) return existing;
1609
- const entry = {
1610
- def: [],
1611
- uses: []
1612
- };
1613
- symbols.set(key, entry);
1614
- return entry;
1615
- }
1616
- /**
1617
- * Why a name of a flow table cannot be followed: the alias it is imported through reaches no
1618
- * file, or it is declared in no file of the root.
1619
- *
1620
- * @param resolution - The module facts and the aliases.
1621
- * @param file - The flow file.
1622
- * @param node - The key of the entry, `main/home`.
1623
- * @param ref - The name the entry holds.
1624
- * @returns The reason for the unresolved list.
1625
- */
1626
- function unfollowed(resolution, file, node, ref) {
1627
- const alias = missedAlias(resolution, file, ref);
1628
- return alias === void 0 ? `node "${node}": "${ref}" is not declared in a file of the root` : `node "${node}": "${ref}" is imported from "${alias}", which names no file of the root`;
1629
- }
1630
- /**
1631
- * Adds the `node:` keys of one flow table. A plain name is followed to the file that declares
1632
- * it, and the table is its use; a slot or an inline node is anchored at the table. A name that
1633
- * cannot be followed is anchored at the table and listed as unresolved.
1634
- *
1635
- * @param assembly - The records and module facts.
1636
- * @param symbols - The keys so far.
1637
- * @param unresolved - The unresolved list so far.
1638
- * @param file - The flow file.
1639
- * @param table - The table.
1640
- */
1641
- function addTable(assembly, symbols, unresolved, file, table) {
1642
- const use = table.binding === void 0 ? { path: file } : {
1643
- path: file,
1644
- binding: table.binding
1645
- };
1646
- const resolution = {
1647
- modules: assembly.modules,
1648
- aliases: assembly.aliases
1649
- };
1650
- for (const { name, ref } of table.entries) {
1651
- const entry = entryOf(symbols, `${NODE}${table.flow}/${name}`);
1652
- const atTable = {
1653
- ...use,
1654
- key: name
1655
- };
1656
- const target = ref === void 0 ? void 0 : resolveBinding(resolution, file, ref);
1657
- if (target?.kind === "declared") {
1658
- entry.def.push({
1659
- path: target.path,
1660
- binding: target.binding
1661
- });
1662
- entry.uses.push(atTable);
1663
- continue;
1664
- }
1665
- entry.def.push(atTable);
1666
- if (ref !== void 0) unresolved.push({
1667
- path: file,
1668
- reason: unfollowed(resolution, file, `${table.flow}/${name}`, ref)
1669
- });
1670
- }
1671
- }
1672
- /**
1673
- * Adds, to every style key, the files that import its binding by name.
1674
- *
1675
- * @param assembly - The records and module facts.
1676
- * @param symbols - The keys so far.
1677
- */
1678
- function addStyleUses(assembly, symbols) {
1679
- const resolution = {
1680
- modules: assembly.modules,
1681
- aliases: assembly.aliases
1682
- };
1683
- for (const [file, record] of assembly.files) for (const [local, ref] of record.good?.module.imports ?? []) {
1684
- const target = ref.imported === "*" ? void 0 : resolveBinding(resolution, file, local);
1685
- (target?.kind === "declared" ? symbols.get(`style:${target.path}#${target.binding}`) : void 0)?.uses.push({
1686
- path: file,
1687
- binding: local
1688
- });
1689
- }
1690
- }
1691
- /**
1692
- * The component entries by the binding they are rendered as. Two anchors of one name in a
1693
- * conflict share one binding, so a file that renders it is one use.
1694
- *
1695
- * @param symbols - The keys so far.
1696
- * @returns Binding to the component entries bound to it.
1697
- */
1698
- function componentsByBinding(symbols) {
1699
- const byBinding = /* @__PURE__ */ new Map();
1700
- for (const [key, entry] of symbols) {
1701
- if (!key.startsWith("component:")) continue;
1702
- const bindings = new Set(entry.def.flatMap((anchor) => anchor.binding ?? []));
1703
- for (const binding of bindings) byBinding.set(binding, [...byBinding.get(binding) ?? [], entry]);
1704
- }
1705
- return byBinding;
1706
- }
1707
- /**
1708
- * The component names one file renders.
1709
- *
1710
- * @param record - The record of the file.
1711
- * @returns The names; none for a broken file.
1712
- */
1713
- function renderedBy(record) {
1714
- return record.good === void 0 ? [] : record.extracted?.result.rendered ?? [];
1715
- }
1716
- /**
1717
- * Adds one use of a rendered binding to every component entry bound to it.
1718
- *
1719
- * @param byBinding - Binding to the component entries bound to it.
1720
- * @param file - The file that renders the binding.
1721
- * @param binding - The rendered name.
1722
- */
1723
- function useComponent(byBinding, file, binding) {
1724
- for (const entry of byBinding.get(binding) ?? []) entry.uses.push({
1725
- path: file,
1726
- binding
1727
- });
1728
- }
1729
- /**
1730
- * Adds, to every component key, the files that render its binding: one use per file and binding,
1731
- * in path order. A member tag `<ui.RoundButton>` renders `RoundButton`.
1732
- *
1733
- * @param assembly - The records and module facts.
1734
- * @param symbols - The keys so far.
1735
- */
1736
- function addComponentUses(assembly, symbols) {
1737
- const byBinding = componentsByBinding(symbols);
1738
- for (const [file, record] of assembly.files) for (const binding of renderedBy(record)) useComponent(byBinding, file, binding);
1739
- }
1740
- /**
1741
- * The symbols as the index holds them: sorted by key, `uses` only when there are some, and
1742
- * `conflict` on a key other than `jsx:` defined twice.
1743
- *
1744
- * @param symbols - The keys as collected.
1745
- * @returns The symbols of the index.
1746
- */
1747
- function finishSymbols(symbols) {
1748
- const finished = {};
1749
- const sorted = [...symbols].toSorted(([first], [second]) => first < second ? -1 : 1);
1750
- for (const [key, { def, uses }] of sorted) {
1751
- const isConflict = !key.startsWith("jsx:") && def.length > 1;
1752
- finished[key] = {
1753
- def,
1754
- ...uses.length === 0 ? {} : { uses },
1755
- ...isConflict ? { conflict: true } : {}
1756
- };
1757
- }
1758
- return finished;
1759
- }
1760
- /**
1761
- * Freezes a value and everything it holds.
1762
- *
1763
- * @param value - A plain value.
1764
- * @returns The same value, frozen.
1765
- */
1766
- function deepFreeze(value) {
1767
- if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
1768
- Object.freeze(value);
1769
- for (const child of Object.values(value)) deepFreeze(child);
1770
- return value;
1771
- }
1772
- /**
1773
- * Assembles the index of a catalog. The index names the tsconfig when the assembly has aliases.
1774
- *
1775
- * @param assembly - The records sorted by path, the module facts of the good files, the aliases.
1776
- * @param manifest - The root-relative manifest path when the file exists.
1777
- * @returns The index, frozen.
1778
- */
1779
- function assembleIndex(assembly, manifest) {
1780
- const symbols = /* @__PURE__ */ new Map();
1781
- const files = {};
1782
- const unresolved = [];
1783
- for (const [file, record] of assembly.files) {
1784
- files[file] = {
1785
- hash: record.hash,
1786
- state: record.state,
1787
- ...record.error === void 0 ? {} : { error: record.error }
1788
- };
1789
- const extracted = record.extracted?.result;
1790
- if (record.good === void 0 || extracted === void 0) {
1791
- unresolved.push({
1792
- path: file,
1793
- reason: record.error ?? "the file never parsed"
1794
- });
1795
- continue;
1796
- }
1797
- for (const { key, anchor } of extracted.definitions) entryOf(symbols, key).def.push(anchor);
1798
- unresolved.push(...extracted.unresolved);
1799
- for (const table of extracted.tables) addTable(assembly, symbols, unresolved, file, table);
1800
- }
1801
- addStyleUses(assembly, symbols);
1802
- addComponentUses(assembly, symbols);
1803
- return deepFreeze({
1804
- schemaVersion: 1,
1805
- revision: revisionOf(files),
1806
- ...manifest === void 0 ? {} : { manifest },
1807
- ...assembly.aliases === void 0 ? {} : { tsconfig: assembly.aliases.file },
1808
- symbols: finishSymbols(symbols),
1809
- files,
1810
- unresolved
1811
- });
1812
- }
1813
- //#endregion
1814
- //#region src/project/extract/definitions.ts
1815
- /** The key prefix of each definer that takes its id as the first argument. */
1816
- const ID_PREFIXES = {
1817
- defineFlow: "flow",
1818
- defineFeature: "feature",
1819
- defineScene: "scene",
1820
- defineEmitter: "emitter",
1821
- defineComponent: "component"
1822
- };
1823
- /**
1824
- * The text of a string literal or a template without holes.
1825
- *
1826
- * @param typescript - The TypeScript module.
1827
- * @param node - An expression, if any.
1828
- * @returns The text, or `undefined` for anything else.
1829
- */
1830
- function literalOf(typescript, node) {
1831
- const inner = node === void 0 ? void 0 : unwrap(typescript, node);
1832
- return inner !== void 0 && typescript.isStringLiteralLike(inner) ? inner.text : void 0;
1833
- }
1834
- /**
1835
- * The value of a named property of an object literal; for a shorthand, the name itself.
1836
- *
1837
- * @param typescript - The TypeScript module.
1838
- * @param object - The object literal.
1839
- * @param name - The property name.
1840
- * @returns The value, or `undefined` when the object has no such property.
1841
- */
1842
- function propertyValue(typescript, object, name) {
1843
- for (const property of object.properties) {
1844
- if (propertyNameOf(typescript, property) !== name) continue;
1845
- if (typescript.isPropertyAssignment(property)) return property.initializer;
1846
- if (typescript.isShorthandPropertyAssignment(property)) return property.name;
1847
- }
1848
- }
1849
- /**
1850
- * The object literal an expression is, directly or through a same-file const.
1851
- *
1852
- * @param typescript - The TypeScript module.
1853
- * @param node - An expression, if any.
1854
- * @returns The object literal, or `undefined`.
1855
- */
1856
- function objectOf(typescript, node) {
1857
- const inner = node === void 0 ? void 0 : unwrap(typescript, node);
1858
- if (inner === void 0) return void 0;
1859
- if (typescript.isObjectLiteralExpression(inner)) return inner;
1860
- if (!typescript.isIdentifier(inner)) return void 0;
1861
- const meaning = meaningOf(typescript, inner);
1862
- const value = meaning?.kind === "const" ? unwrap(typescript, meaning.initializer) : void 0;
1863
- return value !== void 0 && typescript.isObjectLiteralExpression(value) ? value : void 0;
1864
- }
1865
- /**
1866
- * The const a call is bound to: `export const boardFlow = defineFlow(…)` at the top of the file.
1867
- *
1868
- * @param typescript - The TypeScript module.
1869
- * @param call - The definer call.
1870
- * @returns The binding, or `undefined` when the call is not the initializer of a module const.
1871
- */
1872
- function bindingOf(typescript, call) {
1873
- let node = call.parent;
1874
- while (typescript.isExpression(node)) node = node.parent;
1875
- if (!typescript.isVariableDeclaration(node) || !typescript.isIdentifier(node.name)) return void 0;
1876
- if (node.initializer === void 0 || unwrap(typescript, node.initializer) !== call) return void 0;
1877
- const list = node.parent;
1878
- return (list.flags & typescript.NodeFlags.Const) !== 0 && typescript.isVariableStatement(list.parent) && typescript.isSourceFile(list.parent.parent) ? node.name.text : void 0;
1879
- }
1880
- /**
1881
- * The definer a call calls, by its callee as written: `defineFlow(…)` or `kit.defineFlow(…)`.
1882
- *
1883
- * @param typescript - The TypeScript module.
1884
- * @param call - The call.
1885
- * @param definers - The definers of the file.
1886
- * @returns The definer, or `undefined` for any other call.
1887
- */
1888
- function definerOfCall(typescript, call, definers) {
1889
- const callee = call.expression;
1890
- if (typescript.isIdentifier(callee)) return definers.get(callee.text);
1891
- if (!typescript.isPropertyAccessExpression(callee) || !typescript.isIdentifier(callee.expression)) return;
1892
- return definers.get(`${callee.expression.text}.${callee.name.text}`);
1893
- }
1894
- /**
1895
- * Adds a key bound to the const of a call, or anchored by its id when the call has no const.
1896
- *
1897
- * @param reading - The file being read.
1898
- * @param call - The definer call.
1899
- * @param key - The key.
1900
- * @param id - The literal id, the anchor key when there is no binding.
1901
- * @param bound - What the anchor adds to the binding: the `name` key of a projection.
1902
- */
1903
- function define(reading, call, key, id, bound = {}) {
1904
- const binding = bindingOf(reading.typescript, call);
1905
- const anchor = binding === void 0 ? {
1906
- path: reading.path,
1907
- key: id
1908
- } : {
1909
- path: reading.path,
1910
- binding,
1911
- ...bound
1912
- };
1913
- reading.definitions.push({
1914
- key,
1915
- anchor
1916
- });
1917
- }
1918
- /**
1919
- * Notes something the file holds that cannot be read.
1920
- *
1921
- * @param reading - The file being read.
1922
- * @param reason - What and why.
1923
- */
1924
- function unresolve(reading, reason) {
1925
- reading.unresolved.push({
1926
- path: reading.path,
1927
- reason
1928
- });
1929
- }
1930
- /**
1931
- * The plain name a table entry's value is: `merge` for `{ merge }` and for `{ merge: merge }`.
1932
- *
1933
- * @param typescript - The TypeScript module.
1934
- * @param property - The table entry.
1935
- * @returns The name, or `undefined` for a slot, an inline node or anything else.
1936
- */
1937
- function refOf(typescript, property) {
1938
- if (typescript.isShorthandPropertyAssignment(property)) return property.name.text;
1939
- if (!typescript.isPropertyAssignment(property)) return void 0;
1940
- const value = unwrap(typescript, property.initializer);
1941
- return typescript.isIdentifier(value) ? value.text : void 0;
1942
- }
1943
- /**
1944
- * Reads the `nodes` table of a flow into its entries.
1945
- *
1946
- * @param reading - The file being read.
1947
- * @param call - The `defineFlow` call.
1948
- * @param flow - The flow id.
1949
- */
1950
- function readTable(reading, call, flow) {
1951
- const { typescript } = reading;
1952
- const spec = objectOf(typescript, call.arguments[1]);
1953
- const nodes = spec === void 0 ? void 0 : objectOf(typescript, propertyValue(typescript, spec, "nodes"));
1954
- if (nodes === void 0) {
1955
- unresolve(reading, `defineFlow "${flow}" nodes is not an object literal`);
1956
- return;
1957
- }
1958
- const entries = [];
1959
- for (const property of nodes.properties) {
1960
- if (typescript.isSpreadAssignment(property)) {
1961
- unresolve(reading, `defineFlow "${flow}" nodes has a spread`);
1962
- continue;
1963
- }
1964
- const name = propertyNameOf(typescript, property);
1965
- if (name === void 0) {
1966
- unresolve(reading, `defineFlow "${flow}" node name is not a literal`);
1967
- continue;
1968
- }
1969
- const ref = refOf(typescript, property);
1970
- entries.push(ref === void 0 ? { name } : {
1971
- name,
1972
- ref
1973
- });
1974
- }
1975
- const binding = bindingOf(typescript, call);
1976
- reading.tables.push(binding === void 0 ? {
1977
- flow,
1978
- entries
1979
- } : {
1980
- flow,
1981
- binding,
1982
- entries
1983
- });
1984
- }
1985
- /**
1986
- * Reads a definer that takes its id as the first argument; a flow also brings its table. A
1987
- * component is bound to its const, and the key takes the literal id.
1988
- *
1989
- * @param reading - The file being read.
1990
- * @param call - The call.
1991
- * @param definer - The definer.
1992
- * @param prefix - The key prefix.
1993
- */
1994
- function readIdCall(reading, call, definer, prefix) {
1995
- const id = literalOf(reading.typescript, call.arguments[0]);
1996
- if (id === void 0) {
1997
- unresolve(reading, `${definer} id is not a string literal`);
1998
- return;
1999
- }
2000
- define(reading, call, `${prefix}:${id}`, id);
2001
- if (definer === "defineFlow") readTable(reading, call, id);
2002
- }
2003
- /**
2004
- * Reads a projection: its key is its `name`, its line the `name` property.
2005
- *
2006
- * @param reading - The file being read.
2007
- * @param call - The call.
2008
- */
2009
- function readProjection(reading, call) {
2010
- const { typescript } = reading;
2011
- const spec = objectOf(typescript, call.arguments[0]);
2012
- const name = spec === void 0 ? void 0 : literalOf(typescript, propertyValue(typescript, spec, "name"));
2013
- if (name === void 0) {
2014
- unresolve(reading, "projection name is not a string literal");
2015
- return;
2016
- }
2017
- define(reading, call, `projection:${name}`, name, { key: "name" });
2018
- }
2019
- /**
2020
- * Reads a text style table: every literal key is a key.
2021
- *
2022
- * @param reading - The file being read.
2023
- * @param call - The call.
2024
- */
2025
- function readTextStyles(reading, call) {
2026
- const { typescript } = reading;
2027
- const table = call.arguments[0] === void 0 ? void 0 : unwrap(typescript, call.arguments[0]);
2028
- if (table === void 0 || !typescript.isObjectLiteralExpression(table)) {
2029
- unresolve(reading, "defineTextStyles argument is not an object literal");
2030
- return;
2031
- }
2032
- const binding = bindingOf(typescript, call);
2033
- for (const property of table.properties) {
2034
- const key = propertyNameOf(typescript, property);
2035
- if (key === void 0) {
2036
- unresolve(reading, "defineTextStyles key is not a literal");
2037
- continue;
2038
- }
2039
- const anchor = binding === void 0 ? {
2040
- path: reading.path,
2041
- key
2042
- } : {
2043
- path: reading.path,
2044
- binding,
2045
- key
2046
- };
2047
- reading.definitions.push({
2048
- key: `textStyle:${key}`,
2049
- anchor
2050
- });
2051
- }
2052
- }
2053
- /**
2054
- * Adds a style key; the same key and anchor twice in one file is one anchor, since `find`
2055
- * answers every style call of a factory.
2056
- *
2057
- * @param reading - The file being read.
2058
- * @param key - The key.
2059
- * @param anchor - The anchor.
2060
- */
2061
- function addStyle(reading, key, anchor) {
2062
- const identity = JSON.stringify({
2063
- key,
2064
- anchor
2065
- });
2066
- if (reading.styles.has(identity)) return;
2067
- reading.styles.add(identity);
2068
- reading.definitions.push({
2069
- key,
2070
- anchor
2071
- });
2072
- }
2073
- /**
2074
- * The key and anchor of a style built in a module-level function: `style:<path>#<function>`, or
2075
- * `style:<path>#<function>.<property>` when the call is the value of an object property there.
2076
- *
2077
- * @param reading - The file being read.
2078
- * @param call - The `defineStyle` call.
2079
- * @param factory - The name of the function the call sits in.
2080
- * @returns The key and its anchor.
2081
- */
2082
- function styleKeyIn(reading, call, factory) {
2083
- const { typescript, path } = reading;
2084
- const property = propertyOfValue(typescript, call);
2085
- if (property === void 0) return {
2086
- key: `style:${path}#${factory}`,
2087
- anchor: {
2088
- path,
2089
- binding: factory
2090
- }
2091
- };
2092
- return {
2093
- key: `style:${path}#${factory}.${property}`,
2094
- anchor: {
2095
- path,
2096
- binding: factory,
2097
- key: property
2098
- }
2099
- };
2100
- }
2101
- /**
2102
- * Reads a style. One bound to a module const is `style:<path>#<binding>`. One built in a
2103
- * module-level function, at any depth, is `style:<path>#<function>`, or
2104
- * `style:<path>#<function>.<property>` when it is the value of an object property there. Any
2105
- * other style is unresolved.
2106
- *
2107
- * @param reading - The file being read.
2108
- * @param call - The call.
2109
- */
2110
- function readStyle(reading, call) {
2111
- const { typescript, path } = reading;
2112
- const binding = bindingOf(typescript, call);
2113
- if (binding !== void 0) {
2114
- addStyle(reading, `style:${path}#${binding}`, {
2115
- path,
2116
- binding
2117
- });
2118
- return;
2119
- }
2120
- const factory = enclosingFunction(typescript, call);
2121
- if (factory !== void 0) {
2122
- const style = styleKeyIn(reading, call, factory);
2123
- addStyle(reading, style.key, style.anchor);
2124
- return;
2125
- }
2126
- const around = enclosingName(typescript, call);
2127
- unresolve(reading, `defineStyle${around === void 0 ? "" : ` in "${around}"`} is not bound to a module-level const`);
2128
- }
2129
- /**
2130
- * Reads one definer call.
2131
- *
2132
- * @param reading - The file being read.
2133
- * @param call - The call.
2134
- * @param definer - The definer it calls.
2135
- */
2136
- function readCall(reading, call, definer) {
2137
- const prefix = ID_PREFIXES[definer];
2138
- if (prefix !== void 0) readIdCall(reading, call, definer, prefix);
2139
- if (definer === "projection") readProjection(reading, call);
2140
- if (definer === "defineTextStyles") readTextStyles(reading, call);
2141
- if (definer === "defineStyle") readStyle(reading, call);
2142
- }
2143
- /**
2144
- * Adds the JSX keys of the file; the same key twice in one file is one anchor, since `find`
2145
- * answers every place of an anchor in its file.
2146
- *
2147
- * @param reading - The file being read.
2148
- * @param source - The parsed file.
2149
- */
2150
- function readJsx(reading, source) {
2151
- const seen = /* @__PURE__ */ new Set();
2152
- for (const hit of collectJsx(reading.typescript, source)) {
2153
- if (isOpaque(hit)) {
2154
- unresolve(reading, opaqueReason(hit));
2155
- continue;
2156
- }
2157
- const anchor = anchorOfHit(reading.path, hit);
2158
- const identity = JSON.stringify(anchor);
2159
- if (seen.has(identity)) continue;
2160
- seen.add(identity);
2161
- reading.definitions.push({
2162
- key: `jsx:${hit.key}`,
2163
- anchor
2164
- });
2165
- }
2166
- }
2167
- /**
2168
- * Reads the keys one file defines.
2169
- *
2170
- * @param typescript - The TypeScript module.
2171
- * @param source - The parsed file.
2172
- * @param path - The root-relative path of the file.
2173
- * @param definers - The definers the file can call, from `definersOf`.
2174
- * @returns The definitions, the node tables and the unresolved items in source order, and the
2175
- * component names the file renders.
2176
- */
2177
- function extractFile(typescript, source, path, definers) {
2178
- const reading = {
2179
- typescript,
2180
- path,
2181
- definers,
2182
- definitions: [],
2183
- tables: [],
2184
- unresolved: [],
2185
- styles: /* @__PURE__ */ new Set()
2186
- };
2187
- const visit = (node) => {
2188
- const definer = typescript.isCallExpression(node) ? definerOfCall(typescript, node, definers) : void 0;
2189
- if (definer !== void 0 && typescript.isCallExpression(node)) readCall(reading, node, definer);
2190
- typescript.forEachChild(node, visit);
2191
- };
2192
- if (definers.size > 0) visit(source);
2193
- reading.definitions.push(...readComponents(typescript, source, path));
2194
- readJsx(reading, source);
2195
- return {
2196
- definitions: reading.definitions,
2197
- tables: reading.tables,
2198
- unresolved: reading.unresolved,
2199
- rendered: renderedOf(typescript, source)
2200
- };
2201
- }
2202
- //#endregion
2203
- //#region src/project/typescript.ts
2204
- /**
2205
- * Loads the optional peer `typescript`.
2206
- *
2207
- * @param load - The import; the real dynamic import by default.
2208
- * @returns The TypeScript module.
2209
- * @throws {Error} When the package is not installed.
2210
- */
2211
- async function loadTypeScript(load = () => import("typescript")) {
2212
- try {
2213
- const loaded = await load();
2214
- return loaded.default ?? loaded;
2215
- } catch {
2216
- throw new Error("[game] The project index needs the \"typescript\" package.\n Install it as a dev dependency of the game.");
2217
- }
2218
- }
2219
- /**
2220
- * Formats the first parse error of a file as `path:line:col message`, 1-based.
2221
- *
2222
- * @param typescript - The TypeScript module.
2223
- * @param source - The parsed file.
2224
- * @param diagnostic - The parse error.
2225
- * @returns The message.
2226
- */
2227
- function describeError(typescript, source, diagnostic) {
2228
- const { line, character } = source.getLineAndCharacterOfPosition(diagnostic.start);
2229
- const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, " ");
2230
- return `${source.fileName}:${line + 1}:${character + 1} ${message}`;
2231
- }
2232
- /**
2233
- * Parses one file with parent links, as TSX for a `.tsx` path and as TS otherwise.
2234
- *
2235
- * @param typescript - The TypeScript module.
2236
- * @param path - The root-relative path; it names the file in the error.
2237
- * @param text - The source text.
2238
- * @returns The tree and the first parse error.
2239
- */
2240
- function parseFile(typescript, path, text) {
2241
- const kind = path.endsWith(".tsx") ? typescript.ScriptKind.TSX : typescript.ScriptKind.TS;
2242
- const source = typescript.createSourceFile(path, text, typescript.ScriptTarget.Latest, true, kind);
2243
- const [first] = source.parseDiagnostics ?? [];
2244
- if (first === void 0) return { source };
2245
- return {
2246
- source,
2247
- error: describeError(typescript, source, first)
2248
- };
2249
- }
2250
- //#endregion
2251
- //#region src/project/catalog.ts
2252
- /** How many parses the cache keeps; the oldest goes first. */
2253
- const PARSE_CACHE_SIZE = 512;
2254
- /**
2255
- * Creates an empty catalog.
2256
- *
2257
- * @param typescript - The TypeScript module.
2258
- * @returns The catalog.
2259
- */
2260
- function createCatalog(typescript) {
2261
- return {
2262
- typescript,
2263
- records: /* @__PURE__ */ new Map(),
2264
- parses: /* @__PURE__ */ new Map()
2265
- };
2266
- }
2267
- /**
2268
- * Parses one version of a file, or takes it from the cache.
2269
- *
2270
- * @param catalog - The catalog.
2271
- * @param path - The root-relative path.
2272
- * @param hash - The sha1 of the bytes.
2273
- * @param text - The text of the bytes.
2274
- * @returns The parse.
2275
- */
2276
- function parseCached(catalog, path, hash, text) {
2277
- const id = `${path}\0${hash}`;
2278
- const cached = catalog.parses.get(id);
2279
- catalog.parses.delete(id);
2280
- const parsed = cached ?? parseFile(catalog.typescript, path, text);
2281
- catalog.parses.set(id, parsed);
2282
- for (const old of catalog.parses.keys()) {
2283
- if (catalog.parses.size <= PARSE_CACHE_SIZE) break;
2284
- catalog.parses.delete(old);
2285
- }
2286
- return parsed;
2287
- }
2288
- /**
2289
- * Takes in the bytes of a file. Unchanged bytes change nothing; bytes that parse replace the
2290
- * good version; bytes that do not parse mark the file broken and keep the good version.
2291
- *
2292
- * @param catalog - The catalog.
2293
- * @param path - The root-relative path.
2294
- * @param bytes - The bytes on disk.
2295
- * @returns True when the bytes differ from the ones last read.
2296
- */
2297
- function putFile(catalog, path, bytes) {
2298
- const hash = sha1(bytes);
2299
- const record = catalog.records.get(path);
2300
- if (record?.hash === hash) return false;
2301
- const text = new TextDecoder().decode(bytes);
2302
- const parsed = parseCached(catalog, path, hash, text);
2303
- if (parsed.error === void 0) {
2304
- const good = {
2305
- hash,
2306
- text,
2307
- module: readModule(catalog.typescript, parsed.source)
2308
- };
2309
- catalog.records.set(path, {
2310
- hash,
2311
- state: "ok",
2312
- good
2313
- });
2314
- return true;
2315
- }
2316
- catalog.records.set(path, {
2317
- hash,
2318
- state: "broken",
2319
- error: parsed.error,
2320
- ...record?.good === void 0 ? {} : { good: record.good },
2321
- ...record?.extracted === void 0 ? {} : { extracted: record.extracted }
2322
- });
2323
- return true;
2324
- }
2325
- /**
2326
- * Drops a file that is gone.
2327
- *
2328
- * @param catalog - The catalog.
2329
- * @param path - The root-relative path.
2330
- * @returns True when the catalog held the file.
2331
- */
2332
- function dropFile(catalog, path) {
2333
- return catalog.records.delete(path);
2334
- }
2335
- /**
2336
- * Brings the keys of every good file up to date: a file is read again when its good version or
2337
- * the definers it reaches changed since it was last read. An alias change that moves the
2338
- * definers of a file moves its stamp, so a tsconfig edit re-reads exactly the files it affects.
2339
- *
2340
- * @param catalog - The catalog.
2341
- * @param resolution - The module facts of every good file, and the aliases.
2342
- */
2343
- function refreshExtractions(catalog, resolution) {
2344
- for (const [path, record] of catalog.records) {
2345
- const good = record.good;
2346
- if (good === void 0) continue;
2347
- const definers = definersOf(resolution, path);
2348
- const signature = [...definers].map(([callee, definer]) => `${callee}=${definer}`).join(";");
2349
- const stamp = `${good.hash}|${signature}`;
2350
- if (record.extracted?.definers === stamp) continue;
2351
- const { source } = parseCached(catalog, path, good.hash, good.text);
2352
- record.extracted = {
2353
- definers: stamp,
2354
- result: extractFile(catalog.typescript, source, path, definers)
2355
- };
2356
- }
2357
- }
2358
- /**
2359
- * Builds the index of everything the catalog holds.
2360
- *
2361
- * @param catalog - The catalog.
2362
- * @param manifest - The root-relative manifest path when the file exists.
2363
- * @param aliases - The tsconfig aliases when the game declares some.
2364
- * @returns The index, frozen.
2365
- */
2366
- function buildIndex(catalog, manifest, aliases) {
2367
- const files = [...catalog.records].toSorted(([first], [second]) => first < second ? -1 : 1);
2368
- const modules = /* @__PURE__ */ new Map();
2369
- for (const [path, record] of files) if (record.good !== void 0) modules.set(path, record.good.module);
2370
- refreshExtractions(catalog, {
2371
- modules,
2372
- aliases
2373
- });
2374
- return assembleIndex({
2375
- files,
2376
- modules,
2377
- aliases
2378
- }, manifest);
2379
- }
2380
- //#endregion
2381
3
  //#region src/project/locate.ts
2382
4
  /** The kind of a style key. */
2383
5
  const STYLE_KIND = "style";
@@ -2616,7 +238,7 @@ function patternAnchors(shapes, wanted) {
2616
238
  */
2617
239
  function anchorsOf(session, key) {
2618
240
  const exact = session.index.symbols[key]?.def ?? [];
2619
- const reached = key.startsWith("jsx:") && !key.includes("*") && !hasHole(key) ? patternAnchors(shapesOf(session), key.slice(4)) : [];
241
+ const reached = key.startsWith("jsx:") && !key.includes("*") && !hasHole(key) ? patternAnchors(shapesOf(session), key.slice(JSX.length)) : [];
2620
242
  return [.../* @__PURE__ */ new Set([...exact, ...reached])];
2621
243
  }
2622
244
  /**
@@ -2647,7 +269,8 @@ async function viewOf(session, file) {
2647
269
  const bytes = await readInside(session.root, file);
2648
270
  if (bytes === void 0) return void 0;
2649
271
  const hash = sha1(bytes);
2650
- const fresh = parseCached(catalog, file, hash, hash === good?.hash ? good.text : new TextDecoder().decode(bytes));
272
+ const text = hash === good?.hash ? good.text : new TextDecoder().decode(bytes);
273
+ const fresh = parseCached(catalog, file, hash, text);
2651
274
  if (fresh.error === void 0) return {
2652
275
  source: fresh.source,
2653
276
  hash,
@@ -2743,384 +366,6 @@ async function findKey(session, key) {
2743
366
  return found;
2744
367
  }
2745
368
  //#endregion
2746
- //#region src/project/change.ts
2747
- /**
2748
- * The files that define a key, each once, in the order of its anchors.
2749
- *
2750
- * @param entry - The symbol entry.
2751
- * @param entry.def - Its definitions.
2752
- * @returns The paths.
2753
- */
2754
- function pathsOf(entry) {
2755
- return [...new Set(entry.def.map((anchor) => anchor.path))];
2756
- }
2757
- /**
2758
- * Compares the index before a batch with the index after it.
2759
- *
2760
- * @param before - The index before the batch.
2761
- * @param after - The index after the batch.
2762
- * @param files - The paths whose bytes changed, appeared or vanished.
2763
- * @returns The change: the new revision, the files sorted, the moves and the removed keys.
2764
- */
2765
- function diffIndexes(before, after, files) {
2766
- const moved = [];
2767
- const removed = [];
2768
- for (const [key, entry] of Object.entries(before.symbols)) {
2769
- const now = after.symbols[key];
2770
- if (now === void 0) {
2771
- removed.push(key);
2772
- continue;
2773
- }
2774
- const was = pathsOf(entry);
2775
- const is = pathsOf(now);
2776
- const left = was.filter((file) => !is.includes(file));
2777
- const arrived = is.filter((file) => !was.includes(file));
2778
- for (const [index, from] of left.entries()) {
2779
- const to = arrived[index];
2780
- if (to !== void 0) moved.push({
2781
- key,
2782
- from,
2783
- to
2784
- });
2785
- }
2786
- }
2787
- return {
2788
- revision: after.revision,
2789
- files: files.toSorted(),
2790
- moved,
2791
- removed
2792
- };
2793
- }
2794
- //#endregion
2795
- //#region src/project/walk.ts
2796
- /**
2797
- * Reads the stamp of a file.
2798
- *
2799
- * @param file - The absolute path.
2800
- * @returns The stamp, or `undefined` when the file is gone.
2801
- */
2802
- async function stampOf(file) {
2803
- try {
2804
- const stats = await stat(file, { bigint: true });
2805
- return {
2806
- ino: stats.ino,
2807
- size: stats.size,
2808
- mtimeNs: stats.mtimeNs,
2809
- ctimeNs: stats.ctimeNs
2810
- };
2811
- } catch {
2812
- return;
2813
- }
2814
- }
2815
- /**
2816
- * Whether two stamps describe the same write.
2817
- *
2818
- * @param first - One stamp.
2819
- * @param second - The other.
2820
- * @returns True when inode, size and both time stamps are equal.
2821
- * @example
2822
- * ```ts
2823
- * const stamp = { ino: 1n, size: 10n, mtimeNs: 5n, ctimeNs: 5n };
2824
- * sameStamp(stamp, { ...stamp, size: 11n }); // false
2825
- * ```
2826
- */
2827
- function sameStamp(first, second) {
2828
- return first.ino === second.ino && first.size === second.size && first.mtimeNs === second.mtimeNs && first.ctimeNs === second.ctimeNs;
2829
- }
2830
- /**
2831
- * Lists one folder, empty when it vanished during the walk.
2832
- *
2833
- * @param folder - The absolute folder.
2834
- * @returns Its entries.
2835
- */
2836
- async function entriesOf(folder) {
2837
- return readdir(folder, { withFileTypes: true }).catch(() => []);
2838
- }
2839
- /**
2840
- * Reads one folder of the walk: its sub-folders join the queue, its source files join the list
2841
- * with their stamps.
2842
- *
2843
- * @param root - The real root.
2844
- * @param folder - The folder, root-relative; `""` is the root.
2845
- * @param walk - The files so far, and the queue of folders still to read.
2846
- * @param walk.files - The files so far.
2847
- * @param walk.pending - The folders still to read.
2848
- */
2849
- async function readFolder(root, folder, walk) {
2850
- for (const entry of await entriesOf(path.join(root, folder))) {
2851
- const relative = folder === "" ? entry.name : `${folder}/${entry.name}`;
2852
- if (entry.isDirectory() && !isSkippedFolder(entry.name)) walk.pending.push(relative);
2853
- if (!entry.isFile() || !isIndexedFile(relative)) continue;
2854
- const stamp = await stampOf(path.join(root, relative));
2855
- if (stamp !== void 0) walk.files.set(relative, stamp);
2856
- }
2857
- }
2858
- /**
2859
- * Walks a root: every indexed source file with its stamp and every folder outside the skipped
2860
- * ones. Symlinks are skipped, files and folders alike.
2861
- *
2862
- * @param root - The real root.
2863
- * @returns The files and the folders, root-relative POSIX.
2864
- */
2865
- async function walkRoot(root) {
2866
- const files = /* @__PURE__ */ new Map();
2867
- const folders = [];
2868
- const pending = [""];
2869
- for (let folder = pending.pop(); folder !== void 0; folder = pending.pop()) {
2870
- folders.push(folder);
2871
- await readFolder(root, folder, {
2872
- files,
2873
- pending
2874
- });
2875
- }
2876
- return {
2877
- files,
2878
- folders
2879
- };
2880
- }
2881
- //#endregion
2882
- //#region src/project/session.ts
2883
- /**
2884
- * @file project — an open project: the real root, the catalog of its files, the stamps of the last
2885
- * walk, the tsconfig aliases with the stamps of the config files they came from, the current index
2886
- * and a queue that runs one update at a time. `syncWithDisk` is the batch walk of a watch batch:
2887
- * it walks the root, hashes the files whose stamp moved and every new one, drops the vanished
2888
- * ones, reads the aliases again when a config file moved, and rebuilds the index when bytes or
2889
- * aliases changed. `reindexPath` reads one path now for `changed`.
2890
- */
2891
- /** The dev manifest `moku-game keys` writes. */
2892
- const MANIFEST_FILE = "generated/manifest.json";
2893
- /** The tsconfig whose `paths` are read by default. */
2894
- const TSCONFIG_FILE = "tsconfig.json";
2895
- /**
2896
- * The manifest path to report, when the file exists.
2897
- *
2898
- * @param root - The real root.
2899
- * @param manifest - The root-relative manifest path.
2900
- * @returns The path, or `undefined` when nothing is there.
2901
- */
2902
- async function presentManifest(root, manifest) {
2903
- try {
2904
- if (await resolveInside(root, manifest) !== void 0) return manifest;
2905
- } catch {}
2906
- }
2907
- /**
2908
- * The stamp of a config file, read through the guard.
2909
- *
2910
- * @param root - The real root.
2911
- * @param file - The root-relative path.
2912
- * @returns The stamp, or `undefined` when nothing is there or the path left the root.
2913
- */
2914
- async function configStampOf(root, file) {
2915
- try {
2916
- const real = await resolveInside(root, file);
2917
- return real === void 0 ? void 0 : await stampOf(real);
2918
- } catch {
2919
- return;
2920
- }
2921
- }
2922
- /**
2923
- * The stamps of config files.
2924
- *
2925
- * @param root - The real root.
2926
- * @param files - The root-relative config files.
2927
- * @returns Path to stamp, `undefined` for a file that is not there.
2928
- */
2929
- async function stampConfigs(root, files) {
2930
- const stamps = /* @__PURE__ */ new Map();
2931
- for (const file of files) stamps.set(file, await configStampOf(root, file));
2932
- return stamps;
2933
- }
2934
- /**
2935
- * The config files a read of the tsconfig touched: its sources, or the tsconfig alone when it is
2936
- * missing.
2937
- *
2938
- * @param tsconfig - The root-relative tsconfig path.
2939
- * @param read - The read, `undefined` for a missing tsconfig.
2940
- * @returns The root-relative files to stamp.
2941
- * @example
2942
- * ```ts
2943
- * configsOf("tsconfig.json", undefined); // ["tsconfig.json"]
2944
- * ```
2945
- */
2946
- function configsOf(tsconfig, read) {
2947
- return read?.sources ?? [tsconfig];
2948
- }
2949
- /**
2950
- * The aliases the index follows: a tsconfig without patterns maps nothing, as no tsconfig does.
2951
- *
2952
- * @param read - The read, `undefined` for a missing tsconfig.
2953
- * @returns The read when it holds a pattern, else `undefined`.
2954
- * @example
2955
- * ```ts
2956
- * followedAliases({ file: "tsconfig.json", sources: ["tsconfig.json"], patterns: [] }); // undefined
2957
- * ```
2958
- */
2959
- function followedAliases(read) {
2960
- return read === void 0 || read.patterns.length === 0 ? void 0 : read;
2961
- }
2962
- /**
2963
- * Whether a config file moved, appeared or vanished since its stamp.
2964
- *
2965
- * @param session - The project.
2966
- * @returns True when a stamp of `configStamps` is not the one on disk now.
2967
- */
2968
- async function configsMoved(session) {
2969
- for (const [file, known] of session.configStamps) {
2970
- const now = await configStampOf(session.root, file);
2971
- if (known === void 0 || now === void 0 ? known !== now : !sameStamp(known, now)) return true;
2972
- }
2973
- return false;
2974
- }
2975
- /**
2976
- * Reads the aliases again and stamps the config files the read touched. A tsconfig that stops
2977
- * parsing keeps the aliases it had until it parses again, as a broken source file keeps its keys,
2978
- * and the files of the last good read are stamped again.
2979
- *
2980
- * @param session - The project.
2981
- * @returns True when the aliases resolve differently now.
2982
- */
2983
- async function rereadAliases(session) {
2984
- const before = session.aliases;
2985
- let configs = [...session.configStamps.keys()];
2986
- try {
2987
- const read = await readAliases(session.catalog.typescript, session.root, session.tsconfig);
2988
- session.aliases = followedAliases(read);
2989
- configs = configsOf(session.tsconfig, read);
2990
- } catch {}
2991
- session.configStamps = await stampConfigs(session.root, configs);
2992
- return !sameAliases(before, session.aliases);
2993
- }
2994
- /**
2995
- * Reads one file into the catalog, or drops it when it is gone or cannot be read.
2996
- *
2997
- * @param session - The project.
2998
- * @param file - The root-relative path.
2999
- * @returns True when the catalog changed.
3000
- */
3001
- async function readIntoCatalog(session, file) {
3002
- const bytes = await readInside(session.root, file);
3003
- if (bytes === void 0) {
3004
- session.stamps.delete(file);
3005
- return dropFile(session.catalog, file);
3006
- }
3007
- return putFile(session.catalog, file, bytes);
3008
- }
3009
- /**
3010
- * Rebuilds the index after files changed and says what changed.
3011
- *
3012
- * @param session - The project.
3013
- * @param files - The paths whose bytes changed, appeared or vanished.
3014
- * @returns The change.
3015
- */
3016
- async function rebuild(session, files) {
3017
- const before = session.index;
3018
- const manifest = await presentManifest(session.root, session.manifest);
3019
- session.index = buildIndex(session.catalog, manifest, session.aliases);
3020
- return diffIndexes(before, session.index, files);
3021
- }
3022
- /**
3023
- * Opens a project: checks the root, loads TypeScript, reads the tsconfig aliases and every source
3024
- * file, and builds the index.
3025
- *
3026
- * @param options - The root, the manifest, the tsconfig and the debounce.
3027
- * @returns The open project.
3028
- * @throws {Error} When the root is missing or not a directory, the manifest or the tsconfig path
3029
- * leaves the root, the tsconfig does not parse, or TypeScript is not installed.
3030
- */
3031
- async function openSession(options) {
3032
- const root = await openRoot(options.root);
3033
- const typescript = await loadTypeScript();
3034
- const manifest = toPosix(options.manifest ?? MANIFEST_FILE);
3035
- const tsconfig = toPosix(options.tsconfig ?? TSCONFIG_FILE);
3036
- await resolveInside(root, manifest);
3037
- await resolveInside(root, tsconfig);
3038
- const read = await readAliases(typescript, root, tsconfig);
3039
- const aliases = followedAliases(read);
3040
- const walk = await walkRoot(root);
3041
- const catalog = createCatalog(typescript);
3042
- const session = {
3043
- root,
3044
- manifest,
3045
- tsconfig,
3046
- aliases,
3047
- configStamps: await stampConfigs(root, configsOf(tsconfig, read)),
3048
- catalog,
3049
- stamps: walk.files,
3050
- folders: walk.folders,
3051
- index: buildIndex(catalog, void 0, aliases),
3052
- shapes: /* @__PURE__ */ new WeakMap(),
3053
- queue: Promise.resolve()
3054
- };
3055
- for (const file of [...walk.files.keys()].toSorted()) await readIntoCatalog(session, file);
3056
- session.index = buildIndex(catalog, await presentManifest(root, manifest), aliases);
3057
- return session;
3058
- }
3059
- /**
3060
- * Runs an update after the ones before it, so two never interleave.
3061
- *
3062
- * @param session - The project.
3063
- * @param task - The update.
3064
- * @returns What the update returns.
3065
- */
3066
- function serialize(session, task) {
3067
- const result = session.queue.then(task);
3068
- session.queue = result.catch(() => {});
3069
- return result;
3070
- }
3071
- /**
3072
- * The batch walk: walks the root, hashes each file whose stamp moved and each new one, drops the
3073
- * vanished ones, reads the aliases again when a config file moved, appeared or vanished, and
3074
- * rebuilds the index when bytes or aliases changed. A save with the same bytes, or a tsconfig
3075
- * edit that leaves the aliases as they were, changes nothing. New aliases list the tsconfig
3076
- * among the changed files.
3077
- *
3078
- * @param session - The project.
3079
- * @returns The change, or `undefined` when the index stayed the same.
3080
- */
3081
- async function syncWithDisk(session) {
3082
- const walk = await walkRoot(session.root);
3083
- const files = [];
3084
- for (const file of session.catalog.records.keys()) if (!walk.files.has(file) && dropFile(session.catalog, file)) files.push(file);
3085
- const previous = session.stamps;
3086
- session.stamps = walk.files;
3087
- session.folders = walk.folders;
3088
- for (const [file, stamp] of walk.files) {
3089
- const known = previous.get(file);
3090
- if (known !== void 0 && sameStamp(known, stamp)) continue;
3091
- if (await readIntoCatalog(session, file)) files.push(file);
3092
- }
3093
- if (await configsMoved(session) && await rereadAliases(session)) files.push(session.tsconfig);
3094
- const manifest = await presentManifest(session.root, session.manifest);
3095
- if (files.length === 0 && manifest === session.index.manifest) return void 0;
3096
- return rebuild(session, files);
3097
- }
3098
- /**
3099
- * Reads one root-relative path now: a changed or new source file is parsed, a deleted one is
3100
- * dropped, the tsconfig or a file it extends reads the aliases again, and anything else changes
3101
- * nothing.
3102
- *
3103
- * @param session - The project.
3104
- * @param file - The root-relative path.
3105
- * @returns The index after the read.
3106
- * @throws {Error} When the path leaves the root.
3107
- */
3108
- async function reindexPath(session, file) {
3109
- const relative = toPosix(file);
3110
- const real = await resolveInside(session.root, relative);
3111
- if (path.isAbsolute(relative)) return session.index;
3112
- if (session.configStamps.has(relative)) {
3113
- if (await rereadAliases(session)) await rebuild(session, [session.tsconfig]);
3114
- return session.index;
3115
- }
3116
- if (real !== void 0 && real !== path.join(session.root, relative) || !isIndexedFile(relative)) return session.index;
3117
- const stamp = real === void 0 ? void 0 : await stampOf(real);
3118
- if (stamp !== void 0) session.stamps.set(relative, stamp);
3119
- if (!await readIntoCatalog(session, relative)) return session.index;
3120
- await rebuild(session, [relative]);
3121
- return session.index;
3122
- }
3123
- //#endregion
3124
369
  //#region src/project/cli.ts
3125
370
  /**
3126
371
  * @file project — the command line `moku-game-index`. It opens the project of `--root` and runs one
@@ -3336,210 +581,18 @@ async function runCommand(session, options, ui) {
3336
581
  async function runCli(argv, ui = createBrandConsole()) {
3337
582
  try {
3338
583
  const options = parseArgv(argv);
3339
- return await runCommand(await openSession({
584
+ const projectOptions = {
3340
585
  root: options.root,
3341
586
  ...options.manifest === void 0 ? {} : { manifest: options.manifest },
3342
587
  ...options.tsconfig === void 0 ? {} : { tsconfig: options.tsconfig }
3343
- }), options, ui);
588
+ };
589
+ return await runCommand(await openSession(projectOptions), options, ui);
3344
590
  } catch (error) {
3345
591
  ui.error(messageOf(error));
3346
592
  return 1;
3347
593
  }
3348
594
  }
3349
595
  //#endregion
3350
- //#region src/project/watch.ts
3351
- /**
3352
- * @file project — the watcher of an open project. A platform event is only a trigger: Bun on macOS
3353
- * reports every edit as `rename`, drops the destination of a move and names the `.tmp` of an
3354
- * atomic save instead of the file. So events only poke a batcher; when the quiet period ends, the
3355
- * batch walks the root (`syncWithDisk`) and finds what really changed. The watcher can also miss a
3356
- * write made as it starts, so the handle walks once when watching starts and then every two
3357
- * seconds as a backstop. The root is watched recursively; where the platform cannot, every folder
3358
- * of the last walk gets its own watcher, re-armed after each walk. Every caller of `watch` shares
3359
- * the one watcher of the handle.
3360
- */
3361
- /** The longest a burst of events holds a batch back, at the least. */
3362
- const LONGEST_WAIT_MS = 1e3;
3363
- /** The longest wait as a multiple of the quiet period, when that is longer. */
3364
- const LONGEST_WAIT_FACTOR = 10;
3365
- /**
3366
- * How often a watching handle walks the root with no event: the platform watcher can miss a write
3367
- * made as it starts, and drop events under load. A walk that finds the same bytes calls nobody.
3368
- */
3369
- const BACKSTOP_MS = 2e3;
3370
- /** The error `watch` throws on a closed handle. */
3371
- const CLOSED = "[game] The project is closed.\n Open it again with openProject().";
3372
- /**
3373
- * Watches every folder of the last walk with its own watcher, for a platform without recursive
3374
- * watching.
3375
- *
3376
- * @param root - The real root.
3377
- * @param listener - Gets every event.
3378
- * @param onError - Called when a folder watcher errors.
3379
- * @param watchFunction - `fs.watch`.
3380
- * @returns The tree watcher.
3381
- */
3382
- function watchFolders(root, listener, onError, watchFunction) {
3383
- const watchers = /* @__PURE__ */ new Map();
3384
- const arm = (folder) => {
3385
- try {
3386
- const handle = watchFunction(path.join(root, folder), { recursive: false }, listener);
3387
- handle.on("error", () => {
3388
- handle.close();
3389
- if (watchers.get(folder) === handle) watchers.delete(folder);
3390
- onError();
3391
- });
3392
- watchers.set(folder, handle);
3393
- } catch {}
3394
- };
3395
- return {
3396
- rearm: (folders) => {
3397
- for (const [folder, handle] of watchers) {
3398
- if (folders.includes(folder)) continue;
3399
- handle.close();
3400
- watchers.delete(folder);
3401
- }
3402
- for (const folder of folders) if (!watchers.has(folder)) arm(folder);
3403
- },
3404
- close: () => {
3405
- for (const handle of watchers.values()) handle.close();
3406
- watchers.clear();
3407
- }
3408
- };
3409
- }
3410
- /**
3411
- * Watches a root: recursively when the platform can, else one watcher per folder. Events under
3412
- * the skipped folders are ignored.
3413
- *
3414
- * @param root - The real root.
3415
- * @param onEvent - Called on every event that may matter.
3416
- * @param watchFunction - `fs.watch` by default.
3417
- * @returns The tree watcher.
3418
- */
3419
- function watchTree(root, onEvent, watchFunction = watch) {
3420
- const listener = (_event, file) => {
3421
- if (file === null || !isSkippedPath(`${file}/`)) onEvent();
3422
- };
3423
- try {
3424
- const handle = watchFunction(root, { recursive: true }, listener);
3425
- handle.on("error", () => onEvent());
3426
- return {
3427
- rearm: () => {},
3428
- close: () => handle.close()
3429
- };
3430
- } catch {
3431
- return watchFolders(root, listener, onEvent, watchFunction);
3432
- }
3433
- }
3434
- /**
3435
- * Creates the debounce of a watcher: a batch runs once the events stop for `quietMs`, or after the
3436
- * longest wait during an endless burst. One batch runs at a time; events during a batch queue one
3437
- * more.
3438
- *
3439
- * @param quietMs - The quiet period that closes a batch.
3440
- * @param run - The batch.
3441
- * @returns The batcher.
3442
- */
3443
- function createBatcher(quietMs, run) {
3444
- const longestMs = Math.max(LONGEST_WAIT_MS, quietMs * LONGEST_WAIT_FACTOR);
3445
- let quiet;
3446
- let longest;
3447
- let running = false;
3448
- let pending = false;
3449
- let cancelled = false;
3450
- const clear = () => {
3451
- clearTimeout(quiet);
3452
- clearTimeout(longest);
3453
- quiet = void 0;
3454
- longest = void 0;
3455
- };
3456
- const fire = () => {
3457
- clear();
3458
- if (running) {
3459
- pending = true;
3460
- return;
3461
- }
3462
- const settle = () => {
3463
- running = false;
3464
- if (pending && !cancelled) poke();
3465
- pending = false;
3466
- };
3467
- running = true;
3468
- run().then(settle, settle);
3469
- };
3470
- const poke = () => {
3471
- if (cancelled) return;
3472
- clearTimeout(quiet);
3473
- quiet = setTimeout(fire, quietMs);
3474
- longest ??= setTimeout(fire, longestMs);
3475
- };
3476
- return {
3477
- poke,
3478
- cancel: () => {
3479
- cancelled = true;
3480
- clear();
3481
- }
3482
- };
3483
- }
3484
- /**
3485
- * Creates the callers' hub of one handle: the watcher starts with the first caller and stops with
3486
- * the last; each batch that changed the index calls every caller once.
3487
- *
3488
- * @param session - The open project.
3489
- * @param debounceMs - The quiet period.
3490
- * @param watchFunction - `fs.watch` by default.
3491
- * @param backstopMs - The period of the walk with no event, two seconds by default.
3492
- * @returns The hub.
3493
- */
3494
- function createHub(session, debounceMs, watchFunction = watch, backstopMs = BACKSTOP_MS) {
3495
- const listeners = /* @__PURE__ */ new Set();
3496
- let tree;
3497
- let batcher;
3498
- let backstop;
3499
- let closed = false;
3500
- const batch = async () => {
3501
- const change = await serialize(session, () => syncWithDisk(session));
3502
- tree?.rearm(session.folders);
3503
- if (change === void 0 || closed) return;
3504
- for (const { listener } of [...listeners]) try {
3505
- listener(session.index, change);
3506
- } catch {}
3507
- };
3508
- const stop = () => {
3509
- clearInterval(backstop);
3510
- tree?.close();
3511
- batcher?.cancel();
3512
- backstop = void 0;
3513
- tree = void 0;
3514
- batcher = void 0;
3515
- };
3516
- const start = () => {
3517
- const started = createBatcher(debounceMs, batch);
3518
- batcher = started;
3519
- tree = watchTree(session.root, () => started.poke(), watchFunction);
3520
- tree.rearm(session.folders);
3521
- started.poke();
3522
- backstop = setInterval(() => started.poke(), backstopMs);
3523
- };
3524
- return {
3525
- add: (listener) => {
3526
- if (closed) throw new Error(CLOSED);
3527
- const entry = { listener };
3528
- listeners.add(entry);
3529
- if (tree === void 0) start();
3530
- return () => {
3531
- listeners.delete(entry);
3532
- if (listeners.size === 0) stop();
3533
- };
3534
- },
3535
- close: () => {
3536
- closed = true;
3537
- listeners.clear();
3538
- stop();
3539
- }
3540
- };
3541
- }
3542
- //#endregion
3543
596
  //#region src/project/open.ts
3544
597
  /**
3545
598
  * @file project — `openProject`, the one function of the project index a caller starts from. It