@moku-labs/game 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/lint.d.mts +27 -6
- package/dist/lint.mjs +180 -52
- package/dist/project.d.mts +23 -12
- package/dist/project.mjs +1222 -853
- package/llms.txt +5 -3
- package/package.json +1 -1
package/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
|
|
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/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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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 {
|
|
311
|
-
|
|
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
|
package/dist/project.d.mts
CHANGED
|
@@ -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
|
|
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;
|
|
48
|
-
|
|
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
|
|
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
|
|
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
|
|
146
|
-
*
|
|
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:
|
|
167
|
+
* // ["features/ui/kit.tsx:844", "features/settings/settings.tsx:290"]
|
|
157
168
|
* ```
|
|
158
169
|
*/
|
|
159
170
|
find(key: string): Promise<Found[]>;
|