@moku-labs/game 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/bin/moku-game-index.mjs +4 -0
- package/dist/lint.d.mts +27 -6
- package/dist/lint.mjs +180 -52
- package/dist/project.d.mts +278 -0
- package/dist/project.mjs +3113 -0
- package/llms.txt +25 -2
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -194,11 +194,12 @@ createApp({ plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin] });
|
|
|
194
194
|
| `@moku-labs/game/control` | dev builds | Drive a running game |
|
|
195
195
|
| `@moku-labs/game/hot` | the Bun dev server | The `bunfig.toml` plugin that hot swaps views. See [Hot swap](./docs/hot-swap.md) |
|
|
196
196
|
| `@moku-labs/game/lint` | oxlint | The game lint rules, as an oxlint JS plugin |
|
|
197
|
+
| `@moku-labs/game/project` | Node and Bun | Where every engine id of a game is defined, line fresh at the call. The bin `moku-game-index`. See [Project index](./docs/project-index.md) |
|
|
197
198
|
| `@moku-labs/game/jsx-runtime`, `/jsx-dev-runtime` | anywhere | The JSX runtime. A game never imports it by hand. |
|
|
198
199
|
|
|
199
200
|
### Lint for games
|
|
200
201
|
|
|
201
|
-
`@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.
|
|
202
203
|
|
|
203
204
|
```json
|
|
204
205
|
// .oxlintrc.json
|
|
@@ -210,7 +211,8 @@ createApp({ plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin] });
|
|
|
210
211
|
"moku-game/dev-imports": "error",
|
|
211
212
|
"moku-game/no-module-state": "error",
|
|
212
213
|
"moku-game/determinism": "error",
|
|
213
|
-
"moku-game/rules-siblings": "error"
|
|
214
|
+
"moku-game/rules-siblings": "error",
|
|
215
|
+
"moku-game/static-keys": "error"
|
|
214
216
|
}
|
|
215
217
|
}
|
|
216
218
|
```
|
|
@@ -229,6 +231,7 @@ Each rule takes `{ "files": [...], "ignores": [...] }`. The defaults follow the
|
|
|
229
231
|
| [Events](./docs/events.md) | Every event and its payload |
|
|
230
232
|
| [Configuration](./docs/configuration.md) | Every config field and its default |
|
|
231
233
|
| [Lint for games](./docs/lint.md) | The `moku-game` oxlint rules, their options and defaults |
|
|
234
|
+
| [Project index](./docs/project-index.md) | `openProject`, the key scheme, `find` and `watch`, the `moku-game-index` bin |
|
|
232
235
|
| [Testing](./docs/testing.md) | Headless and visual tests, all scripts, test layout, lint rules |
|
|
233
236
|
| [`llms.txt`](./llms.txt) | The engine in one page, for an AI that writes a game |
|
|
234
237
|
|
|
@@ -265,7 +268,7 @@ A save of a logic file reloads the page. What swaps and what reloads: [docs/hot-
|
|
|
265
268
|
|
|
266
269
|
- **Node `>= 24`** and **Bun `>= 1.3.14`**. Use `bun` only.
|
|
267
270
|
- **TypeScript** in strict mode, with `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`.
|
|
268
|
-
- **`pixi.js` `^8.0.0`** as a peer dependency. `playwright-core` for visual tests
|
|
271
|
+
- **`pixi.js` `^8.0.0`** as a peer dependency. `playwright-core` for visual tests, `sharp` for the asset pack and `typescript` for the project index are optional peers. `yoga-layout` is loaded lazily by `ui`. The string compiler uses `@formatjs/icu-messageformat-parser` at build time only.
|
|
269
272
|
- **[`@moku-labs/core`](https://github.com/moku-labs/core)** is the kernel. **[`@moku-labs/common`](https://github.com/moku-labs/common)** brings `log` and `env`.
|
|
270
273
|
|
|
271
274
|
## License
|
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
|