@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
|
@@ -0,0 +1,278 @@
|
|
|
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, components 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 key-carrying prop on a component: `id=`, or a prop
|
|
28
|
+
* whose name ends in `Key` (`id="settingsBoard"`, `amountKey="giftReward"`).
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const kind: JsxKind = "idProp"; // id="settingsBoard" on <Signboard>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
type JsxKind = "literal" | "template" | "idProp" | "ident";
|
|
36
|
+
/**
|
|
37
|
+
* Where a key is defined or used, without a line: the file and what in it holds the key. `find`
|
|
38
|
+
* turns an anchor into a line on the file as it is now.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* // node:board/merge, defined by the const `merge` of nodes/merge.ts.
|
|
43
|
+
* const anchor: Anchor = { path: "nodes/merge.ts", binding: "merge" };
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
type Anchor = {
|
|
47
|
+
/** The file, root-relative POSIX. */path: string; /** The const, function or export the definition is bound to. */
|
|
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
|
+
*/
|
|
58
|
+
component?: string; /** JSX only: how the key is written. */
|
|
59
|
+
kind?: JsxKind; /** JSX templates and identifiers: the literal head of the pattern, `card` for `card*Picture`. */
|
|
60
|
+
stem?: string; /** JSX idProp: the prop the value sits on, `id` or `amountKey`. */
|
|
61
|
+
prop?: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The index of one game at one moment: every key with its anchors, every indexed file with its
|
|
65
|
+
* hash, and what could not be read without guessing. Computed on demand, never written to disk.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* index.symbols["flow:board"]; // { def: [{ path: "flows/board.ts", binding: "boardFlow" }] }
|
|
70
|
+
* index.files["nodes/merge.ts"]?.state; // "ok"
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
type ProjectIndex = {
|
|
74
|
+
/** 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. */
|
|
75
|
+
revision: string; /** The asset manifest, root-relative, when that file exists. */
|
|
76
|
+
manifest?: string;
|
|
77
|
+
/**
|
|
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.
|
|
80
|
+
*/
|
|
81
|
+
symbols: Record<string, {
|
|
82
|
+
def: Anchor[];
|
|
83
|
+
uses?: Anchor[];
|
|
84
|
+
conflict?: true;
|
|
85
|
+
}>; /** Every indexed file: the sha1 of its bytes, and `broken` with the first parse error. */
|
|
86
|
+
files: Record<string, {
|
|
87
|
+
hash: string;
|
|
88
|
+
state: "ok" | "broken";
|
|
89
|
+
error?: string;
|
|
90
|
+
}>; /** What a definer or a JSX key holds that cannot be read without running the game. */
|
|
91
|
+
unresolved: {
|
|
92
|
+
path: string;
|
|
93
|
+
reason: string;
|
|
94
|
+
}[];
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* What one watch batch changed: the new revision, the files whose bytes changed, the keys that
|
|
98
|
+
* left one file for another, and the keys gone from every file.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* // An agent moved nodes/toast.ts to nodes/board/toast.ts and fixed the import of the board flow.
|
|
103
|
+
* change.files; // ["flows/board.ts", "nodes/board/toast.ts", "nodes/toast.ts"]
|
|
104
|
+
* change.moved; // [{ key: "node:board/toast", from: "nodes/toast.ts", to: "nodes/board/toast.ts" }]
|
|
105
|
+
* change.removed; // []
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
type ProjectChange = {
|
|
109
|
+
/** The revision of the index after the batch. */revision: string; /** The root-relative paths whose bytes changed, appeared or vanished, sorted. */
|
|
110
|
+
files: string[]; /** A key that left one file and appeared in another in the same batch. */
|
|
111
|
+
moved: {
|
|
112
|
+
key: string;
|
|
113
|
+
from: string;
|
|
114
|
+
to: string;
|
|
115
|
+
}[]; /** Keys gone from every file. */
|
|
116
|
+
removed: string[];
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* One answer of `find`: the anchor with its line read from the file now. Lines and columns are
|
|
120
|
+
* 1-based; the end column is exclusive. A binding answers its declaration statement, a text style
|
|
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.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* // The editor opens the Home scene of the fixture game.
|
|
127
|
+
* const [found] = await project.find("scene:home");
|
|
128
|
+
* [found?.path, found?.line, found?.range]; // ["features/home/scene.ts", 8, [8, 1, 13, 4]]
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
type Found = Anchor & {
|
|
132
|
+
/** The line, 1-based. */line: number; /** Start line, start column, end line, end column; 1-based, the end column exclusive. */
|
|
133
|
+
range: [startLine: number, startColumn: number, endLine: number, endColumn: number]; /** sha1 hex of the bytes the line was read from. */
|
|
134
|
+
hash: string; /** The file does not parse now; the line comes from its last good parse. */
|
|
135
|
+
broken?: true;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* The handle `openProject` returns: one index of one game root, kept fresh by one watcher, and
|
|
139
|
+
* the lines of a key read from disk at the call. Close it when the game closes.
|
|
140
|
+
*/
|
|
141
|
+
type ProjectApi = {
|
|
142
|
+
/**
|
|
143
|
+
* The current index. A watch batch or `changed` replaces it with a new frozen object.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* // The editor checks a node file before it opens it.
|
|
148
|
+
* project.index.files["nodes/merge.ts"]?.state; // "ok"
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
readonly index: ProjectIndex;
|
|
152
|
+
/**
|
|
153
|
+
* Answers where a key is defined, with lines read from the file on disk now. A file whose bytes
|
|
154
|
+
* changed since the index was built is parsed again for the answer; the index itself stays as
|
|
155
|
+
* it is until the next watch batch. A JSX key the game reports at run time also finds the
|
|
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 `[]`.
|
|
159
|
+
*
|
|
160
|
+
* @param key - An engine id: `node:board/merge`, `flow:board`, `textStyle:ui.title`, `jsx:card2Picture`.
|
|
161
|
+
* @returns One entry per place, in the order above; `broken` when the file does not parse now.
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* // The editor jumps to the node the flow view clicked, and to the X of the settings board.
|
|
165
|
+
* (await project.find("node:board/merge"))[0]?.line; // 17, in "nodes/merge.ts"
|
|
166
|
+
* (await project.find("jsx:settingsBoardClose")).map(found => `${found.path}:${found.line}`);
|
|
167
|
+
* // ["features/ui/kit.tsx:844", "features/settings/settings.tsx:290"]
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
find(key: string): Promise<Found[]>;
|
|
171
|
+
/**
|
|
172
|
+
* Calls back once per batch of file changes that changed the index, with the new index and
|
|
173
|
+
* what changed. Events only start a batch: when the quiet period ends, the root is walked and
|
|
174
|
+
* every file whose size or time stamp moved is hashed; a save with the same bytes changes
|
|
175
|
+
* nothing and calls nobody. The handle also walks once when watching starts and every two
|
|
176
|
+
* seconds after, for what the platform watcher missed. Every caller shares the one watcher of
|
|
177
|
+
* the handle. An error thrown by a callback is dropped, so the other callers still hear the
|
|
178
|
+
* batch.
|
|
179
|
+
*
|
|
180
|
+
* @param onIndex - Gets the new index and the change of the batch.
|
|
181
|
+
* @returns Removes this caller; the watcher stops with the last one.
|
|
182
|
+
* @throws {Error} When the handle is closed.
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* // The editor refreshes its flow pages when an agent saves a game file.
|
|
186
|
+
* const stop = project.watch((index, change) => pages.refresh(index.revision, change.moved));
|
|
187
|
+
* stop(); // this caller hears no more batches
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
watch(onIndex: (index: ProjectIndex, change: ProjectChange) => void): () => void;
|
|
191
|
+
/**
|
|
192
|
+
* Re-indexes one root-relative path now, for a caller that watches files itself. A deleted
|
|
193
|
+
* file drops its entries; a path that is not a `.ts` or `.tsx` source of the index changes
|
|
194
|
+
* nothing. The watch callers are not called.
|
|
195
|
+
*
|
|
196
|
+
* @param path - The root-relative POSIX path of the file.
|
|
197
|
+
* @returns The index after the file was read.
|
|
198
|
+
* @throws {Error} When the path leaves the root, by `..` or by a symlink.
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* // The editor saved nodes/merge.ts through its own file layer.
|
|
202
|
+
* const index = await project.changed("nodes/merge.ts");
|
|
203
|
+
* index.files["nodes/merge.ts"]?.state; // "ok"
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
changed(path: string): Promise<ProjectIndex>;
|
|
207
|
+
/**
|
|
208
|
+
* Stops the watcher and drops every watch caller. `find` and `changed` keep working.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```ts
|
|
212
|
+
* // The editor closes the game.
|
|
213
|
+
* project.close(); // no more batches; project.watch(...) now throws
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
close(): void;
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* The part of the branded console the `moku-game-index` command line writes through. A
|
|
220
|
+
* `BrandConsole` of `@moku-labs/common/cli` fits it; a test passes a recorder.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* // A test records what the command printed.
|
|
225
|
+
* const lines: string[] = [];
|
|
226
|
+
* const ui: IndexUi = {
|
|
227
|
+
* line: text => lines.push(text),
|
|
228
|
+
* info: text => lines.push(text),
|
|
229
|
+
* error: text => lines.push(text)
|
|
230
|
+
* };
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
type IndexUi = {
|
|
234
|
+
/** 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. */
|
|
235
|
+
info(message: string): void; /** Writes one error line: a broken file, a conflict, an unknown key, a bad flag. */
|
|
236
|
+
error(message: string): void;
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/project/cli.d.ts
|
|
240
|
+
/**
|
|
241
|
+
* Runs `moku-game-index`: opens the project of `--root` and runs one command. Nothing here calls
|
|
242
|
+
* `process.exit`; the caller does.
|
|
243
|
+
*
|
|
244
|
+
* @param argv - The arguments after the script name.
|
|
245
|
+
* @param ui - Where the lines go. The branded console by default.
|
|
246
|
+
* @returns The exit code: `1` for a bad flag, a root that cannot be opened, an unknown key, or a
|
|
247
|
+
* `--check` that found a broken file or a conflict; else `0`.
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* // An agent asks where a node of the fixture game lives.
|
|
251
|
+
* await runCli(["--root", "tests/integration/merge-game", "where", "node:board/merge"]);
|
|
252
|
+
* // 0, and the console printed "nodes/merge.ts:17"
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
declare function runCli(argv: readonly string[], ui?: IndexUi): Promise<number>;
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/project/open.d.ts
|
|
258
|
+
/**
|
|
259
|
+
* Opens the project index of a game: checks the root (it exists, it is a directory, its real
|
|
260
|
+
* path), loads TypeScript, parses every source file and builds the index. Nothing is written to
|
|
261
|
+
* disk and nothing is inferred from the working directory but the root itself.
|
|
262
|
+
*
|
|
263
|
+
* @param options - The game root, the manifest path and the quiet period of `watch`.
|
|
264
|
+
* @returns The handle; close it when the game closes.
|
|
265
|
+
* @throws {Error} When the root is missing or not a directory, or the `typescript` package is not
|
|
266
|
+
* installed: `[game] The project index needs the "typescript" package.`
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* // The editor opens the fixture game and answers where a node and a text style live.
|
|
270
|
+
* const project = await openProject({ root: "tests/integration/merge-game" });
|
|
271
|
+
* Object.keys(project.index.files).length; // 111
|
|
272
|
+
* (await project.find("textStyle:ui.title"))[0]?.line; // 39, in "features/ui/styles.ts"
|
|
273
|
+
* project.close();
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
declare function openProject(options: ProjectOptions): Promise<ProjectApi>;
|
|
277
|
+
//#endregion
|
|
278
|
+
export { type Anchor, type Found, type IndexUi, type JsxKind, type ProjectApi, type ProjectChange, type ProjectIndex, type ProjectOptions, openProject, runCli };
|