@moku-labs/game 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -199,7 +199,7 @@ createApp({ plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin] });
199
199
 
200
200
  ### Lint for games
201
201
 
202
- `@moku-labs/game/lint` is an oxlint JS plugin, `moku-game`. It checks the engine rules in a game: lazy Pixi and Yoga, no native package in the logic, the editor only in dev files, no module-scope state, determinism and pure rules.
202
+ `@moku-labs/game/lint` is an oxlint JS plugin, `moku-game`. It checks the engine rules in a game: lazy Pixi and Yoga, no native package in the logic, the editor only in dev files, no module-scope state, determinism, pure rules and JSX keys the project index can follow.
203
203
 
204
204
  ```json
205
205
  // .oxlintrc.json
@@ -211,7 +211,8 @@ createApp({ plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin] });
211
211
  "moku-game/dev-imports": "error",
212
212
  "moku-game/no-module-state": "error",
213
213
  "moku-game/determinism": "error",
214
- "moku-game/rules-siblings": "error"
214
+ "moku-game/rules-siblings": "error",
215
+ "moku-game/static-keys": "error"
215
216
  }
216
217
  }
217
218
  ```
package/dist/index.mjs CHANGED
@@ -30350,6 +30350,7 @@ function createJsxApi(ctx, modules) {
30350
30350
  state.components.set(definition.name, definition);
30351
30351
  },
30352
30352
  refreshAll: () => {
30353
+ for (const element of state.elements.values()) modules.layout.markMeasured(element);
30353
30354
  for (const root of state.roots.values()) {
30354
30355
  root.dirty = true;
30355
30356
  root.needsSolve = true;
@@ -31715,6 +31716,7 @@ function createLayoutApi(ctx) {
31715
31716
  markMeasured(state, element);
31716
31717
  },
31717
31718
  remeasure: (element) => remeasureShown(state, element, source),
31719
+ markMeasured: (element) => markMeasured(state, element),
31718
31720
  place: (parent, children) => placeChildren(state, parent, children),
31719
31721
  free: (element) => freeNode(state, element),
31720
31722
  solve: (rootElement, size, lookup) => solveRoot(state, rootElement, size, lookup),
package/dist/lint.d.mts CHANGED
@@ -6,8 +6,8 @@ type GameLintOptions = {
6
6
  };
7
7
  /** The part of an ESTree node the rules read. oxlint and ESLint hand over full nodes. */
8
8
  type GameLintNode = {
9
- readonly type: string;
10
- readonly name?: string;
9
+ readonly type: string; /** A string on an `Identifier`; the `JSXIdentifier` node on a `JSXAttribute`. */
10
+ readonly name?: string | GameLintNode;
11
11
  readonly value?: unknown;
12
12
  readonly kind?: string;
13
13
  readonly computed?: boolean;
@@ -22,12 +22,33 @@ type GameLintNode = {
22
22
  readonly arguments?: readonly GameLintNode[];
23
23
  readonly object?: GameLintNode;
24
24
  readonly property?: GameLintNode;
25
+ readonly expression?: GameLintNode;
26
+ readonly expressions?: readonly GameLintNode[];
25
27
  };
26
- /** The part of the rule context the rules read: where the file is, the options, the report. */
28
+ /** One declaration of a name, as the scope manager of oxlint and ESLint hands it over. */
29
+ type GameLintDefinition = {
30
+ /** `Variable`, `Parameter`, `ImportBinding`, `FunctionName` and the other ESLint kinds. */readonly type: string; /** The declarator of a `Variable`: its `init` is the value. */
31
+ readonly node: GameLintNode; /** The declaration of a `Variable`: its `kind` is `const`, `let` or `var`. */
32
+ readonly parent?: GameLintNode | null;
33
+ };
34
+ /** The part of a scope the rules read: the names it declares and the scope around it. */
35
+ type GameLintScope = {
36
+ readonly set: ReadonlyMap<string, {
37
+ readonly defs: readonly GameLintDefinition[];
38
+ }>;
39
+ readonly upper?: GameLintScope | null;
40
+ };
41
+ /**
42
+ * The part of the rule context the rules read: where the file is, the options, the scopes of the
43
+ * file and the report.
44
+ */
27
45
  type GameLintContext = {
28
46
  readonly cwd: string;
29
47
  readonly filename: string;
30
48
  readonly options: readonly unknown[];
49
+ readonly sourceCode: {
50
+ getScope(node: GameLintNode): GameLintScope;
51
+ };
31
52
  report(descriptor: {
32
53
  node: GameLintNode;
33
54
  message: string;
@@ -46,7 +67,7 @@ type GameLintRule = {
46
67
  };
47
68
  create(context: GameLintContext): GameLintListeners;
48
69
  };
49
- /** The plugin object: `moku-game` and its six rules. */
70
+ /** The plugin object: `moku-game` and its seven rules. */
50
71
  type GameLintPlugin = {
51
72
  readonly meta: {
52
73
  readonly name: "moku-game";
@@ -54,7 +75,7 @@ type GameLintPlugin = {
54
75
  readonly rules: Readonly<Record<GameLintRuleName, GameLintRule>>;
55
76
  };
56
77
  /** The rule names, without the `moku-game/` prefix. */
57
- type GameLintRuleName = "lazy-imports" | "native-imports" | "dev-imports" | "no-module-state" | "determinism" | "rules-siblings";
78
+ type GameLintRuleName = "lazy-imports" | "native-imports" | "dev-imports" | "no-module-state" | "determinism" | "rules-siblings" | "static-keys";
58
79
  /**
59
80
  * Turn a glob into an anchored regular expression: `**`, `*`, `?` and one level of `{a,b}`.
60
81
  *
@@ -76,4 +97,4 @@ declare function globToRegExp(glob: string): RegExp;
76
97
  */
77
98
  declare const plugin: GameLintPlugin;
78
99
  //#endregion
79
- export { GameLintContext, GameLintListeners, GameLintNode, GameLintOptions, GameLintPlugin, GameLintRule, GameLintRuleName, plugin as default, globToRegExp };
100
+ export { GameLintContext, GameLintDefinition, GameLintListeners, GameLintNode, GameLintOptions, GameLintPlugin, GameLintRule, GameLintRuleName, GameLintScope, plugin as default, globToRegExp };
package/dist/lint.mjs CHANGED
@@ -62,6 +62,18 @@ const CLOCK_READS = [
62
62
  /** The collections a module may not hold at module scope (L5). */
63
63
  const COLLECTION = /^(?:Map|Set|WeakMap|WeakSet)$/;
64
64
  /**
65
+ * A key-carrying prop the project index fills a key from: `id`, or a name that ends in `Key`. The
66
+ * index holds its own copy; a behavioural unit test keeps the two in step: the rule accepts
67
+ * `props.<name>` exactly when the index turns it into a `{<name>}` hole.
68
+ */
69
+ const KEY_PROP = /^(?:id|\w+Key)$/;
70
+ /** The wrappers a key is read through, as the index reads them: casts, parentheses, `?.` chains. */
71
+ const KEY_WRAPPER = /^(?:TSAsExpression|TSSatisfiesExpression|TSNonNullExpression|TSTypeAssertion|ParenthesizedExpression|ChainExpression)$/;
72
+ /** How many `const` initializers a key name is followed through. */
73
+ const KEY_HOPS = 3;
74
+ /** What static-keys says about a key the project index cannot follow. */
75
+ const STATIC_KEYS_MESSAGE = "Keys: a JSX key must be a literal, a template, or props.id / props.<name>Key. Pass the key in as a prop: <X id=\"…\">.";
76
+ /**
65
77
  * Turn a glob into an anchored regular expression: `**`, `*`, `?` and one level of `{a,b}`.
66
78
  *
67
79
  * @param glob - A POSIX glob relative to the lint root.
@@ -185,6 +197,18 @@ function importRule(description, defaults, checks, lazy = false) {
185
197
  };
186
198
  }
187
199
  /**
200
+ * The name a node carries: the name of an `Identifier`, the name of the `JSXIdentifier` of a
201
+ * `JSXAttribute`.
202
+ *
203
+ * @param node - Any node.
204
+ * @returns The name, or undefined for a node without one.
205
+ */
206
+ function nameOf(node) {
207
+ const name = node?.name;
208
+ if (typeof name === "string") return name;
209
+ return typeof name?.name === "string" ? name.name : void 0;
210
+ }
211
+ /**
188
212
  * The name of a member's property: `a.b` and `a["b"]`.
189
213
  *
190
214
  * @param node - A MemberExpression.
@@ -193,7 +217,7 @@ function importRule(description, defaults, checks, lazy = false) {
193
217
  function propertyName(node) {
194
218
  const key = node.property;
195
219
  if (key === void 0) return void 0;
196
- if (!node.computed) return key.name;
220
+ if (!node.computed) return nameOf(key);
197
221
  return typeof key.value === "string" ? key.value : void 0;
198
222
  }
199
223
  /**
@@ -214,7 +238,7 @@ function reportModuleState(context, statement) {
214
238
  }
215
239
  for (const declarator of declaration.declarations ?? []) {
216
240
  const init = declarator.init;
217
- if (init?.type === "NewExpression" && COLLECTION.test(init.callee?.name ?? "")) context.report({
241
+ if (init?.type === "NewExpression" && COLLECTION.test(nameOf(init.callee) ?? "")) context.report({
218
242
  node: init,
219
243
  message: "L5: no module-scope collections. Create them inside a function."
220
244
  });
@@ -227,9 +251,9 @@ function reportModuleState(context, statement) {
227
251
  * @returns True for a timer.
228
252
  */
229
253
  function isTimer(callee) {
230
- if (callee?.type === "Identifier") return TIMER.test(callee.name ?? "");
254
+ if (callee?.type === "Identifier") return TIMER.test(nameOf(callee) ?? "");
231
255
  if (callee?.type !== "MemberExpression") return false;
232
- return GLOBAL_OBJECT.test(callee.object?.name ?? "") && TIMER.test(propertyName(callee) ?? "");
256
+ return GLOBAL_OBJECT.test(nameOf(callee.object) ?? "") && TIMER.test(propertyName(callee) ?? "");
233
257
  }
234
258
  /** L2: static value imports of the packages the engine loads lazily. */
235
259
  const lazyImports = importRule("No static import of pixi.js or yoga-layout; import() and import type pass.", {
@@ -276,6 +300,145 @@ const rulesSiblings = importRule("A file under rules/ imports only its siblings.
276
300
  pattern: /^(?!\.\/)/,
277
301
  message: "L4: a rule imports only its siblings in rules/: (state, input, tables) => result."
278
302
  }]);
303
+ /** L5: no module-scope `let` or `var` and no module-scope collection. */
304
+ const noModuleState = {
305
+ meta: {
306
+ type: "problem",
307
+ docs: { description: "No module-scope let or var and no module-scope Map, Set, WeakMap, WeakSet." },
308
+ schema: SCHEMA
309
+ },
310
+ /**
311
+ * Visit the file when the rule's globs match it.
312
+ *
313
+ * @param context - The rule context.
314
+ * @returns The node visitors, none for a file out of scope.
315
+ */
316
+ create(context) {
317
+ if (!inScope(context, {
318
+ files: ["**"],
319
+ ignores: TESTS
320
+ })) return {};
321
+ return { Program: (program) => {
322
+ for (const statement of program.body ?? []) reportModuleState(context, statement);
323
+ } };
324
+ }
325
+ };
326
+ /** L3: no device clock, no timers, no unseeded randomness in the logic. */
327
+ const determinism = {
328
+ meta: {
329
+ type: "problem",
330
+ docs: { description: "No Math.random, Date.now, performance.now, new Date(), setTimeout or setInterval in the logic." },
331
+ schema: SCHEMA
332
+ },
333
+ /**
334
+ * Visit the file when the rule's globs match it.
335
+ *
336
+ * @param context - The rule context.
337
+ * @returns The node visitors, none for a file out of scope.
338
+ */
339
+ create(context) {
340
+ if (!inScope(context, {
341
+ files: LOGIC,
342
+ ignores: [...DEV_MODULES, ...TESTS]
343
+ })) return {};
344
+ return {
345
+ MemberExpression: (node) => {
346
+ const property = propertyName(node);
347
+ const read = CLOCK_READS.find((item) => item.object === nameOf(node.object) && item.property === property);
348
+ if (read !== void 0) context.report({
349
+ node,
350
+ message: read.message
351
+ });
352
+ },
353
+ NewExpression: (node) => {
354
+ if (nameOf(node.callee) === "Date" && (node.arguments ?? []).length === 0) context.report({
355
+ node,
356
+ message: "L3: use `now` from the node context."
357
+ });
358
+ },
359
+ CallExpression: (node) => {
360
+ if (isTimer(node.callee)) context.report({
361
+ node,
362
+ message: "L3: store the moment in player, await fx(schedule(moment))."
363
+ });
364
+ }
365
+ };
366
+ }
367
+ };
368
+ /**
369
+ * Whether a value an attribute holds is a node: the `JSXExpressionContainer` of `key={…}`.
370
+ *
371
+ * @param value - The `value` of a node.
372
+ * @returns True for a node.
373
+ */
374
+ function isNode(value) {
375
+ return typeof value === "object" && value !== null && "type" in value;
376
+ }
377
+ /**
378
+ * A key expression without the wrappers around it: `props.id as string` and `props?.id` read
379
+ * `props.id`.
380
+ *
381
+ * @param node - The key expression.
382
+ * @returns The expression inside.
383
+ */
384
+ function unwrapKey(node) {
385
+ let inner = node;
386
+ while (inner !== null && inner !== void 0 && KEY_WRAPPER.test(inner.type)) inner = inner.expression;
387
+ return inner;
388
+ }
389
+ /**
390
+ * The declaration of a name where it is used, read scope by scope outwards.
391
+ *
392
+ * @param context - The rule context.
393
+ * @param identifier - The name where it is used.
394
+ * @returns Its first declaration, or undefined for a name the file does not declare.
395
+ */
396
+ function definitionOf(context, identifier) {
397
+ const name = nameOf(identifier) ?? "";
398
+ for (let scope = context.sourceCode.getScope(identifier); scope; scope = scope.upper) {
399
+ const variable = scope.set.get(name);
400
+ if (variable !== void 0) return variable.defs[0];
401
+ }
402
+ }
403
+ /** The declarations a key name is left to the index for: `--check` lists what it cannot read. */
404
+ const OUT_OF_REACH = /^(?:Parameter|ImportBinding)$/;
405
+ /**
406
+ * Whether a key name is one the project index reads: a `const` whose value is a static key, or a
407
+ * name out of reach here (a parameter, an import, a global), which `--check` lists when the index
408
+ * cannot read it.
409
+ *
410
+ * @param context - The rule context.
411
+ * @param identifier - The name.
412
+ * @param hops - How many `const` initializers were followed to reach it.
413
+ * @returns False for a `let`, a `var`, a `const` whose value the index cannot follow, and a
414
+ * function, a class or a catch parameter.
415
+ */
416
+ function isStaticName(context, identifier, hops) {
417
+ const definition = definitionOf(context, identifier);
418
+ if (definition === void 0 || OUT_OF_REACH.test(definition.type) || hops >= KEY_HOPS) return true;
419
+ if (definition.type !== "Variable") return false;
420
+ return definition.parent?.kind === "const" && isStaticKey(context, definition.node.init, hops + 1);
421
+ }
422
+ /**
423
+ * Whether a key expression is one the project index reads: a literal, a template of static parts,
424
+ * `props.id` or `props.<name>Key`, a call, or a name that holds one.
425
+ *
426
+ * @param context - The rule context.
427
+ * @param node - The key expression.
428
+ * @param hops - How many `const` initializers were followed to reach it.
429
+ * @returns False for an element access, `??`, `||`, `&&`, `?:` and any other member.
430
+ */
431
+ function isStaticKey(context, node, hops) {
432
+ const expression = unwrapKey(node);
433
+ switch (expression?.type) {
434
+ case "Literal": return typeof expression.value === "string" || typeof expression.value === "number";
435
+ case "TemplateLiteral": return (expression.expressions ?? []).every((part) => isStaticKey(context, part, hops));
436
+ case "MemberExpression": return !expression.computed && expression.object?.type === "Identifier" && KEY_PROP.test(propertyName(expression) ?? "");
437
+ case "Identifier": return isStaticName(context, expression, hops);
438
+ case "CallExpression": return true;
439
+ default: return false;
440
+ }
441
+ }
279
442
  /**
280
443
  * The `moku-game` oxlint plugin. A game loads it by package name and turns its rules on.
281
444
  *
@@ -290,10 +453,13 @@ const plugin = {
290
453
  "lazy-imports": lazyImports,
291
454
  "native-imports": nativeImports,
292
455
  "dev-imports": developmentImports,
293
- "no-module-state": {
456
+ "no-module-state": noModuleState,
457
+ determinism,
458
+ "rules-siblings": rulesSiblings,
459
+ "static-keys": {
294
460
  meta: {
295
461
  type: "problem",
296
- docs: { description: "No module-scope let or var and no module-scope Map, Set, WeakMap, WeakSet." },
462
+ docs: { description: "A JSX key is a literal, a template, props.id or props.<name>Key, or a call." },
297
463
  schema: SCHEMA
298
464
  },
299
465
  /**
@@ -304,56 +470,18 @@ const plugin = {
304
470
  */
305
471
  create(context) {
306
472
  if (!inScope(context, {
307
- files: ["**"],
473
+ files: ["**/*.tsx"],
308
474
  ignores: TESTS
309
475
  })) return {};
310
- return { Program: (program) => {
311
- for (const statement of program.body ?? []) reportModuleState(context, statement);
476
+ return { JSXAttribute: (node) => {
477
+ const container = isNode(node.value) ? node.value : void 0;
478
+ if (nameOf(node) === "key" && container?.type === "JSXExpressionContainer" && !isStaticKey(context, container.expression, 0)) context.report({
479
+ node,
480
+ message: STATIC_KEYS_MESSAGE
481
+ });
312
482
  } };
313
483
  }
314
- },
315
- determinism: {
316
- meta: {
317
- type: "problem",
318
- docs: { description: "No Math.random, Date.now, performance.now, new Date(), setTimeout or setInterval in the logic." },
319
- schema: SCHEMA
320
- },
321
- /**
322
- * Visit the file when the rule's globs match it.
323
- *
324
- * @param context - The rule context.
325
- * @returns The node visitors, none for a file out of scope.
326
- */
327
- create(context) {
328
- if (!inScope(context, {
329
- files: LOGIC,
330
- ignores: [...DEV_MODULES, ...TESTS]
331
- })) return {};
332
- return {
333
- MemberExpression: (node) => {
334
- const property = propertyName(node);
335
- const read = CLOCK_READS.find((item) => item.object === node.object?.name && item.property === property);
336
- if (read !== void 0) context.report({
337
- node,
338
- message: read.message
339
- });
340
- },
341
- NewExpression: (node) => {
342
- if (node.callee?.name === "Date" && (node.arguments ?? []).length === 0) context.report({
343
- node,
344
- message: "L3: use `now` from the node context."
345
- });
346
- },
347
- CallExpression: (node) => {
348
- if (isTimer(node.callee)) context.report({
349
- node,
350
- message: "L3: store the moment in player, await fx(schedule(moment))."
351
- });
352
- }
353
- };
354
- }
355
- },
356
- "rules-siblings": rulesSiblings
484
+ }
357
485
  }
358
486
  };
359
487
  //#endregion
@@ -3,7 +3,7 @@
3
3
  * @file project — the public types of the project index: the options of `openProject`, the index
4
4
  * it holds, the anchors of a key, the fresh lines `find` answers and the handle itself. The index
5
5
  * maps every engine id of a game (flows, nodes, features, scenes, projections, emitters, text
6
- * styles, styles and JSX keys) to anchors: a path plus a binding, a key or a component. It never
6
+ * styles, styles, components and JSX keys) to anchors: a path plus a binding, a key or a component. It never
7
7
  * holds a line number; `find` reads the line from the file as it is on disk at the call. No type
8
8
  * here names a `typescript` type, so the shipped declarations need no TypeScript.
9
9
  */
@@ -24,7 +24,8 @@ type ProjectOptions = {
24
24
  };
25
25
  /**
26
26
  * How a JSX key is written: a string literal `key="hudRow"`, a template `` key={`${id}Picture`} ``,
27
- * any other expression `key={id}`, or a literal `id=` prop on a component (`id="settingsBoard"`).
27
+ * any other expression `key={id}`, or a literal key-carrying prop on a component: `id=`, or a prop
28
+ * whose name ends in `Key` (`id="settingsBoard"`, `amountKey="giftReward"`).
28
29
  *
29
30
  * @example
30
31
  * ```ts
@@ -44,11 +45,20 @@ type JsxKind = "literal" | "template" | "idProp" | "ident";
44
45
  */
45
46
  type Anchor = {
46
47
  /** The file, root-relative POSIX. */path: string; /** The const, function or export the definition is bound to. */
47
- binding?: string; /** The property or attribute key: a text style key, the `name` of a projection, a JSX key. */
48
- key?: string; /** JSX: the component an `id=` prop sits on, or the component an `{id}` pattern is written in. */
48
+ binding?: string;
49
+ /**
50
+ * The property or attribute key: a text style key, the `name` of a projection, a JSX key, or
51
+ * the property a style function builds a style under (`icon` of `roundStylesOf`).
52
+ */
53
+ key?: string;
54
+ /**
55
+ * JSX: the component a key-carrying prop sits on, or the component a `{id}` or `{amountKey}`
56
+ * pattern is written in.
57
+ */
49
58
  component?: string; /** JSX only: how the key is written. */
50
59
  kind?: JsxKind; /** JSX templates and identifiers: the literal head of the pattern, `card` for `card*Picture`. */
51
- stem?: string;
60
+ stem?: string; /** JSX idProp: the prop the value sits on, `id` or `amountKey`. */
61
+ prop?: string;
52
62
  };
53
63
  /**
54
64
  * The index of one game at one moment: every key with its anchors, every indexed file with its
@@ -65,8 +75,8 @@ type ProjectIndex = {
65
75
  revision: string; /** The asset manifest, root-relative, when that file exists. */
66
76
  manifest?: string;
67
77
  /**
68
- * Every key: where it is defined (`def`), where its binding is used (`uses`, nodes and styles,
69
- * one level), and `conflict` when a key other than `jsx:` is defined twice.
78
+ * Every key: where it is defined (`def`), where its binding is used (`uses`, nodes, styles and
79
+ * components, one level), and `conflict` when a key other than `jsx:` is defined twice.
70
80
  */
71
81
  symbols: Record<string, {
72
82
  def: Anchor[];
@@ -108,7 +118,8 @@ type ProjectChange = {
108
118
  /**
109
119
  * One answer of `find`: the anchor with its line read from the file now. Lines and columns are
110
120
  * 1-based; the end column is exclusive. A binding answers its declaration statement, a text style
111
- * key its property, a JSX key the line of its attribute with the whole element as the range.
121
+ * key its property, a style built in a function each of its style calls, a JSX key the line of its
122
+ * attribute with the whole element as the range.
112
123
  *
113
124
  * @example
114
125
  * ```ts
@@ -142,9 +153,9 @@ type ProjectApi = {
142
153
  * Answers where a key is defined, with lines read from the file on disk now. A file whose bytes
143
154
  * changed since the index was built is parsed again for the answer; the index itself stays as
144
155
  * it is until the next watch batch. A JSX key the game reports at run time also finds the
145
- * patterns it was built from: exact keys first, then `{id}` patterns filled with a literal
146
- * `id=` prop (both the pattern and the prop come back), then `*` patterns. An unknown key
147
- * answers `[]`.
156
+ * patterns it was built from: exact keys first, then `{id}` and `{amountKey}` patterns filled
157
+ * with a literal prop of the same name on their component (both the pattern and the prop come
158
+ * back), then `*` patterns. An unknown key answers `[]`.
148
159
  *
149
160
  * @param key - An engine id: `node:board/merge`, `flow:board`, `textStyle:ui.title`, `jsx:card2Picture`.
150
161
  * @returns One entry per place, in the order above; `broken` when the file does not parse now.
@@ -153,7 +164,7 @@ type ProjectApi = {
153
164
  * // The editor jumps to the node the flow view clicked, and to the X of the settings board.
154
165
  * (await project.find("node:board/merge"))[0]?.line; // 17, in "nodes/merge.ts"
155
166
  * (await project.find("jsx:settingsBoardClose")).map(found => `${found.path}:${found.line}`);
156
- * // ["features/ui/kit.tsx:844", "features/settings/settings.tsx:301"]
167
+ * // ["features/ui/kit.tsx:844", "features/settings/settings.tsx:290"]
157
168
  * ```
158
169
  */
159
170
  find(key: string): Promise<Found[]>;