@moku-labs/game 0.14.12 → 0.14.13

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.
Files changed (56) hide show
  1. package/README.md +13 -12
  2. package/dist/app/page.d.mts +8 -3
  3. package/dist/app/page.mjs +57 -12
  4. package/dist/app/system.d.mts +5 -3
  5. package/dist/app/system.mjs +2 -0
  6. package/dist/app.d.mts +15 -7
  7. package/dist/app.mjs +131 -27
  8. package/dist/{assets-BrKSg1O6.mjs → assets-CEMfBogh.mjs} +471 -120
  9. package/dist/assets.d.mts +30 -13
  10. package/dist/assets.mjs +1 -1
  11. package/dist/cli.d.mts +105 -8
  12. package/dist/cli.mjs +1036 -128
  13. package/dist/{commands-BaCx2B9j.d.mts → commands-DAETmK86.d.mts} +43 -7
  14. package/dist/{config-CdARrrhU.mjs → config-B87aFTW-.mjs} +25 -4
  15. package/dist/control.d.mts +130 -17
  16. package/dist/control.mjs +4 -4
  17. package/dist/{define-Ce8SVEN-.d.mts → define-BASd9ZUh.d.mts} +32 -4
  18. package/dist/dev-DbHgx-R-.mjs +89 -0
  19. package/dist/{fake-MpzwZhIZ.mjs → fake-6mazmHK3.mjs} +20 -2
  20. package/dist/features-DowEilZm.mjs +266 -0
  21. package/dist/{headless-CMHNsKjW.d.mts → headless-cFveciuY.d.mts} +1 -1
  22. package/dist/hot.d.mts +4 -2
  23. package/dist/hot.mjs +7 -4
  24. package/dist/{index-CLQ8NOTw.d.mts → index-DedbED3T.d.mts} +317 -69
  25. package/dist/index.d.mts +5 -6
  26. package/dist/index.mjs +3 -3
  27. package/dist/inspect.d.mts +12 -7
  28. package/dist/inspect.mjs +2 -2
  29. package/dist/jsx-dev-runtime.d.mts +1 -1
  30. package/dist/jsx-runtime.d.mts +1 -1
  31. package/dist/lint.d.mts +8 -3
  32. package/dist/lint.mjs +432 -20
  33. package/dist/project.d.mts +15 -1
  34. package/dist/project.mjs +1 -1
  35. package/dist/{run-Bf2hKyjZ.mjs → run-BbipFRc6.mjs} +250 -51
  36. package/dist/{runtime-BCnKn0xf.d.mts → runtime-DoSlRx8X.d.mts} +1 -1
  37. package/dist/{session-DgukhR_7.mjs → session-CeKgO2hy.mjs} +30 -2
  38. package/dist/{sources-Duzj6lhV.mjs → sources-Cdvb0rOS.mjs} +33 -6
  39. package/dist/{src-CtC8w3_z.mjs → src-BEbJ_sSx.mjs} +35536 -17789
  40. package/dist/testing.d.mts +23 -13
  41. package/dist/testing.mjs +10 -6
  42. package/dist/{types-DOlpeuGx.d.mts → types-7GXcXxxv.d.mts} +174 -242
  43. package/dist/{types-BjTtGSFb.d.mts → types-BGcda5Le.d.mts} +4459 -732
  44. package/dist/{types-gisvSHoC.d.mts → types-C73hbmxi.d.mts} +129 -17
  45. package/dist/{types-CAPv9l29.d.mts → types-DngFQJro.d.mts} +43 -5
  46. package/dist/{types-Y_rCuXtj.d.mts → types-GHE4J5Uf.d.mts} +1025 -191
  47. package/dist/{visual-dMW8iqAE.mjs → visual-2m4wJL_z.mjs} +111 -7
  48. package/dist/visual-_XhoGeIC.d.mts +345 -0
  49. package/dist/visual.d.mts +1 -332
  50. package/dist/visual.mjs +130 -20
  51. package/dist/{watch-CWI_on4T.mjs → watch-BEdrp80i.mjs} +149 -30
  52. package/llms.txt +99 -48
  53. package/package.json +8 -6
  54. package/dist/dev-Cc50Dk7H.mjs +0 -47
  55. package/dist/memory-pUQ7YZWC.d.mts +0 -50
  56. package/dist/types-BX4Ge_j5.d.mts +0 -656
@@ -0,0 +1,266 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ //#region src/project/features.ts
4
+ /** The folders that hold one kind of code or resource of a feature: never a sub-feature, whatever they hold. */
5
+ const KIND_FOLDER = /^(?:flow|views|rules|styles|motion|world|effects|nodes|assets|strings|content|__tests__)$/;
6
+ /** The file names of a door, in the order a door is looked for. */
7
+ const DOORS = ["index.ts", "index.tsx"];
8
+ /** A `//` comment to the end of the line or a block comment, whichever starts first. */
9
+ const COMMENT = /\/\*[\s\S]*?\*\/|\/\/[^\n]*/g;
10
+ /** An import declaration: the text up to its source (the clause and `from`), and the source. */
11
+ const IMPORT = /\bimport\b([^;"']*)["']([^"']+)["']/g;
12
+ /** A `features: [ … ]` property, key bare or quoted, up to the first `]`. */
13
+ const LISTING = /(?:\bfeatures|"features"|'features')\s*:\s*\[([^\]]*)\]/g;
14
+ /** An identifier. */
15
+ const IDENTIFIER = /[$A-Z_a-z][\w$]*/g;
16
+ /** A source naming a folder beside the door: `./<folder>`, `./<folder>/index`, either with `.ts`, `.tsx` or `.js`. */
17
+ const FOLDER_SOURCE = /^\.\/([^/]+?)(?:\/index)?(?:\.(?:tsx?|js))?$/;
18
+ /**
19
+ * Finds every feature owner of a game: each folder of the features folder, and inside an owner each folder (not a kind
20
+ * folder such as `flow/` or `assets/`) with a door `index.ts` and resources of its own, at any depth. Depth first, a
21
+ * parent before its children, siblings sorted as the asset scanner sorts features. Reads folders only, never a door.
22
+ *
23
+ * @param root - The game folder.
24
+ * @param features - The name of the folder that holds the features. Default `"features"`.
25
+ * @returns The owners; none when the features folder is missing.
26
+ * @throws {Error} When one feature name belongs to two folders; the message names both.
27
+ * @example
28
+ * ```ts
29
+ * // tests/fixtures/episodes-game
30
+ * findFeatureOwners("tests/fixtures/episodes-game").map(owner => owner.dir);
31
+ * // ["features/episodes", "features/episodes/ep01", "features/episodes/ep02",
32
+ * // "features/episodes/episodes-kit", "features/home"]
33
+ * ```
34
+ */
35
+ function findFeatureOwners(root, features = "features") {
36
+ return walk(root, features, false);
37
+ }
38
+ /**
39
+ * Finds every feature folder lint and the project index treat as a unit: the owners of `findFeatureOwners`, and
40
+ * inside an owner each code-only folder with a door that the owner's door imports from `./<folder>` and lists in
41
+ * `features: [...]`. Reads the owners' doors as text, never with TypeScript. Same order and the same name check as
42
+ * `findFeatureOwners`. A code-only sub-feature declares no sub-features: its own folders are never searched.
43
+ *
44
+ * @param root - The game folder.
45
+ * @param features - The name of the folder that holds the features. Default `"features"`.
46
+ * @returns The owners and the code-only sub-features; none when the features folder is missing.
47
+ * @throws {Error} When one feature name belongs to two folders; the message names both.
48
+ * @example
49
+ * ```ts
50
+ * // features/episodes/index.ts imports { toolsFeature } from "./tools" and lists it in features: [...];
51
+ * // features/episodes/tools/ has an index.ts and code only.
52
+ * findFeatureUnits("game").find(unit => unit.name === "tools");
53
+ * // { name: "tools", dir: "features/episodes/tools", parent: "episodes", depth: 1, resources: { assets: false, strings: false } }
54
+ * ```
55
+ */
56
+ function findFeatureUnits(root, features = "features") {
57
+ return walk(root, features, true);
58
+ }
59
+ /**
60
+ * The owner a root-relative path belongs to: the one with the longest `dir` that is the path or a folder above it.
61
+ *
62
+ * @param owners - The owners of `findFeatureOwners`, or the units of `findFeatureUnits`.
63
+ * @param file - A root-relative POSIX path, a file or a folder, with or without an extension.
64
+ * @returns The deepest owner holding it; `undefined` outside every owner.
65
+ * @example
66
+ * ```ts
67
+ * ownerOf([{ dir: "features/episodes" }, { dir: "features/episodes/ep01" }], "features/episodes/ep01/flow/story.ts")?.dir; // "features/episodes/ep01"
68
+ * ```
69
+ */
70
+ function ownerOf(owners, file) {
71
+ let deepest;
72
+ for (const owner of owners) if ((file === owner.dir || file.startsWith(`${owner.dir}/`)) && (deepest === void 0 || owner.dir.length > deepest.dir.length)) deepest = owner;
73
+ return deepest;
74
+ }
75
+ /**
76
+ * Walks the features folder: each of its folders is an owner, and below each owner its sub-features.
77
+ *
78
+ * @param root - The game folder.
79
+ * @param features - The features folder, relative to the root.
80
+ * @param units - True to add the code-only sub-features the owners' doors list.
81
+ * @returns The owners (and units), depth first, siblings sorted.
82
+ * @throws {Error} When one feature name belongs to two folders.
83
+ */
84
+ function walk(root, features, units) {
85
+ const top = path.join(root, features);
86
+ const base = features.replaceAll("\\", "/");
87
+ const found = [];
88
+ for (const entry of sorted(list(top).filter((item) => item.isDirectory()))) {
89
+ const folder = path.join(top, entry.name);
90
+ const entries = list(folder);
91
+ collect(found, {
92
+ name: entry.name,
93
+ dir: path.posix.join(base, entry.name),
94
+ depth: 0,
95
+ resources: resourcesOf(entries)
96
+ }, folder, entries, units);
97
+ }
98
+ checkNames(found);
99
+ return found;
100
+ }
101
+ /**
102
+ * Adds an owner, then its sub-features in sibling order: an owner sub-feature with its own sub-features, a code-only
103
+ * one its door lists as a leaf.
104
+ *
105
+ * @param found - The list to add to, in walk order.
106
+ * @param owner - The owner to add.
107
+ * @param folder - The owner's folder on disk.
108
+ * @param entries - The entries of that folder.
109
+ * @param units - True to add the code-only sub-features the door lists.
110
+ */
111
+ function collect(found, owner, folder, entries, units) {
112
+ found.push(owner);
113
+ const listed = units ? listedIn(folder, entries) : [];
114
+ for (const child of sorted(entries.filter((entry) => isCandidate(entry)))) {
115
+ const childFolder = path.join(folder, child.name);
116
+ const childEntries = list(childFolder);
117
+ if (doorOf(childEntries) === void 0) continue;
118
+ const resources = resourcesOf(childEntries);
119
+ const sub = {
120
+ name: child.name,
121
+ dir: `${owner.dir}/${child.name}`,
122
+ parent: owner.name,
123
+ depth: owner.depth + 1
124
+ };
125
+ if (resources.assets || resources.strings) collect(found, {
126
+ ...sub,
127
+ resources
128
+ }, childFolder, childEntries, units);
129
+ else if (listed.includes(child.name)) found.push({
130
+ ...sub,
131
+ resources
132
+ });
133
+ }
134
+ }
135
+ /**
136
+ * Tells whether a folder entry may be a sub-feature: a folder, not a kind folder, not a dot folder.
137
+ *
138
+ * @param entry - An entry of an owner's folder.
139
+ * @returns True when the entry is worth a look.
140
+ */
141
+ function isCandidate(entry) {
142
+ return entry.isDirectory() && !KIND_FOLDER.test(entry.name) && !entry.name.startsWith(".");
143
+ }
144
+ /**
145
+ * Lists a folder; a folder that is missing, a file or unreadable counts as empty.
146
+ *
147
+ * @param folder - The folder on disk.
148
+ * @returns Its entries.
149
+ */
150
+ function list(folder) {
151
+ try {
152
+ return readdirSync(folder, { withFileTypes: true });
153
+ } catch {
154
+ return [];
155
+ }
156
+ }
157
+ /**
158
+ * Sorts folder entries by name, with the comparator the asset scanner sorts features with.
159
+ *
160
+ * @param entries - The entries.
161
+ * @returns A sorted copy.
162
+ */
163
+ function sorted(entries) {
164
+ return entries.toSorted((left, right) => left.name.localeCompare(right.name));
165
+ }
166
+ /**
167
+ * The door of a folder: `index.ts`, else `index.tsx`.
168
+ *
169
+ * @param entries - The entries of the folder.
170
+ * @returns The door's file name; `undefined` without one.
171
+ */
172
+ function doorOf(entries) {
173
+ return DOORS.find((door) => entries.some((entry) => entry.isFile() && entry.name === door));
174
+ }
175
+ /**
176
+ * What a folder brings of its own.
177
+ *
178
+ * @param entries - The entries of the folder.
179
+ * @returns Whether it has `assets/` or `assets.ts`, and whether it has `strings/`.
180
+ */
181
+ function resourcesOf(entries) {
182
+ const has = (name, folder) => entries.some((entry) => entry.name === name && (folder ? entry.isDirectory() : entry.isFile()));
183
+ return {
184
+ assets: has("assets", true) || has("assets.ts", false),
185
+ strings: has("strings", true)
186
+ };
187
+ }
188
+ /**
189
+ * The folders an owner's door imports and lists in `features: [...]`. A door that cannot be read lists nothing.
190
+ *
191
+ * @param folder - The owner's folder on disk.
192
+ * @param entries - The entries of that folder.
193
+ * @returns The listed folder names.
194
+ */
195
+ function listedIn(folder, entries) {
196
+ const door = doorOf(entries);
197
+ if (door === void 0) return [];
198
+ try {
199
+ return listedFolders(readFileSync(path.join(folder, door), "utf8"));
200
+ } catch {
201
+ return [];
202
+ }
203
+ }
204
+ /**
205
+ * The folders a door imports from `./<folder>` (not `import type`) and names in a `features: [...]` array, comments
206
+ * removed first. A text scan, not a parse.
207
+ *
208
+ * @param doorText - The text of the door.
209
+ * @returns The folder names, in import order.
210
+ * @example listedFolders('import { kitFeature } from "./kit";\nexport const f = defineFeature("a", { features: [kitFeature] });'); // ["kit"]
211
+ */
212
+ function listedFolders(doorText) {
213
+ const text = doorText.replaceAll(COMMENT, "");
214
+ const listed = /* @__PURE__ */ new Set();
215
+ for (const [, items = ""] of text.matchAll(LISTING)) for (const [identifier] of items.matchAll(IDENTIFIER)) listed.add(identifier);
216
+ const folders = /* @__PURE__ */ new Set();
217
+ for (const [, head = "", source = ""] of text.matchAll(IMPORT)) {
218
+ const folder = FOLDER_SOURCE.exec(source)?.[1];
219
+ const clause = head.trim().replace(/from$/, "");
220
+ if (folder === void 0 || /^type\s/.test(clause)) continue;
221
+ if (localNames(clause).some((name) => listed.has(name))) folders.add(folder);
222
+ }
223
+ return [...folders];
224
+ }
225
+ /**
226
+ * The local names an import clause binds at runtime: the default import, the namespace import and each named
227
+ * specifier that is not `type`-only (`a as b` gives `b`).
228
+ *
229
+ * @param clause - The text between `import` and `from`.
230
+ * @returns The local names.
231
+ * @example localNames("main, { a as b, type c, d }"); // ["b", "d", "main"]
232
+ */
233
+ function localNames(clause) {
234
+ const open = clause.indexOf("{");
235
+ const close = clause.indexOf("}");
236
+ const named = open === -1 ? [] : clause.slice(open + 1, close).split(",");
237
+ const outside = open === -1 ? clause : clause.slice(0, open) + clause.slice(close + 1);
238
+ const names = [];
239
+ for (const part of [...named, ...outside.split(",")]) {
240
+ const words = part.trim().split(/\s+/);
241
+ if (words[0] === "type" && words.length > 1) continue;
242
+ const name = words.at(-1);
243
+ if (name) names.push(name);
244
+ }
245
+ return names;
246
+ }
247
+ /**
248
+ * Refuses a feature name used by two folders, before anything is returned. The first folder is the earlier one in
249
+ * walk order.
250
+ *
251
+ * @param found - The owners (and units), in walk order.
252
+ * @throws {Error} For the first name met twice; the message names both folders and a free name.
253
+ */
254
+ function checkNames(found) {
255
+ const seen = /* @__PURE__ */ new Map();
256
+ for (const owner of found) {
257
+ const first = seen.get(owner.name);
258
+ if (first !== void 0) {
259
+ const parent = owner.parent ?? first.parent;
260
+ throw new Error(`[game] features: the feature name "${owner.name}" is taken by two folders: ${first.dir} and ${owner.dir}.\n A feature is named like its folder, once in the game. Name a shared sub-feature after its parent: ${parent}-${owner.name}.`);
261
+ }
262
+ seen.set(owner.name, owner);
263
+ }
264
+ }
265
+ //#endregion
266
+ export { findFeatureUnits as n, ownerOf as r, findFeatureOwners as t };
@@ -1,4 +1,4 @@
1
- import { Mt as JournalEntry, Pt as RouteStep, Vt as Answer, Zt as Json, dt as Api$1, gn as Api, jt as FlowState, ln as RngState } from "./types-BjTtGSFb.mjs";
1
+ import { Ar as Api, Cr as RngState, Gn as RouteStep, Hn as FlowState, Un as JournalEntry, bn as Api$1, er as Answer, ur as Json } from "./types-BGcda5Le.mjs";
2
2
 
3
3
  //#region src/plugins/flow/headless.d.ts
4
4
  /**
package/dist/hot.d.mts CHANGED
@@ -16,7 +16,8 @@ type HotOptions = {
16
16
  * The files that get the footer. Default: `.tsx` files, `styles.ts`, `view.ts`,
17
17
  * `animations.ts`, `effects.ts`, any `.ts` directly in a `styles/`, `motion/`, `effects/` or
18
18
  * `views/` folder, and in `world/projections/` or `world/layout/`,
19
- * `generated/strings.<locale>.ts`, and the stamp module `.moku/assets-stamp.ts`. Bun runs it as
19
+ * `generated/strings.<locale>.ts` and `generated/strings.<feature>.<locale>.ts`, and the stamp
20
+ * module `.moku/assets-stamp.ts`. Bun runs it as
20
21
  * the load filter on the absolute path, so it should not anchor at the start.
21
22
  */
22
23
  include?: RegExp;
@@ -25,7 +26,8 @@ type HotOptions = {
25
26
  * `node_modules/`, `web/`, `generated/`, `__tests__/` or `tests/`, and `.test`/`.spec` files.
26
27
  * Tested on the path relative to the game root with a leading `/` and `/` separators, also on
27
28
  * Windows: `/features/home/view.tsx`. A folder above the game root never excludes a file. A
28
- * `generated/strings.<locale>.ts` file is never excluded: the generated strings swap.
29
+ * `generated/strings.<locale>.ts` and a `generated/strings.<feature>.<locale>.ts` file are never
30
+ * excluded: the generated strings swap.
29
31
  */
30
32
  exclude?: RegExp;
31
33
  };
package/dist/hot.mjs CHANGED
@@ -10,7 +10,7 @@ import { readFile } from "node:fs/promises";
10
10
  * the keys watch names. A view module is a `.tsx` file, a `styles.ts`, a `view.ts`, an
11
11
  * `animations.ts`, an `effects.ts`, any `.ts` directly in a `styles/`, `motion/`, `effects/` or
12
12
  * `views/` folder, any `.ts` directly in `world/projections/` or `world/layout/`, or a generated
13
- * `generated/strings.<locale>.ts`. The stamp module `.moku/assets-stamp.ts` gets the footer too:
13
+ * `generated/strings.<locale>.ts` or `generated/strings.<feature>.<locale>.ts`. The stamp module `.moku/assets-stamp.ts` gets the footer too:
14
14
  * its new exports say which asset files changed. A logic module never gets the footer
15
15
  * (`world/components/` and `world/systems/` included: they are registered at start, and
16
16
  * `.moku/main.ts` too), so its save reaches the root and the page reloads and restores its state. `moku-game dev` lists the
@@ -21,9 +21,12 @@ import { readFile } from "node:fs/promises";
21
21
  * The default modules that swap: the view files by name or by kind folder, and the stamp module.
22
22
  * Three literals joined, so each stays readable.
23
23
  */
24
- const DEFAULT_INCLUDE = new RegExp(`(${/\.tsx|\/styles\.ts|\/view\.ts|\/animations\.ts|\/effects\.ts|\/generated\/strings\.[\w-]+\.ts/.source}|${/\/(?:styles|motion|effects|views)\/[^/]+\.ts|\/world\/(?:projections|layout)\/[^/]+\.ts/.source}|${/\/\.moku\/assets-stamp\.ts/.source})$`);
25
- /** The generated strings of one locale, the one file under `generated/` that swaps. */
26
- const STRINGS = /\/generated\/strings\.[\w-]+\.ts$/;
24
+ const DEFAULT_INCLUDE = new RegExp(`(${/\.tsx|\/styles\.ts|\/view\.ts|\/animations\.ts|\/effects\.ts|\/generated\/strings\.(?:[\w-]+\.)?[\w-]+\.ts/.source}|${/\/(?:styles|motion|effects|views)\/[^/]+\.ts|\/world\/(?:projections|layout)\/[^/]+\.ts/.source}|${/\/\.moku\/assets-stamp\.ts/.source})$`);
25
+ /**
26
+ * The generated strings of one locale, whole-game or of one sub-feature: the files under
27
+ * `generated/` that swap.
28
+ */
29
+ const STRINGS = /\/generated\/strings\.(?:[\w-]+\.)?[\w-]+\.ts$/;
27
30
  /** The default folders and test files that never hot swap. The outer group only scopes the `$`. */
28
31
  const DEFAULT_EXCLUDE = /(\/(node_modules|web|generated|__tests__|tests)\/|\.(test|spec)\.tsx?$)/;
29
32
  /**