@moku-labs/game 0.7.1 → 0.9.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.
Files changed (40) hide show
  1. package/README.md +12 -8
  2. package/dist/assets.d.mts +97 -33
  3. package/dist/assets.mjs +350 -110
  4. package/dist/{commands-mzE9PX0H.d.mts → commands-DuwCxK5t.d.mts} +5 -4
  5. package/dist/control.d.mts +4 -3
  6. package/dist/control.mjs +2 -2
  7. package/dist/{define-BbgXv4cy.d.mts → define-B6pHpdBi.d.mts} +4 -3
  8. package/dist/headless-BsNdJs9W.mjs +186 -0
  9. package/dist/headless-SCQXJxQf.d.mts +181 -0
  10. package/dist/hot.d.mts +4 -2
  11. package/dist/hot.mjs +8 -6
  12. package/dist/index-BO9kZzY3.d.mts +1611 -0
  13. package/dist/index.d.mts +6 -1596
  14. package/dist/index.mjs +4 -34334
  15. package/dist/{input-CK4nxBO1.mjs → input-z_35Jwfn.mjs} +2 -2
  16. package/dist/inspect.d.mts +7 -6
  17. package/dist/inspect.mjs +2 -2
  18. package/dist/jsx-dev-runtime.d.mts +1 -1
  19. package/dist/jsx-runtime.d.mts +1 -1
  20. package/dist/lint.d.mts +55 -4
  21. package/dist/lint.mjs +993 -40
  22. package/dist/project.d.mts +57 -41
  23. package/dist/project.mjs +994 -505
  24. package/dist/{run-Bcyg32M5.mjs → run-CofJEdYx.mjs} +4 -187
  25. package/dist/{runtime-HTlmRb81.d.mts → runtime-CdNJ0UBm.d.mts} +1 -1
  26. package/dist/{sources-DVWJ8nfr.mjs → sources-DNnzyCYf.mjs} +2 -2
  27. package/dist/src-536arXjV.mjs +34644 -0
  28. package/dist/testing.d.mts +85 -283
  29. package/dist/testing.mjs +158 -2109
  30. package/dist/{types-BMEn0tSC.d.mts → types-B0v5VVa3.d.mts} +10 -9
  31. package/dist/{types-ClF8say0.d.mts → types-CQueODzo.d.mts} +2 -2
  32. package/dist/{types-CYYKjvLn.d.mts → types-DUMoYPYe.d.mts} +5 -182
  33. package/dist/{types-CXyipKhE.d.mts → types-VZVWwqCg.d.mts} +42 -4
  34. package/dist/{types-BaUUcIaN.d.mts → types-xXfyk56Q.d.mts} +1 -1
  35. package/dist/{visual-DUvvPPCt.mjs → visual-dMW8iqAE.mjs} +8 -4
  36. package/dist/visual.d.mts +330 -0
  37. package/dist/visual.mjs +2142 -0
  38. package/llms.txt +59 -21
  39. package/package.json +7 -8
  40. package/dist/memory-CvgdnsQO.mjs +0 -259
package/README.md CHANGED
@@ -115,7 +115,7 @@ await game.walk([{ at: "home", intent: "roll" }, { at: "home", intent: "roll" }]
115
115
  app.model.store.snapshot().session; // { rolls: 2 }
116
116
  ```
117
117
 
118
- The full version, with a reset node and a vitest file, is in [docs/quick-start.md](./docs/quick-start.md). A bigger game lives in [`tests/integration/merge-game/`](./tests/integration/merge-game). It is a test fixture and is not published.
118
+ The full version, with a reset node and a vitest file, is in [docs/quick-start.md](./docs/quick-start.md). A bigger game, the merge game, lives in [moku-labs/demos](https://github.com/moku-labs/demos) with its tests.
119
119
 
120
120
  A game with a screen gets two more things from the package. The body font `fonts/font-body.*` (Pangolin, SIL OFL 1.1) is the default of the built-in text style `body` once copied into `src/features/ui/assets/`. The bin `moku-game-assets` writes the typed asset keys, the manifest and the strings. It runs under bun; with node only, run `bun node_modules/@moku-labs/game/dist/assets.mjs`. Both are in [docs/quick-start.md](./docs/quick-start.md#a-screen-the-body-font-and-the-asset-keys).
121
121
 
@@ -188,7 +188,8 @@ createApp({ plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin] });
188
188
  | Import | Runs in | For |
189
189
  |---|---|---|
190
190
  | `@moku-labs/game` | anywhere | The engine: plugins, helpers, components |
191
- | `@moku-labs/game/testing` | Node and Bun | Headless games and visual tests |
191
+ | `@moku-labs/game/testing` | anywhere | Headless and isolated games, the fake clock, the in-memory save |
192
+ | `@moku-labs/game/visual` | Node and Bun | Visual tests: baselines of state, describe and pixels |
192
193
  | `@moku-labs/game/assets` | Node and Bun | Asset keys, the manifest, strings, the pack |
193
194
  | `@moku-labs/game/inspect` | anywhere | Read a running game |
194
195
  | `@moku-labs/game/control` | dev builds | Drive a running game |
@@ -199,7 +200,7 @@ createApp({ plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin] });
199
200
 
200
201
  ### Lint for games
201
202
 
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.
203
+ `@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. Three layout rules check a game on the layered layout: the order of the layers, the doors of the features and the suffix of each test file.
203
204
 
204
205
  ```json
205
206
  // .oxlintrc.json
@@ -212,12 +213,15 @@ createApp({ plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin] });
212
213
  "moku-game/no-module-state": "error",
213
214
  "moku-game/determinism": "error",
214
215
  "moku-game/rules-siblings": "error",
215
- "moku-game/static-keys": "error"
216
+ "moku-game/static-keys": "error",
217
+ "moku-game/layer-imports": "error",
218
+ "moku-game/feature-door": "error",
219
+ "moku-game/test-suffix": "error"
216
220
  }
217
221
  }
218
222
  ```
219
223
 
220
- Each rule takes `{ "files": [...], "ignores": [...] }`. The defaults follow the template layout. See [Lint for games](./docs/lint.md).
224
+ Each rule takes `{ "files": [...], "ignores": [...] }`. `layer-imports`, `feature-door` and `rules-siblings` also take `root` and `tsconfig`; `test-suffix` takes `root` and `suffixes`. The defaults follow the layout of a game. See [Lint for games](./docs/lint.md).
221
225
 
222
226
  ## Docs
223
227
 
@@ -232,7 +236,7 @@ Each rule takes `{ "files": [...], "ignores": [...] }`. The defaults follow the
232
236
  | [Configuration](./docs/configuration.md) | Every config field and its default |
233
237
  | [Lint for games](./docs/lint.md) | The `moku-game` oxlint rules, their options and defaults |
234
238
  | [Project index](./docs/project-index.md) | `openProject`, the key scheme, `find` and `watch`, the `moku-game-index` bin |
235
- | [Testing](./docs/testing.md) | Headless and visual tests, all scripts, test layout, lint rules |
239
+ | [Testing](./docs/testing.md) | Headless, isolated and visual tests, all scripts, test layout, lint rules |
236
240
  | [`llms.txt`](./llms.txt) | The engine in one page, for an AI that writes a game |
237
241
 
238
242
  Each plugin has its own README, linked in the table above. The kernel is specified in the [Moku Core specification](https://github.com/moku-labs/core/tree/main/specification).
@@ -246,8 +250,8 @@ bun run lint # biome check . && eslint .
246
250
  bun run test # vitest, unit and integration
247
251
  bun run test:coverage # with the 90% threshold
248
252
  bun run validate # publint and attw
249
- bun run fixture:visual # visual tests of the fixture game
250
- bun run fixture:native # the fixture as a native app
253
+ bun run mini:pack # pack the mini game of tests/fixtures
254
+ bun run mini:visual # visual tests of the mini game
251
255
  bun run release # moku-release
252
256
  ```
253
257
 
package/dist/assets.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { l as Manifest } from "./types-BMEn0tSC.mjs";
1
+ import { l as Manifest } from "./types-B0v5VVa3.mjs";
2
2
 
3
3
  //#region src/plugins/assets/scan/cli.d.ts
4
4
  /**
@@ -19,18 +19,20 @@ type StringsReport = {
19
19
  };
20
20
  /**
21
21
  * Compiles the feature strings of a root into a folder, or checks them in a check run; with
22
- * `pseudo` it writes the pseudo-locale `en-XA` too. `compileStrings` of i18n fits it, and a test
23
- * passes a stub instead.
22
+ * `pseudo` it writes the pseudo-locale `en-XA` too, and with `layers` it walks the `strings/` of
23
+ * every layer as well. `compileStrings` of i18n fits it, and a test passes a stub instead.
24
24
  *
25
25
  * @example
26
26
  * ```ts
27
27
  * // A test of a wrapper script: a game with one English key.
28
28
  * const compile: StringsCompiler = async () => ({ changed: false, locales: ["en"], keys: ["hud.title"], notes: [] });
29
+ * // runCli passes { check: false, pseudo: true, layers: { shared: "ui" } } for "--pseudo --layer shared=ui"
29
30
  * ```
30
31
  */
31
32
  type StringsCompiler = (root: string, out: string, options: {
32
33
  check: boolean;
33
34
  pseudo: boolean;
35
+ layers?: Readonly<Record<string, string>>;
34
36
  }) => Promise<StringsReport>;
35
37
  /**
36
38
  * Writes one exchange file per locale for the translators into a folder, and reports how many
@@ -44,6 +46,7 @@ type StringsCompiler = (root: string, out: string, options: {
44
46
  */
45
47
  type StringsExporter = (root: string, dir: string, options: {
46
48
  source: string;
49
+ layers?: Readonly<Record<string, string>>;
47
50
  }) => Promise<{
48
51
  locales: readonly string[];
49
52
  missing: Readonly<Record<string, number>>;
@@ -63,6 +66,7 @@ type StringsImporter = (root: string, dir: string, options: {
63
66
  source: string;
64
67
  pseudo: boolean;
65
68
  out: string;
69
+ layers?: Readonly<Record<string, string>>;
66
70
  }) => Promise<{
67
71
  locales: readonly string[];
68
72
  keys: number;
@@ -90,8 +94,10 @@ type StringsTools = {
90
94
  * compile the feature strings next to the key module, or check that all of it is current. With
91
95
  * `--pack <dir>` it packs the art for production on the same scan (`--no-cache` for a cold run);
92
96
  * `--pseudo` adds the pseudo-locale to a dev run. `--export <dir>` and `--import <dir>` exchange
93
- * the strings with translators and run alone, from the locale of `--source`. Nothing here calls
94
- * `process.exit`; the caller does.
97
+ * the strings with translators and run alone, from the locale of `--source`.
98
+ * `--layer <folder>[=<name>]` (repeatable) scans a folder under the root like one more feature,
99
+ * its keys and bundles under `<name>` (`<folder>` by default); it goes with every mode. Nothing
100
+ * here calls `process.exit`; the caller does.
95
101
  *
96
102
  * @param argv - The arguments after the script name.
97
103
  * @param strings - The string tools of i18n: compile, export and import.
@@ -105,6 +111,8 @@ type StringsTools = {
105
111
  * // 0, and dist/assets holds the pages, the loose files and the packed manifest.json
106
112
  * await runCli(["--root", "src", "--export", "translations"], strings);
107
113
  * // 0, and translations/ holds one <locale>.json per locale for the translators
114
+ * await runCli(["--root", "src", "--layer", "shared=ui"], strings);
115
+ * // 0, shared/assets/* keyed ui.*, shared/strings/* compiled
108
116
  * ```
109
117
  */
110
118
  declare function runCli(argv: string[], strings: StringsTools, ui?: ScanUi): Promise<number>;
@@ -121,36 +129,56 @@ type CompileReport = {
121
129
  };
122
130
  /**
123
131
  * What one compile is told beyond its two paths.
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * // A v15 game: the dev build writes en-XA, and shared/strings/ compiles next to the features.
136
+ * const options: CompileOptions = { pseudo: true, layers: { shared: "ui" } };
137
+ * ```
124
138
  */
125
139
  type CompileOptions = {
126
140
  /** True for a check run: nothing is written. */check?: boolean; /** Name of the folder that holds the features. Default `"features"`. */
127
141
  features?: string; /** True to also write `strings.en-XA.ts`, the pseudo-locale, from the `"en"` messages. */
128
142
  pseudo?: boolean;
143
+ /**
144
+ * Layers walked next to the features: folder under `root` to the name its keys take.
145
+ * `{ shared: "ui" }` reads `<root>/shared/strings/<locale>.json` exactly as a feature's
146
+ * `strings/` is read. A folder is one name, without "/", "\" or ".". Default `{}`: features
147
+ * only.
148
+ */
149
+ layers?: Readonly<Record<string, string>>;
129
150
  };
130
151
  /**
131
- * Walks `features/*​/strings/<locale>.json` of a game, compiles every message and writes the
132
- * generated modules: the types module `strings.ts` and one `strings.<locale>.ts` per locale, plus
133
- * `strings.en-XA.ts` with `pseudo`. A check run writes nothing and only reports whether something
134
- * would change; it checks `strings.en-XA.ts` only with `pseudo`.
152
+ * Walks `features/*​/strings/<locale>.json` of a game and the `strings/` of every layer, compiles
153
+ * every message and writes the generated modules: the types module `strings.ts` and one
154
+ * `strings.<locale>.ts` per locale, plus `strings.en-XA.ts` with `pseudo`. A check run writes
155
+ * nothing and only reports whether something would change; it checks `strings.en-XA.ts` only with
156
+ * `pseudo`.
135
157
  *
136
158
  * @param root - Game source root, the folder that holds the features.
137
159
  * @param out - The generated directory the modules are written into.
138
- * @param options - Whether this is a check run, what the features folder is called, and whether
139
- * the pseudo-locale is written.
160
+ * @param options - Whether this is a check run, what the features folder is called, whether the
161
+ * pseudo-locale is written, and the layers.
140
162
  * @returns Whether an output differed, the locales, the keys and the notes.
141
- * @throws {Error} One error that lists every problem the compile found, or the pseudo error when
142
- * no feature brings an `en` string file.
163
+ * @throws {Error} One error that lists every problem the compile found, the pseudo error when
164
+ * no feature or layer brings an `en` string file, or the layer error when a layer folder is not
165
+ * one folder name: nothing is read then.
143
166
  * @example
144
167
  * ```ts
145
168
  * // A dev build also writes the pseudo-locale, derived from the English messages.
146
169
  * const report = await compileStrings("src", "src/generated", { pseudo: true });
147
170
  * // report.locales: ["en", "en-XA", "ru"], report.changed: true on the first run
171
+ * // A v15 game: features/board/strings/en.json { "board.full": "Full" } and the layer file
172
+ * // shared/strings/en.json { "ui.ok": "OK" } compile together.
173
+ * const layered = await compileStrings("src", "src/generated", { layers: { shared: "ui" } });
174
+ * // layered.keys: ["board.full", "ui.ok"]
148
175
  * ```
149
176
  */
150
177
  declare function compileStrings(root: string, out: string, options?: CompileOptions): Promise<CompileReport>;
151
178
  /**
152
179
  * Tells whether the generated modules of a game are current, without writing anything. The
153
- * `--check` run of `bun run assets:keys` covers the strings with it.
180
+ * `--check` run of `bun run assets:keys` covers the strings with it. It walks the features only: a
181
+ * game with layers checks through `runCli` with `--check --layer …`.
154
182
  *
155
183
  * @param root - Game source root, the folder that holds the features.
156
184
  * @param out - The generated directory the modules live in.
@@ -164,17 +192,27 @@ declare function compileStrings(root: string, out: string, options?: CompileOpti
164
192
  declare function checkStrings(root: string, out: string): Promise<boolean>;
165
193
  //#endregion
166
194
  //#region src/plugins/i18n/compile/exchange.d.ts
195
+ /** A layer map: folder under the root to the name its keys take. */
196
+ type Layers = Readonly<Record<string, string>>;
167
197
  /**
168
198
  * What an export and an import are told beyond their two paths.
169
199
  *
170
200
  * @example
171
201
  * ```ts
172
202
  * const options: ExchangeOptions = { source: "ru" }; // the translators read the Russian text
203
+ * // A v15 game: the keys of shared/strings/ go out and come back with the rest.
204
+ * const layered: ExchangeOptions = { source: "ru", layers: { shared: "ui" } };
173
205
  * ```
174
206
  */
175
207
  type ExchangeOptions = {
176
208
  /** The locale translators read from, written as `source` beside each text. Default `"en"`. */source?: string; /** Name of the folder that holds the features. Default `"features"`. */
177
209
  features?: string;
210
+ /**
211
+ * Layers read next to the features: folder under `root` to the name its keys take.
212
+ * `{ shared: "ui" }` exports the keys of `<root>/shared/strings/` and imports their texts back
213
+ * into that folder. A folder is one name, without "/", "\" or ".". Default `{}`: features only.
214
+ */
215
+ layers?: Layers;
178
216
  };
179
217
  /**
180
218
  * What an import is told: the exchange options and the compile that runs after the files are
@@ -211,20 +249,21 @@ type ExportReport = {
211
249
  */
212
250
  type ImportReport = {
213
251
  /** Every locale an exchange file was read for, sorted. */locales: readonly string[]; /** How many keys were written. */
214
- keys: number; /** The string files of the features that were rewritten, relative to the root, sorted. */
252
+ keys: number; /** The string files of the features and layers rewritten, relative to the root, sorted. */
215
253
  files: readonly string[];
216
254
  };
217
255
  /**
218
- * Writes the files translators work in: one `<dir>/<locale>.json` per locale the features bring,
219
- * the source locale included, so a new language starts as a copy of `en.json`. Every key of the
220
- * game is in every file, sorted: `{ text, source, note? }`, with `text: ""` where the locale has no
221
- * message yet. The game tree is not touched, and a second run writes the same bytes.
256
+ * Writes the files translators work in: one `<dir>/<locale>.json` per locale the features and
257
+ * layers bring, the source locale included, so a new language starts as a copy of `en.json`. Every
258
+ * key of the game is in every file, sorted: `{ text, source, note? }`, with `text: ""` where the
259
+ * locale has no message yet. The game tree is not touched, and a second run writes the same bytes.
222
260
  *
223
261
  * @param root - Game source root, the folder that holds the features.
224
262
  * @param dir - The folder the exchange files are written into; created when missing.
225
- * @param options - The source locale and the name of the features folder.
263
+ * @param options - The source locale, the name of the features folder and the layers.
226
264
  * @returns The exported locales and how many keys each one lacks.
227
- * @throws {Error} When the game's strings do not compile, or no feature brings the source locale.
265
+ * @throws {Error} When the game's strings do not compile, a layer folder is not one folder name,
266
+ * or no feature brings the source locale.
228
267
  * @example
229
268
  * ```ts
230
269
  * // Before a release, the game hands its strings to the translators.
@@ -235,21 +274,26 @@ type ImportReport = {
235
274
  declare function exportStrings(root: string, dir: string, options?: ExchangeOptions): Promise<ExportReport>;
236
275
  /**
237
276
  * Takes translated exchange files back into the game: every `<dir>/<locale>.json` is checked
238
- * against the game first, then each new text is written into the string file of the feature that
239
- * owns its key, and the strings are compiled into `out` as a dev run does. An empty text and a
240
- * text equal to the current message are skipped. With any problem nothing is written.
277
+ * against the game first, then each new text is written into the string file of the feature or
278
+ * layer that owns its key, and the strings are compiled into `out` as a dev run does. An empty text
279
+ * and a text equal to the current message are skipped. With any problem nothing is written.
241
280
  *
242
281
  * @param root - Game source root, the folder that holds the features.
243
282
  * @param dir - The folder the translators handed back.
244
- * @param options - The features folder, where the compile writes, and whether it writes `en-XA`.
283
+ * @param options - The features folder, the layers, where the compile writes, and whether it
284
+ * writes `en-XA`.
245
285
  * @returns The locales read, how many keys were written and the files rewritten.
246
- * @throws {Error} One error that lists every problem of the exchange files, or the compile's own
247
- * error after the files are written (a parameter kind that differs across locales).
286
+ * @throws {Error} One error that lists every problem of the exchange files, the layer error
287
+ * before anything is read when a layer folder is not one folder name, or the compile's own error
288
+ * after the files are written (a parameter kind that differs across locales).
248
289
  * @example
249
290
  * ```ts
250
291
  * // The translators handed back translations/ru.json with two hud texts filled in.
251
292
  * await importStrings("src", "translations", { out: "src/generated" });
252
293
  * // { locales: ["ru"], keys: 2, files: ["features/hud/strings/ru.json"] }
294
+ * // A v15 game: the translated ui.ok lands in the layer's own file.
295
+ * await importStrings("src", "translations", { out: "src/generated", layers: { shared: "ui" } });
296
+ * // { locales: ["ru"], keys: 1, files: ["shared/strings/ru.json"] }
253
297
  * ```
254
298
  */
255
299
  declare function importStrings(root: string, dir: string, options?: ImportOptions): Promise<ImportReport>;
@@ -331,7 +375,19 @@ declare function packAssets(options: PackOptions): Promise<PackResult>;
331
375
  //#endregion
332
376
  //#region src/plugins/assets/scan/scan.d.ts
333
377
  /**
334
- * What one scan is told: where the features are and where the two generated files go.
378
+ * What one scan is told: where the features and the layers are and where the two generated files
379
+ * go.
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * // A v15 game: shared/ is a layer whose assets keep the ui.* keys.
384
+ * const options: ScanOptions = {
385
+ * root: "src",
386
+ * manifest: "public/assets/manifest.json",
387
+ * keys: "src/generated/assets.ts",
388
+ * layers: { shared: "ui" }
389
+ * };
390
+ * ```
335
391
  */
336
392
  type ScanOptions = {
337
393
  /** Path of the game source root, the folder that holds the features. */root: string; /** Name of the folder that holds the features. Default `"features"`. */
@@ -339,6 +395,12 @@ type ScanOptions = {
339
395
  manifest: string; /** Path of the generated key module to write. */
340
396
  keys: string; /** False for a check run: nothing is written. Default true. */
341
397
  write?: boolean;
398
+ /**
399
+ * Layers scanned next to the features: folder under `root` to the name its keys and its default
400
+ * bundle take. `{ shared: "ui" }` reads `<root>/shared/assets.ts` and `<root>/shared/assets/`
401
+ * exactly as `features/ui/` is read, so every key stays `ui.*`. Default `{}`: features only.
402
+ */
403
+ layers?: Readonly<Record<string, string>>;
342
404
  };
343
405
  /**
344
406
  * What a scan produced.
@@ -350,19 +412,21 @@ type ScanResult = {
350
412
  notes: readonly string[];
351
413
  };
352
414
  /**
353
- * Walks the features of a game and writes its two generated files: one bundle per feature, split
354
- * by the optional `assets.ts` of that feature, every file keyed by the key rule. A check run
415
+ * Walks the features and the layers of a game and writes its two generated files: one bundle per
416
+ * feature or layer, split by its optional `assets.ts`, every file keyed by the key rule. A layer is
417
+ * read like one more feature under its mapped name, after the features. A check run
355
418
  * (`write: false`) writes nothing and only reports whether something would change.
356
419
  *
357
- * @param options - The game source root, the two output paths and whether to write.
420
+ * @param options - The game source root, the layers, the two output paths and whether to write.
358
421
  * @returns The manifest, the text of the key module and whether an output differed.
359
- * @throws {Error} One error that lists every problem the scan found.
422
+ * @throws {Error} One error that lists every problem the scan found, the layer problems first.
360
423
  * @example
361
424
  * ```ts
362
425
  * const { manifest, changed } = await scanAssets({
363
426
  * root: "src",
364
427
  * manifest: "public/assets/manifest.json",
365
- * keys: "src/generated/assets.ts"
428
+ * keys: "src/generated/assets.ts",
429
+ * layers: { shared: "ui" } // shared/assets/button/primary.png keeps the key "ui.button.primary"
366
430
  * });
367
431
  * // manifest.bundles.ui.files[0].key: "ui.button.primary", changed: true on the first run
368
432
  * ```