@moku-labs/game 0.4.5 → 0.5.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 +36 -0
- package/dist/assets.d.mts +1 -1
- package/dist/{commands-C83eQ5B9.d.mts → commands-mzE9PX0H.d.mts} +14 -6
- package/dist/control.d.mts +3 -3
- package/dist/control.mjs +1 -1
- package/dist/{define-BKWjWgyQ.d.mts → define-BbgXv4cy.d.mts} +1 -1
- package/dist/hot.d.mts +85 -0
- package/dist/hot.mjs +108 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +624 -71
- package/dist/inspect.d.mts +6 -6
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/lint.d.mts +79 -0
- package/dist/lint.mjs +360 -0
- package/dist/{run-D_R0oJ6g.mjs → run-Bcyg32M5.mjs} +40 -11
- package/dist/{runtime-Bhu442L8.d.mts → runtime-HTlmRb81.d.mts} +1 -1
- package/dist/testing.d.mts +5 -5
- package/dist/testing.mjs +19 -4
- package/dist/{types-Dy1xB68L.d.mts → types-BMEn0tSC.d.mts} +39 -4
- package/dist/{types-VErbiEXy.d.mts → types-BaUUcIaN.d.mts} +59 -6
- package/dist/{types-C-4f2jD_.d.mts → types-CXyipKhE.d.mts} +291 -249
- package/dist/{types-DtmFVCHf.d.mts → types-CYYKjvLn.d.mts} +1 -1
- package/dist/{types-w646dMeA.d.mts → types-ClF8say0.d.mts} +7 -3
- package/llms.txt +52 -6
- package/package.json +10 -1
package/dist/inspect.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { At as JournalEntry, H as EntitySnapshot, Ot as FlowGraph, R as ComponentSchema, U as FrameDiff, Ut as Api, f as Usage, gt as Position, j as Explained, k as Api$1, m as Api$2, t as Api$3, tn as Snapshot, w as RenderStats, x as Under } from "./types-
|
|
2
|
-
import { a as InputOf, c as Source, d as HeadlessApp, i as InputArguments, l as WatchApp, o as InputSchema, t as CheatEntry, u as WatchInput } from "./types-
|
|
3
|
-
import { c as UiNode } from "./types-
|
|
4
|
-
import {
|
|
5
|
-
import { n as defineSource } from "./define-
|
|
6
|
-
import { n as EffectsApi, r as EffectsStats } from "./types-
|
|
1
|
+
import { At as JournalEntry, H as EntitySnapshot, Ot as FlowGraph, R as ComponentSchema, U as FrameDiff, Ut as Api, f as Usage, gt as Position, j as Explained, k as Api$1, m as Api$2, t as Api$3, tn as Snapshot, w as RenderStats, x as Under } from "./types-BMEn0tSC.mjs";
|
|
2
|
+
import { a as InputOf, c as Source, d as HeadlessApp, i as InputArguments, l as WatchApp, o as InputSchema, t as CheatEntry, u as WatchInput } from "./types-CYYKjvLn.mjs";
|
|
3
|
+
import { c as UiNode } from "./types-BaUUcIaN.mjs";
|
|
4
|
+
import { a as SoundEntry, t as AudioApi, v as UiApi } from "./types-CXyipKhE.mjs";
|
|
5
|
+
import { n as defineSource } from "./define-BbgXv4cy.mjs";
|
|
6
|
+
import { n as EffectsApi, r as EffectsStats } from "./types-ClF8say0.mjs";
|
|
7
7
|
|
|
8
8
|
//#region src/plugins/flow/doors/read.d.ts
|
|
9
9
|
/**
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as jsxs, i as jsxDEV, n as JSX, r as jsx, t as Fragment } from "./runtime-
|
|
1
|
+
import { a as jsxs, i as jsxDEV, n as JSX, r as jsx, t as Fragment } from "./runtime-HTlmRb81.mjs";
|
|
2
2
|
export { Fragment, type JSX, jsx, jsxDEV, jsxs };
|
package/dist/jsx-runtime.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as jsxs, n as JSX, r as jsx, t as Fragment } from "./runtime-
|
|
1
|
+
import { a as jsxs, n as JSX, r as jsx, t as Fragment } from "./runtime-HTlmRb81.mjs";
|
|
2
2
|
export { Fragment, type JSX, jsx, jsxs };
|
package/dist/lint.d.mts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/lint.d.ts
|
|
2
|
+
/** The options every rule takes: which files it checks. A key given replaces the rule's default. */
|
|
3
|
+
type GameLintOptions = {
|
|
4
|
+
/** Globs of the files the rule checks, relative to the directory oxlint runs in. */files?: string[]; /** Globs of the files the rule skips, even when `files` matches them. */
|
|
5
|
+
ignores?: string[];
|
|
6
|
+
};
|
|
7
|
+
/** The part of an ESTree node the rules read. oxlint and ESLint hand over full nodes. */
|
|
8
|
+
type GameLintNode = {
|
|
9
|
+
readonly type: string;
|
|
10
|
+
readonly name?: string;
|
|
11
|
+
readonly value?: unknown;
|
|
12
|
+
readonly kind?: string;
|
|
13
|
+
readonly computed?: boolean;
|
|
14
|
+
readonly importKind?: string;
|
|
15
|
+
readonly exportKind?: string;
|
|
16
|
+
readonly source?: GameLintNode | null;
|
|
17
|
+
readonly body?: readonly GameLintNode[];
|
|
18
|
+
readonly declarations?: readonly GameLintNode[];
|
|
19
|
+
readonly declaration?: GameLintNode | null;
|
|
20
|
+
readonly init?: GameLintNode | null;
|
|
21
|
+
readonly callee?: GameLintNode;
|
|
22
|
+
readonly arguments?: readonly GameLintNode[];
|
|
23
|
+
readonly object?: GameLintNode;
|
|
24
|
+
readonly property?: GameLintNode;
|
|
25
|
+
};
|
|
26
|
+
/** The part of the rule context the rules read: where the file is, the options, the report. */
|
|
27
|
+
type GameLintContext = {
|
|
28
|
+
readonly cwd: string;
|
|
29
|
+
readonly filename: string;
|
|
30
|
+
readonly options: readonly unknown[];
|
|
31
|
+
report(descriptor: {
|
|
32
|
+
node: GameLintNode;
|
|
33
|
+
message: string;
|
|
34
|
+
}): void;
|
|
35
|
+
};
|
|
36
|
+
/** A node visitor per ESTree node type. */
|
|
37
|
+
type GameLintListeners = Record<string, (node: GameLintNode) => void>;
|
|
38
|
+
/** One rule in the ESLint rule shape oxlint `jsPlugins` load. */
|
|
39
|
+
type GameLintRule = {
|
|
40
|
+
readonly meta: {
|
|
41
|
+
readonly type: "problem";
|
|
42
|
+
readonly docs: {
|
|
43
|
+
readonly description: string;
|
|
44
|
+
};
|
|
45
|
+
readonly schema: readonly unknown[];
|
|
46
|
+
};
|
|
47
|
+
create(context: GameLintContext): GameLintListeners;
|
|
48
|
+
};
|
|
49
|
+
/** The plugin object: `moku-game` and its six rules. */
|
|
50
|
+
type GameLintPlugin = {
|
|
51
|
+
readonly meta: {
|
|
52
|
+
readonly name: "moku-game";
|
|
53
|
+
};
|
|
54
|
+
readonly rules: Readonly<Record<GameLintRuleName, GameLintRule>>;
|
|
55
|
+
};
|
|
56
|
+
/** The rule names, without the `moku-game/` prefix. */
|
|
57
|
+
type GameLintRuleName = "lazy-imports" | "native-imports" | "dev-imports" | "no-module-state" | "determinism" | "rules-siblings";
|
|
58
|
+
/**
|
|
59
|
+
* Turn a glob into an anchored regular expression: `**`, `*`, `?` and one level of `{a,b}`.
|
|
60
|
+
*
|
|
61
|
+
* @param glob - A POSIX glob relative to the lint root.
|
|
62
|
+
* @returns The expression a relative POSIX path is tested against.
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* globToRegExp("{nodes,rules}/*.ts").test("rules/merge.ts"); // true
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
declare function globToRegExp(glob: string): RegExp;
|
|
69
|
+
/**
|
|
70
|
+
* The `moku-game` oxlint plugin. A game loads it by package name and turns its rules on.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```json
|
|
74
|
+
* { "jsPlugins": ["@moku-labs/game/lint"], "rules": { "moku-game/lazy-imports": "error" } }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
declare const plugin: GameLintPlugin;
|
|
78
|
+
//#endregion
|
|
79
|
+
export { GameLintContext, GameLintListeners, GameLintNode, GameLintOptions, GameLintPlugin, GameLintRule, GameLintRuleName, plugin as default, globToRegExp };
|
package/dist/lint.mjs
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
//#region src/lint.ts
|
|
3
|
+
/**
|
|
4
|
+
* @file The lint door of the engine (subpath `./lint`): an oxlint JS plugin named `moku-game` with
|
|
5
|
+
* the rules a game carries. A game loads it by package name in `.oxlintrc.json`
|
|
6
|
+
* (`"jsPlugins": ["@moku-labs/game/lint"]`) and turns the rules on as `moku-game/<rule>`. The rules
|
|
7
|
+
* use the ESLint rule API, so ESLint 9 loads the same object. Every rule takes
|
|
8
|
+
* `[{ files, ignores }]`: globs relative to the directory oxlint runs in; a key given replaces the
|
|
9
|
+
* default. Only `node:path` is imported; the file is erasable TypeScript, so Node loads it unbuilt.
|
|
10
|
+
*/
|
|
11
|
+
/** Tests and test helpers: no rule checks them by default. */
|
|
12
|
+
const TESTS = [
|
|
13
|
+
"tests/**",
|
|
14
|
+
"**/__tests__/**",
|
|
15
|
+
"**/*.{test,spec}.{ts,tsx}"
|
|
16
|
+
];
|
|
17
|
+
/** Dev modules: a `.dev` module is wired into the editor only. */
|
|
18
|
+
const DEV_MODULES = ["**/*.dev.{ts,tsx}"];
|
|
19
|
+
/** The logic of a game: state, tables, nodes, flows, rules, features. `**` so `src/` matches too. */
|
|
20
|
+
const LOGIC = [
|
|
21
|
+
"**/state.ts",
|
|
22
|
+
"**/tables.ts",
|
|
23
|
+
"**/{nodes,flows,rules,features}/**"
|
|
24
|
+
];
|
|
25
|
+
/** The options schema every rule shares; oxlint refuses options a rule has no schema for. */
|
|
26
|
+
const SCHEMA = [{
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
files: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: { type: "string" }
|
|
32
|
+
},
|
|
33
|
+
ignores: {
|
|
34
|
+
type: "array",
|
|
35
|
+
items: { type: "string" }
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
additionalProperties: false
|
|
39
|
+
}];
|
|
40
|
+
/** Bases of a timer call that still reach the global timer. */
|
|
41
|
+
const GLOBAL_OBJECT = /^(?:globalThis|window|self)$/;
|
|
42
|
+
/** The global timers of L3. */
|
|
43
|
+
const TIMER = /^set(?:Timeout|Interval)$/;
|
|
44
|
+
/** The forbidden member reads of L3: object, property and the fix. */
|
|
45
|
+
const CLOCK_READS = [
|
|
46
|
+
{
|
|
47
|
+
object: "Math",
|
|
48
|
+
property: "random",
|
|
49
|
+
message: "L3: use an rng stream: rng.stream(id)."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
object: "Date",
|
|
53
|
+
property: "now",
|
|
54
|
+
message: "L3: use `now` from the node context."
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
object: "performance",
|
|
58
|
+
property: "now",
|
|
59
|
+
message: "L3: use `now` from the node context."
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
/** The collections a module may not hold at module scope (L5). */
|
|
63
|
+
const COLLECTION = /^(?:Map|Set|WeakMap|WeakSet)$/;
|
|
64
|
+
/**
|
|
65
|
+
* Turn a glob into an anchored regular expression: `**`, `*`, `?` and one level of `{a,b}`.
|
|
66
|
+
*
|
|
67
|
+
* @param glob - A POSIX glob relative to the lint root.
|
|
68
|
+
* @returns The expression a relative POSIX path is tested against.
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* globToRegExp("{nodes,rules}/*.ts").test("rules/merge.ts"); // true
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
function globToRegExp(glob) {
|
|
75
|
+
let source = "";
|
|
76
|
+
let index = 0;
|
|
77
|
+
while (index < glob.length) {
|
|
78
|
+
const char = glob.charAt(index);
|
|
79
|
+
if (glob.startsWith("**/", index)) {
|
|
80
|
+
source += "(?:.*/)?";
|
|
81
|
+
index += 3;
|
|
82
|
+
} else if (glob.startsWith("**", index)) {
|
|
83
|
+
source += ".*";
|
|
84
|
+
index += 2;
|
|
85
|
+
} else {
|
|
86
|
+
source += globChar(char);
|
|
87
|
+
index += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return new RegExp(`^${source}$`);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The expression for one glob character other than `**`.
|
|
94
|
+
*
|
|
95
|
+
* @param char - The character.
|
|
96
|
+
* @returns Its regular-expression source.
|
|
97
|
+
*/
|
|
98
|
+
function globChar(char) {
|
|
99
|
+
switch (char) {
|
|
100
|
+
case "*": return "[^/]*";
|
|
101
|
+
case "?": return "[^/]";
|
|
102
|
+
case "{": return "(?:";
|
|
103
|
+
case "}": return ")";
|
|
104
|
+
case ",": return "|";
|
|
105
|
+
default: return char.replaceAll(/[$()+.[\\\]^|]/g, String.raw`\$&`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Whether the rule checks the file the context lints: `files` match and no `ignores` match.
|
|
110
|
+
*
|
|
111
|
+
* @param context - The rule context.
|
|
112
|
+
* @param defaults - The rule's default globs.
|
|
113
|
+
* @returns True when the rule applies.
|
|
114
|
+
*/
|
|
115
|
+
function inScope(context, defaults) {
|
|
116
|
+
const [given] = context.options;
|
|
117
|
+
const files = given?.files ?? defaults.files;
|
|
118
|
+
const ignores = given?.ignores ?? defaults.ignores;
|
|
119
|
+
const file = path.relative(context.cwd, context.filename).split(path.sep).join("/");
|
|
120
|
+
const matches = (globs) => globs.some((glob) => globToRegExp(glob).test(file));
|
|
121
|
+
return matches(files) && !matches(ignores);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The specifier of an import node, when it is a string literal.
|
|
125
|
+
*
|
|
126
|
+
* @param node - An import, export-from or `import()` node.
|
|
127
|
+
* @returns The specifier, or undefined for a computed `import()` or a local export.
|
|
128
|
+
*/
|
|
129
|
+
function specifierOf(node) {
|
|
130
|
+
const value = node.source?.type === "Literal" ? node.source.value : void 0;
|
|
131
|
+
return typeof value === "string" ? value : void 0;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Whether a static import or export carries types only: `import type` or `export type`.
|
|
135
|
+
* `import { type A }` keeps a side-effect import under `verbatimModuleSyntax`, so it counts as a value.
|
|
136
|
+
*
|
|
137
|
+
* @param node - A static import or export node.
|
|
138
|
+
* @returns True for a type-only statement.
|
|
139
|
+
*/
|
|
140
|
+
function isTypeOnly(node) {
|
|
141
|
+
return node.importKind === "type" || node.exportKind === "type";
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Build a rule that reports imports of forbidden specifiers.
|
|
145
|
+
*
|
|
146
|
+
* @param description - What the rule enforces.
|
|
147
|
+
* @param defaults - The rule's default globs.
|
|
148
|
+
* @param checks - The forbidden patterns and their messages.
|
|
149
|
+
* @param lazy - True for L2: `import()` and type-only statements pass.
|
|
150
|
+
* @returns The rule.
|
|
151
|
+
*/
|
|
152
|
+
function importRule(description, defaults, checks, lazy = false) {
|
|
153
|
+
return {
|
|
154
|
+
meta: {
|
|
155
|
+
type: "problem",
|
|
156
|
+
docs: { description },
|
|
157
|
+
schema: SCHEMA
|
|
158
|
+
},
|
|
159
|
+
/**
|
|
160
|
+
* Visit the file when the rule's globs match it.
|
|
161
|
+
*
|
|
162
|
+
* @param context - The rule context.
|
|
163
|
+
* @returns The node visitors, none for a file out of scope.
|
|
164
|
+
*/
|
|
165
|
+
create(context) {
|
|
166
|
+
if (!inScope(context, defaults)) return {};
|
|
167
|
+
const check = (node) => {
|
|
168
|
+
const name = specifierOf(node);
|
|
169
|
+
const hit = name === void 0 ? void 0 : checks.find((item) => item.pattern.test(name));
|
|
170
|
+
if (hit === void 0) return;
|
|
171
|
+
if (lazy && isTypeOnly(node)) return;
|
|
172
|
+
context.report({
|
|
173
|
+
node,
|
|
174
|
+
message: hit.message
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
const listeners = {
|
|
178
|
+
ImportDeclaration: check,
|
|
179
|
+
ExportAllDeclaration: check,
|
|
180
|
+
ExportNamedDeclaration: check
|
|
181
|
+
};
|
|
182
|
+
if (!lazy) listeners.ImportExpression = check;
|
|
183
|
+
return listeners;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The name of a member's property: `a.b` and `a["b"]`.
|
|
189
|
+
*
|
|
190
|
+
* @param node - A MemberExpression.
|
|
191
|
+
* @returns The property name, or undefined for a computed non-literal key.
|
|
192
|
+
*/
|
|
193
|
+
function propertyName(node) {
|
|
194
|
+
const key = node.property;
|
|
195
|
+
if (key === void 0) return void 0;
|
|
196
|
+
if (!node.computed) return key.name;
|
|
197
|
+
return typeof key.value === "string" ? key.value : void 0;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Report the module-scope state of one top-level statement (L5).
|
|
201
|
+
*
|
|
202
|
+
* @param context - The rule context.
|
|
203
|
+
* @param statement - A statement of the Program body.
|
|
204
|
+
*/
|
|
205
|
+
function reportModuleState(context, statement) {
|
|
206
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
207
|
+
if (declaration?.type !== "VariableDeclaration") return;
|
|
208
|
+
if (declaration.kind === "let" || declaration.kind === "var") {
|
|
209
|
+
context.report({
|
|
210
|
+
node: declaration,
|
|
211
|
+
message: "L5: no module-scope state. State lives in player and session."
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
216
|
+
const init = declarator.init;
|
|
217
|
+
if (init?.type === "NewExpression" && COLLECTION.test(init.callee?.name ?? "")) context.report({
|
|
218
|
+
node: init,
|
|
219
|
+
message: "L5: no module-scope collections. Create them inside a function."
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Whether a call's callee reaches a global timer: `setTimeout(...)`, `globalThis.setInterval(...)`.
|
|
225
|
+
*
|
|
226
|
+
* @param callee - The callee of a CallExpression.
|
|
227
|
+
* @returns True for a timer.
|
|
228
|
+
*/
|
|
229
|
+
function isTimer(callee) {
|
|
230
|
+
if (callee?.type === "Identifier") return TIMER.test(callee.name ?? "");
|
|
231
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
232
|
+
return GLOBAL_OBJECT.test(callee.object?.name ?? "") && TIMER.test(propertyName(callee) ?? "");
|
|
233
|
+
}
|
|
234
|
+
/** L2: static value imports of the packages the engine loads lazily. */
|
|
235
|
+
const lazyImports = importRule("No static import of pixi.js or yoga-layout; import() and import type pass.", {
|
|
236
|
+
files: ["**"],
|
|
237
|
+
ignores: TESTS
|
|
238
|
+
}, [{
|
|
239
|
+
pattern: /^pixi\.js(?:\/|$)/,
|
|
240
|
+
message: "L2: the renderer loads Pixi lazily. Draw with the engine's tags, use import() or import type."
|
|
241
|
+
}, {
|
|
242
|
+
pattern: /^yoga-layout(?:\/|$)/,
|
|
243
|
+
message: "L2: the ui plugin loads Yoga lazily. Use import() or import type."
|
|
244
|
+
}], true);
|
|
245
|
+
/** L13: native packages stay out of the engine-facing layers. */
|
|
246
|
+
const nativeImports = importRule("No @moku-labs/system, @moku-labs/native or @tauri-apps/* in the engine-facing layers.", {
|
|
247
|
+
files: [
|
|
248
|
+
...LOGIC,
|
|
249
|
+
"**/kit.ts",
|
|
250
|
+
"**/game.ts"
|
|
251
|
+
],
|
|
252
|
+
ignores: [...DEV_MODULES, ...TESTS]
|
|
253
|
+
}, [{
|
|
254
|
+
pattern: /^(?:@moku-labs\/(?:system|native)(?:\/|$)|@tauri-apps\/)/,
|
|
255
|
+
message: "L13: only platform-bridge.ts, native.ts and web/ import a native package. Pass a PlatformProvider."
|
|
256
|
+
}]);
|
|
257
|
+
/** Dev only: the editor and the control door stay in dev files. */
|
|
258
|
+
const developmentImports = importRule("@moku-labs/editor and @moku-labs/game/control only in dev files and tests.", {
|
|
259
|
+
files: ["**"],
|
|
260
|
+
ignores: [
|
|
261
|
+
"web/main.ts",
|
|
262
|
+
"web/dev*.ts",
|
|
263
|
+
"web/editor*.ts",
|
|
264
|
+
...DEV_MODULES,
|
|
265
|
+
...TESTS
|
|
266
|
+
]
|
|
267
|
+
}, [{
|
|
268
|
+
pattern: /^@moku-labs\/(?:editor(?:\/|$)|game\/control$)/,
|
|
269
|
+
message: "Dev only: web/main.ts, web/dev*.ts, web/editor*.ts, .dev modules and tests import the editor and /control."
|
|
270
|
+
}]);
|
|
271
|
+
/** L4: a rule imports only its siblings. */
|
|
272
|
+
const rulesSiblings = importRule("A file under rules/ imports only its siblings.", {
|
|
273
|
+
files: ["**/rules/**"],
|
|
274
|
+
ignores: TESTS
|
|
275
|
+
}, [{
|
|
276
|
+
pattern: /^(?!\.\/)/,
|
|
277
|
+
message: "L4: a rule imports only its siblings in rules/: (state, input, tables) => result."
|
|
278
|
+
}]);
|
|
279
|
+
/**
|
|
280
|
+
* The `moku-game` oxlint plugin. A game loads it by package name and turns its rules on.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```json
|
|
284
|
+
* { "jsPlugins": ["@moku-labs/game/lint"], "rules": { "moku-game/lazy-imports": "error" } }
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
const plugin = {
|
|
288
|
+
meta: { name: "moku-game" },
|
|
289
|
+
rules: {
|
|
290
|
+
"lazy-imports": lazyImports,
|
|
291
|
+
"native-imports": nativeImports,
|
|
292
|
+
"dev-imports": developmentImports,
|
|
293
|
+
"no-module-state": {
|
|
294
|
+
meta: {
|
|
295
|
+
type: "problem",
|
|
296
|
+
docs: { description: "No module-scope let or var and no module-scope Map, Set, WeakMap, WeakSet." },
|
|
297
|
+
schema: SCHEMA
|
|
298
|
+
},
|
|
299
|
+
/**
|
|
300
|
+
* Visit the file when the rule's globs match it.
|
|
301
|
+
*
|
|
302
|
+
* @param context - The rule context.
|
|
303
|
+
* @returns The node visitors, none for a file out of scope.
|
|
304
|
+
*/
|
|
305
|
+
create(context) {
|
|
306
|
+
if (!inScope(context, {
|
|
307
|
+
files: ["**"],
|
|
308
|
+
ignores: TESTS
|
|
309
|
+
})) return {};
|
|
310
|
+
return { Program: (program) => {
|
|
311
|
+
for (const statement of program.body ?? []) reportModuleState(context, statement);
|
|
312
|
+
} };
|
|
313
|
+
}
|
|
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
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
//#endregion
|
|
360
|
+
export { plugin as default, globToRegExp };
|
|
@@ -525,7 +525,8 @@ function invalidBookmark() {
|
|
|
525
525
|
return /* @__PURE__ */ new Error("[game] The bookmark is not a flow.bookmark() value.\n Pass the JSON that game.bookmark returned.");
|
|
526
526
|
}
|
|
527
527
|
/**
|
|
528
|
-
* Reads a bookmark out of JSON, the value `flow.bookmark()` made and the editor kept.
|
|
528
|
+
* Reads a bookmark out of JSON, the value `flow.bookmark()` made and the editor kept. The
|
|
529
|
+
* `scene` the `game.bookmark` door added is kept when present.
|
|
529
530
|
*
|
|
530
531
|
* @param value - The JSON the editor sent.
|
|
531
532
|
* @returns The bookmark.
|
|
@@ -539,8 +540,9 @@ function readBookmark(value) {
|
|
|
539
540
|
if (!isRecord(value)) throw invalidBookmark();
|
|
540
541
|
const { path, input, player, session, graph } = value;
|
|
541
542
|
const rng = readRng(value.rng);
|
|
542
|
-
if (typeof path
|
|
543
|
-
|
|
543
|
+
if (!(typeof path === "string" && typeof graph === "string") || !(input !== void 0 && player !== void 0 && session !== void 0) || rng === void 0) throw invalidBookmark();
|
|
544
|
+
const { scene } = value;
|
|
545
|
+
if (scene === void 0) return {
|
|
544
546
|
path,
|
|
545
547
|
input,
|
|
546
548
|
player,
|
|
@@ -548,6 +550,16 @@ function readBookmark(value) {
|
|
|
548
550
|
rng,
|
|
549
551
|
graph
|
|
550
552
|
};
|
|
553
|
+
if (typeof scene !== "string") throw invalidBookmark();
|
|
554
|
+
return {
|
|
555
|
+
path,
|
|
556
|
+
input,
|
|
557
|
+
player,
|
|
558
|
+
session,
|
|
559
|
+
rng,
|
|
560
|
+
graph,
|
|
561
|
+
scene
|
|
562
|
+
};
|
|
551
563
|
}
|
|
552
564
|
/**
|
|
553
565
|
* Builds the error of a value that is not a repro.
|
|
@@ -1051,9 +1063,10 @@ const fillCommand = defineCommand({
|
|
|
1051
1063
|
//#endregion
|
|
1052
1064
|
//#region src/plugins/flow/control.ts
|
|
1053
1065
|
/**
|
|
1054
|
-
* Enters a bookmark, or a repro's bookmark followed by its route.
|
|
1066
|
+
* Enters a bookmark, or a repro's bookmark followed by its route. A bookmark's `scene` is handed
|
|
1067
|
+
* to `scenes.expect` first, so a rest node without a scene of its own mounts it.
|
|
1055
1068
|
*
|
|
1056
|
-
* @param app - The app.
|
|
1069
|
+
* @param app - The app; with `scenes`, the bookmark's scene is expected before the restore.
|
|
1057
1070
|
* @param input - Exactly one of a bookmark and a repro, as JSON.
|
|
1058
1071
|
* @param input.bookmark - A `flow.bookmark()` value.
|
|
1059
1072
|
* @param input.repro - A `/testing` repro.
|
|
@@ -1063,7 +1076,9 @@ const fillCommand = defineCommand({
|
|
|
1063
1076
|
async function restoreFrom(app, input) {
|
|
1064
1077
|
const { bookmark, repro } = input;
|
|
1065
1078
|
if (bookmark !== void 0 && repro === void 0) {
|
|
1066
|
-
|
|
1079
|
+
const read = readBookmark(bookmark);
|
|
1080
|
+
if (read.scene !== void 0) app.scenes?.expect(read.scene);
|
|
1081
|
+
await app.flow.restore(read);
|
|
1067
1082
|
return app.flow.state();
|
|
1068
1083
|
}
|
|
1069
1084
|
if (repro !== void 0 && bookmark === void 0) {
|
|
@@ -1127,13 +1142,15 @@ const walkCommand = defineCommand({
|
|
|
1127
1142
|
}
|
|
1128
1143
|
});
|
|
1129
1144
|
/**
|
|
1130
|
-
* Takes a bookmark of the rest point: the node plus the committed state, plain JSON.
|
|
1145
|
+
* Takes a bookmark of the rest point: the node plus the committed state, plain JSON. An app with
|
|
1146
|
+
* the screen plugins adds the mounted `scene`, so a restore in a fresh page builds it again.
|
|
1131
1147
|
*
|
|
1132
1148
|
* @example
|
|
1133
1149
|
* ```ts
|
|
1134
|
-
* // The editor keeps the position
|
|
1150
|
+
* // The editor keeps the position while the Settings popup stands over Home.
|
|
1135
1151
|
* const { value } = await run(app, commands.bookmark);
|
|
1136
|
-
* value.path; // "
|
|
1152
|
+
* value.path; // "settings/open"
|
|
1153
|
+
* value.scene; // "home"
|
|
1137
1154
|
* ```
|
|
1138
1155
|
*/
|
|
1139
1156
|
const bookmarkCommand = defineCommand({
|
|
@@ -1144,12 +1161,18 @@ const bookmarkCommand = defineCommand({
|
|
|
1144
1161
|
run: (app) => {
|
|
1145
1162
|
if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) throw controlRefused();
|
|
1146
1163
|
app.log.debug("moku:dev", { command: "game.bookmark" });
|
|
1147
|
-
|
|
1164
|
+
const bookmark = app.flow.bookmark();
|
|
1165
|
+
const scene = app.scenes?.current();
|
|
1166
|
+
return scene === void 0 ? bookmark : {
|
|
1167
|
+
...bookmark,
|
|
1168
|
+
scene
|
|
1169
|
+
};
|
|
1148
1170
|
}
|
|
1149
1171
|
});
|
|
1150
1172
|
/**
|
|
1151
1173
|
* Restores a bookmark, or a repro: its state at its checkpoint, then its route. Replaces the
|
|
1152
|
-
* state outside the graph, so the session is tainted and the command journaled.
|
|
1174
|
+
* state outside the graph, so the session is tainted and the command journaled. A bookmark's
|
|
1175
|
+
* `scene` goes to `scenes.expect` first: a bookmark at a popup comes back with the scene under it.
|
|
1153
1176
|
*
|
|
1154
1177
|
* @example
|
|
1155
1178
|
* ```ts
|
|
@@ -1157,6 +1180,12 @@ const bookmarkCommand = defineCommand({
|
|
|
1157
1180
|
* const repro = { player: { coins: 40 }, checkpoint: "home", route: [{ at: "home", intent: "play" }] };
|
|
1158
1181
|
* const ran = await run(app, commands.restore, { repro });
|
|
1159
1182
|
* ran.state; // { path: "board/awaitIntent", frame: 12, tainted: true }
|
|
1183
|
+
*
|
|
1184
|
+
* // A fresh page restores the bookmark game.bookmark took at the Settings popup over Home.
|
|
1185
|
+
* bookmark.scene; // "home"
|
|
1186
|
+
* await run(app, commands.restore, { bookmark });
|
|
1187
|
+
* await app.flow.walk([]); // the scene stage runs after the restore resolved
|
|
1188
|
+
* app.scenes.current(); // "home"
|
|
1160
1189
|
* ```
|
|
1161
1190
|
*/
|
|
1162
1191
|
const restoreCommand = defineCommand({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as JsxChild, m as UiIntrinsicElements, t as DescriptionNode } from "./types-
|
|
1
|
+
import { a as JsxChild, m as UiIntrinsicElements, t as DescriptionNode } from "./types-BaUUcIaN.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/plugins/ui/jsx/runtime.d.ts
|
|
4
4
|
/** What the compiler passes as props: the children plus whatever the tag declared. */
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $t as SaveDoc, Kt as Json, Qt as ProviderCall, Ut as Api$2, Zt as PlayerStateProvider, an as Api$3, k as Api$1, m as Api, pn as FakeClock } from "./types-
|
|
2
|
-
import { _ as stepFrames, a as InputOf, d as HeadlessApp, f as HeadlessGame, g as runRepro, h as createHeadless, m as ReproResult, p as Repro, r as ControlApp } from "./types-
|
|
3
|
-
import { H as InputApi } from "./types-
|
|
4
|
-
import {
|
|
5
|
-
import { t as commands } from "./commands-
|
|
1
|
+
import { $t as SaveDoc, Kt as Json, Qt as ProviderCall, Ut as Api$2, Zt as PlayerStateProvider, an as Api$3, k as Api$1, m as Api, pn as FakeClock } from "./types-BMEn0tSC.mjs";
|
|
2
|
+
import { _ as stepFrames, a as InputOf, d as HeadlessApp, f as HeadlessGame, g as runRepro, h as createHeadless, m as ReproResult, p as Repro, r as ControlApp } from "./types-CYYKjvLn.mjs";
|
|
3
|
+
import { H as InputApi } from "./types-BaUUcIaN.mjs";
|
|
4
|
+
import { b as AnimApi, v as UiApi } from "./types-CXyipKhE.mjs";
|
|
5
|
+
import { t as commands } from "./commands-mzE9PX0H.mjs";
|
|
6
6
|
|
|
7
7
|
//#region src/plugins/clock/fake.d.ts
|
|
8
8
|
/**
|
package/dist/testing.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as saveOf, t as memory } from "./memory-CvgdnsQO.mjs";
|
|
2
|
-
import { a as stepFrames, i as runRepro, n as commands, r as createHeadless, t as run } from "./run-
|
|
2
|
+
import { a as stepFrames, i as runRepro, n as commands, r as createHeadless, t as run } from "./run-Bcyg32M5.mjs";
|
|
3
3
|
import { n as read, t as sources } from "./sources-DVWJ8nfr.mjs";
|
|
4
4
|
import { createBrandConsole } from "@moku-labs/common/cli";
|
|
5
5
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
@@ -629,7 +629,7 @@ function addressesOf(keys) {
|
|
|
629
629
|
* ```
|
|
630
630
|
*/
|
|
631
631
|
function parentOf(parent) {
|
|
632
|
-
if (typeof parent
|
|
632
|
+
if (!(typeof parent === "object" && parent !== null && !Array.isArray(parent))) return void 0;
|
|
633
633
|
return typeof parent.entity === "number" ? parent.entity : void 0;
|
|
634
634
|
}
|
|
635
635
|
/**
|
|
@@ -663,6 +663,20 @@ function stateOf(app) {
|
|
|
663
663
|
};
|
|
664
664
|
}
|
|
665
665
|
/**
|
|
666
|
+
* The ui tree without the words of its texts, at every depth.
|
|
667
|
+
*
|
|
668
|
+
* @param node - A node of the ui source.
|
|
669
|
+
* @returns The node and its children, none with `content`.
|
|
670
|
+
*/
|
|
671
|
+
function withoutContent(node) {
|
|
672
|
+
const copy = {
|
|
673
|
+
...node,
|
|
674
|
+
children: node.children.map((child) => withoutContent(child))
|
|
675
|
+
};
|
|
676
|
+
delete copy.content;
|
|
677
|
+
return copy;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
666
680
|
* Reads the screen a checkpoint saves: the ui tree, and every entity a projection names as
|
|
667
681
|
* `{ projection, key, components }`, sorted by projection then key. Ui parts, rings and unkeyed
|
|
668
682
|
* views appear through the tree only, or not at all.
|
|
@@ -675,13 +689,14 @@ function describeOf(app) {
|
|
|
675
689
|
const views = [];
|
|
676
690
|
for (const entity of read(app, sources.entities)) {
|
|
677
691
|
const address = addresses.get(entity.id);
|
|
678
|
-
if (address
|
|
692
|
+
if (address === void 0) continue;
|
|
693
|
+
views.push({
|
|
679
694
|
...address,
|
|
680
695
|
components: withParentAddress(entity.components, addresses)
|
|
681
696
|
});
|
|
682
697
|
}
|
|
683
698
|
return {
|
|
684
|
-
ui: read(app, sources.ui),
|
|
699
|
+
ui: withoutContent(read(app, sources.ui)),
|
|
685
700
|
views: views.toSorted(byAddress)
|
|
686
701
|
};
|
|
687
702
|
}
|