@moku-labs/game 0.5.0 → 0.6.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 -1
- package/bin/moku-game-index.mjs +4 -0
- package/dist/project.d.mts +267 -0
- package/dist/project.mjs +2744 -0
- package/llms.txt +22 -1
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -194,6 +194,7 @@ 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
|
|
@@ -229,6 +230,7 @@ Each rule takes `{ "files": [...], "ignores": [...] }`. The defaults follow the
|
|
|
229
230
|
| [Events](./docs/events.md) | Every event and its payload |
|
|
230
231
|
| [Configuration](./docs/configuration.md) | Every config field and its default |
|
|
231
232
|
| [Lint for games](./docs/lint.md) | The `moku-game` oxlint rules, their options and defaults |
|
|
233
|
+
| [Project index](./docs/project-index.md) | `openProject`, the key scheme, `find` and `watch`, the `moku-game-index` bin |
|
|
232
234
|
| [Testing](./docs/testing.md) | Headless and visual tests, all scripts, test layout, lint rules |
|
|
233
235
|
| [`llms.txt`](./llms.txt) | The engine in one page, for an AI that writes a game |
|
|
234
236
|
|
|
@@ -265,7 +267,7 @@ A save of a logic file reloads the page. What swaps and what reloads: [docs/hot-
|
|
|
265
267
|
|
|
266
268
|
- **Node `>= 24`** and **Bun `>= 1.3.14`**. Use `bun` only.
|
|
267
269
|
- **TypeScript** in strict mode, with `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`.
|
|
268
|
-
- **`pixi.js` `^8.0.0`** as a peer dependency. `playwright-core` for visual tests
|
|
270
|
+
- **`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
271
|
- **[`@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
272
|
|
|
271
273
|
## License
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
//#region src/project/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* @file project — the public types of the project index: the options of `openProject`, the index
|
|
4
|
+
* it holds, the anchors of a key, the fresh lines `find` answers and the handle itself. The index
|
|
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
|
|
7
|
+
* holds a line number; `find` reads the line from the file as it is on disk at the call. No type
|
|
8
|
+
* here names a `typescript` type, so the shipped declarations need no TypeScript.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* What `openProject` reads. Paths are root-relative POSIX; the root itself is resolved against the
|
|
12
|
+
* working directory once and never inferred.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* // The editor opens the fixture game with the default manifest and a slower debounce.
|
|
17
|
+
* const options: ProjectOptions = { root: "tests/integration/merge-game", debounceMs: 120 };
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
type ProjectOptions = {
|
|
21
|
+
/** The folder of the game. A path that is not a directory rejects `openProject`. */root: string; /** The asset manifest, root-relative. Default `manifest.json`, the scanner's default. */
|
|
22
|
+
manifest?: string; /** The quiet period, in milliseconds, that closes a batch of file events. Default 75. */
|
|
23
|
+
debounceMs?: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
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"`).
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const kind: JsxKind = "idProp"; // id="settingsBoard" on <Signboard>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
type JsxKind = "literal" | "template" | "idProp" | "ident";
|
|
35
|
+
/**
|
|
36
|
+
* Where a key is defined or used, without a line: the file and what in it holds the key. `find`
|
|
37
|
+
* turns an anchor into a line on the file as it is now.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* // node:board/merge, defined by the const `merge` of nodes/merge.ts.
|
|
42
|
+
* const anchor: Anchor = { path: "nodes/merge.ts", binding: "merge" };
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
type Anchor = {
|
|
46
|
+
/** 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. */
|
|
49
|
+
component?: string; /** JSX only: how the key is written. */
|
|
50
|
+
kind?: JsxKind; /** JSX templates and identifiers: the literal head of the pattern, `card` for `card*Picture`. */
|
|
51
|
+
stem?: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The index of one game at one moment: every key with its anchors, every indexed file with its
|
|
55
|
+
* hash, and what could not be read without guessing. Computed on demand, never written to disk.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* index.symbols["flow:board"]; // { def: [{ path: "flows/board.ts", binding: "boardFlow" }] }
|
|
60
|
+
* index.files["nodes/merge.ts"]?.state; // "ok"
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
type ProjectIndex = {
|
|
64
|
+
/** The shape of this object. A change of shape bumps it. */schemaVersion: 1; /** sha1 hex over the sorted lines `${path}\0${hash}\n` of every indexed file. */
|
|
65
|
+
revision: string; /** The asset manifest, root-relative, when that file exists. */
|
|
66
|
+
manifest?: string;
|
|
67
|
+
/**
|
|
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.
|
|
70
|
+
*/
|
|
71
|
+
symbols: Record<string, {
|
|
72
|
+
def: Anchor[];
|
|
73
|
+
uses?: Anchor[];
|
|
74
|
+
conflict?: true;
|
|
75
|
+
}>; /** Every indexed file: the sha1 of its bytes, and `broken` with the first parse error. */
|
|
76
|
+
files: Record<string, {
|
|
77
|
+
hash: string;
|
|
78
|
+
state: "ok" | "broken";
|
|
79
|
+
error?: string;
|
|
80
|
+
}>; /** What a definer or a JSX key holds that cannot be read without running the game. */
|
|
81
|
+
unresolved: {
|
|
82
|
+
path: string;
|
|
83
|
+
reason: string;
|
|
84
|
+
}[];
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* What one watch batch changed: the new revision, the files whose bytes changed, the keys that
|
|
88
|
+
* left one file for another, and the keys gone from every file.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* // An agent moved nodes/toast.ts to nodes/board/toast.ts and fixed the import of the board flow.
|
|
93
|
+
* change.files; // ["flows/board.ts", "nodes/board/toast.ts", "nodes/toast.ts"]
|
|
94
|
+
* change.moved; // [{ key: "node:board/toast", from: "nodes/toast.ts", to: "nodes/board/toast.ts" }]
|
|
95
|
+
* change.removed; // []
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
type ProjectChange = {
|
|
99
|
+
/** The revision of the index after the batch. */revision: string; /** The root-relative paths whose bytes changed, appeared or vanished, sorted. */
|
|
100
|
+
files: string[]; /** A key that left one file and appeared in another in the same batch. */
|
|
101
|
+
moved: {
|
|
102
|
+
key: string;
|
|
103
|
+
from: string;
|
|
104
|
+
to: string;
|
|
105
|
+
}[]; /** Keys gone from every file. */
|
|
106
|
+
removed: string[];
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* One answer of `find`: the anchor with its line read from the file now. Lines and columns are
|
|
110
|
+
* 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.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* // The editor opens the Home scene of the fixture game.
|
|
116
|
+
* const [found] = await project.find("scene:home");
|
|
117
|
+
* [found?.path, found?.line, found?.range]; // ["features/home/scene.ts", 8, [8, 1, 13, 4]]
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
type Found = Anchor & {
|
|
121
|
+
/** The line, 1-based. */line: number; /** Start line, start column, end line, end column; 1-based, the end column exclusive. */
|
|
122
|
+
range: [startLine: number, startColumn: number, endLine: number, endColumn: number]; /** sha1 hex of the bytes the line was read from. */
|
|
123
|
+
hash: string; /** The file does not parse now; the line comes from its last good parse. */
|
|
124
|
+
broken?: true;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* The handle `openProject` returns: one index of one game root, kept fresh by one watcher, and
|
|
128
|
+
* the lines of a key read from disk at the call. Close it when the game closes.
|
|
129
|
+
*/
|
|
130
|
+
type ProjectApi = {
|
|
131
|
+
/**
|
|
132
|
+
* The current index. A watch batch or `changed` replaces it with a new frozen object.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* // The editor checks a node file before it opens it.
|
|
137
|
+
* project.index.files["nodes/merge.ts"]?.state; // "ok"
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
readonly index: ProjectIndex;
|
|
141
|
+
/**
|
|
142
|
+
* Answers where a key is defined, with lines read from the file on disk now. A file whose bytes
|
|
143
|
+
* changed since the index was built is parsed again for the answer; the index itself stays as
|
|
144
|
+
* 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 `[]`.
|
|
148
|
+
*
|
|
149
|
+
* @param key - An engine id: `node:board/merge`, `flow:board`, `textStyle:ui.title`, `jsx:card2Picture`.
|
|
150
|
+
* @returns One entry per place, in the order above; `broken` when the file does not parse now.
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* // The editor jumps to the node the flow view clicked, and to the X of the settings board.
|
|
154
|
+
* (await project.find("node:board/merge"))[0]?.line; // 17, in "nodes/merge.ts"
|
|
155
|
+
* (await project.find("jsx:settingsBoardClose")).map(found => `${found.path}:${found.line}`);
|
|
156
|
+
* // ["features/ui/kit.tsx:844", "features/settings/settings.tsx:301"]
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
find(key: string): Promise<Found[]>;
|
|
160
|
+
/**
|
|
161
|
+
* Calls back once per batch of file changes that changed the index, with the new index and
|
|
162
|
+
* what changed. Events only start a batch: when the quiet period ends, the root is walked and
|
|
163
|
+
* every file whose size or time stamp moved is hashed; a save with the same bytes changes
|
|
164
|
+
* nothing and calls nobody. The handle also walks once when watching starts and every two
|
|
165
|
+
* seconds after, for what the platform watcher missed. Every caller shares the one watcher of
|
|
166
|
+
* the handle. An error thrown by a callback is dropped, so the other callers still hear the
|
|
167
|
+
* batch.
|
|
168
|
+
*
|
|
169
|
+
* @param onIndex - Gets the new index and the change of the batch.
|
|
170
|
+
* @returns Removes this caller; the watcher stops with the last one.
|
|
171
|
+
* @throws {Error} When the handle is closed.
|
|
172
|
+
* @example
|
|
173
|
+
* ```ts
|
|
174
|
+
* // The editor refreshes its flow pages when an agent saves a game file.
|
|
175
|
+
* const stop = project.watch((index, change) => pages.refresh(index.revision, change.moved));
|
|
176
|
+
* stop(); // this caller hears no more batches
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
watch(onIndex: (index: ProjectIndex, change: ProjectChange) => void): () => void;
|
|
180
|
+
/**
|
|
181
|
+
* Re-indexes one root-relative path now, for a caller that watches files itself. A deleted
|
|
182
|
+
* file drops its entries; a path that is not a `.ts` or `.tsx` source of the index changes
|
|
183
|
+
* nothing. The watch callers are not called.
|
|
184
|
+
*
|
|
185
|
+
* @param path - The root-relative POSIX path of the file.
|
|
186
|
+
* @returns The index after the file was read.
|
|
187
|
+
* @throws {Error} When the path leaves the root, by `..` or by a symlink.
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* // The editor saved nodes/merge.ts through its own file layer.
|
|
191
|
+
* const index = await project.changed("nodes/merge.ts");
|
|
192
|
+
* index.files["nodes/merge.ts"]?.state; // "ok"
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
changed(path: string): Promise<ProjectIndex>;
|
|
196
|
+
/**
|
|
197
|
+
* Stops the watcher and drops every watch caller. `find` and `changed` keep working.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* // The editor closes the game.
|
|
202
|
+
* project.close(); // no more batches; project.watch(...) now throws
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
close(): void;
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* The part of the branded console the `moku-game-index` command line writes through. A
|
|
209
|
+
* `BrandConsole` of `@moku-labs/common/cli` fits it; a test passes a recorder.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* // A test records what the command printed.
|
|
214
|
+
* const lines: string[] = [];
|
|
215
|
+
* const ui: IndexUi = {
|
|
216
|
+
* line: text => lines.push(text),
|
|
217
|
+
* info: text => lines.push(text),
|
|
218
|
+
* error: text => lines.push(text)
|
|
219
|
+
* };
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
type IndexUi = {
|
|
223
|
+
/** Writes one line as it is: a `where` answer, the JSON of `--json`. */line(text: string): void; /** Writes one neutral line of the `--check` report. */
|
|
224
|
+
info(message: string): void; /** Writes one error line: a broken file, a conflict, an unknown key, a bad flag. */
|
|
225
|
+
error(message: string): void;
|
|
226
|
+
};
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/project/cli.d.ts
|
|
229
|
+
/**
|
|
230
|
+
* Runs `moku-game-index`: opens the project of `--root` and runs one command. Nothing here calls
|
|
231
|
+
* `process.exit`; the caller does.
|
|
232
|
+
*
|
|
233
|
+
* @param argv - The arguments after the script name.
|
|
234
|
+
* @param ui - Where the lines go. The branded console by default.
|
|
235
|
+
* @returns The exit code: `1` for a bad flag, a root that cannot be opened, an unknown key, or a
|
|
236
|
+
* `--check` that found a broken file or a conflict; else `0`.
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* // An agent asks where a node of the fixture game lives.
|
|
240
|
+
* await runCli(["--root", "tests/integration/merge-game", "where", "node:board/merge"]);
|
|
241
|
+
* // 0, and the console printed "nodes/merge.ts:17"
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
declare function runCli(argv: readonly string[], ui?: IndexUi): Promise<number>;
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/project/open.d.ts
|
|
247
|
+
/**
|
|
248
|
+
* Opens the project index of a game: checks the root (it exists, it is a directory, its real
|
|
249
|
+
* path), loads TypeScript, parses every source file and builds the index. Nothing is written to
|
|
250
|
+
* disk and nothing is inferred from the working directory but the root itself.
|
|
251
|
+
*
|
|
252
|
+
* @param options - The game root, the manifest path and the quiet period of `watch`.
|
|
253
|
+
* @returns The handle; close it when the game closes.
|
|
254
|
+
* @throws {Error} When the root is missing or not a directory, or the `typescript` package is not
|
|
255
|
+
* installed: `[game] The project index needs the "typescript" package.`
|
|
256
|
+
* @example
|
|
257
|
+
* ```ts
|
|
258
|
+
* // The editor opens the fixture game and answers where a node and a text style live.
|
|
259
|
+
* const project = await openProject({ root: "tests/integration/merge-game" });
|
|
260
|
+
* Object.keys(project.index.files).length; // 111
|
|
261
|
+
* (await project.find("textStyle:ui.title"))[0]?.line; // 39, in "features/ui/styles.ts"
|
|
262
|
+
* project.close();
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
declare function openProject(options: ProjectOptions): Promise<ProjectApi>;
|
|
266
|
+
//#endregion
|
|
267
|
+
export { type Anchor, type Found, type IndexUi, type JsxKind, type ProjectApi, type ProjectChange, type ProjectIndex, type ProjectOptions, openProject, runCli };
|