@moku-labs/game 0.13.0 → 0.13.2
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 -3
- package/dist/app/page.d.mts +2 -1
- package/dist/app/page.mjs +1 -0
- package/dist/app/system.d.mts +3 -3
- package/dist/app.d.mts +2 -2
- package/dist/app.mjs +1 -1
- package/dist/assets.d.mts +1 -1
- package/dist/assets.mjs +1 -1
- package/dist/cli.d.mts +69 -3
- package/dist/cli.mjs +482 -57
- package/dist/{commands-DvFr8Uwp.d.mts → commands-BrgeMP5y.d.mts} +5 -5
- package/dist/control.d.mts +4 -4
- package/dist/{define-CCc0ozKe.d.mts → define-ZmWbWxon.d.mts} +2 -2
- package/dist/{headless-G4nuZ-xz.d.mts → headless-Dgxvp6Ym.d.mts} +1 -1
- package/dist/hot.d.mts +3 -3
- package/dist/hot.mjs +11 -9
- package/dist/{index-rnOYAriI.d.mts → index-CdbE_jGs.d.mts} +12 -4
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +1 -1
- package/dist/inspect.d.mts +7 -7
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/{memory-DwDhfFWK.d.mts → memory-DJ2sPExv.d.mts} +1 -1
- package/dist/project.mjs +7 -2954
- package/dist/{runtime-B276Gg0s.d.mts → runtime-6qakboTm.d.mts} +1 -1
- package/dist/{src-SmT-XxVf.mjs → src-CltFFKtc.mjs} +955 -108
- package/dist/testing.d.mts +4 -4
- package/dist/testing.mjs +1 -1
- package/dist/{types-D_2G6Q68.d.mts → types-B3T2X6To.d.mts} +8 -4
- package/dist/{types-ZQLYXo5X.d.mts → types-C6y8EfEB.d.mts} +54 -5
- package/dist/{types-BrTpz9oL.d.mts → types-CmuwAjkV.d.mts} +19 -6
- package/dist/{types-DAdwEJMp.d.mts → types-D57SntUt.d.mts} +2 -2
- package/dist/{types-DLghaGEy.d.mts → types-DGIkfZB7.d.mts} +4 -4
- package/dist/{types-Bcdo5-2w.d.mts → types-zRGJdFGX.d.mts} +7 -3
- package/dist/visual.d.mts +6 -6
- package/dist/watch-CWI_on4T.mjs +2960 -0
- package/llms.txt +11 -7
- package/package.json +4 -2
- package/dist/{assets-DYiKxPBX.mjs → assets-BrKSg1O6.mjs} +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as resolveConfig } from "./config-CdARrrhU.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { r as watchTree, t as createBatcher, x as isSkippedFolder$1 } from "./watch-CWI_on4T.mjs";
|
|
3
|
+
import { a as runCli$1, i as compileStrings, n as importStrings, t as exportStrings } from "./assets-BrKSg1O6.mjs";
|
|
4
|
+
import { constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { createBrandConsole } from "@moku-labs/common/cli";
|
|
6
7
|
import { parseArgs } from "node:util";
|
|
@@ -8,6 +9,11 @@ import { pathToFileURL } from "node:url";
|
|
|
8
9
|
//#region src/app/generate.ts
|
|
9
10
|
/** The first line of a generated file a person may open. */
|
|
10
11
|
const WRITTEN = "// Written by moku-game dev. Do not edit.";
|
|
12
|
+
/**
|
|
13
|
+
* The stamp module of a game, a file of `<game>/.moku/`. One per game: the dev page and the page
|
|
14
|
+
* of `moku-game visual` import the same one.
|
|
15
|
+
*/
|
|
16
|
+
const ASSETS_STAMP = "assets-stamp.ts";
|
|
11
17
|
/** A colour or a language tag: it lands in a `<style>` and in attributes, so nothing else passes. */
|
|
12
18
|
const SAFE_VALUE = /^[#\w(),.%\s-]+$/;
|
|
13
19
|
/** The files of `tests/scenarios/` that are not scenarios. */
|
|
@@ -223,10 +229,40 @@ function importLine(binding, specifier) {
|
|
|
223
229
|
return `import ${binding} from ${JSON.stringify(specifier)};`;
|
|
224
230
|
}
|
|
225
231
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
232
|
+
* Orders two texts by their code units, as `toSorted()` does without a comparer.
|
|
233
|
+
*
|
|
234
|
+
* @param first - One text.
|
|
235
|
+
* @param second - The other text.
|
|
236
|
+
* @returns A negative number, zero or a positive number.
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* compareText("a/b.png", "a/c.png"); // -1
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
function compareText(first, second) {
|
|
243
|
+
if (first === second) return 0;
|
|
244
|
+
return first < second ? -1 : 1;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The path from a page under `.moku/` to `.moku/` itself, where the one stamp of a game lives.
|
|
248
|
+
*
|
|
249
|
+
* @param toRoot - The path from the page to the game folder.
|
|
250
|
+
* @returns `"./"` for the dev page in `.moku/`, `"../"` for the page in `.moku/visual/`.
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* toStampFolder("../../"); // "../"
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
function toStampFolder(toRoot) {
|
|
257
|
+
return toRoot.slice(3) || "./";
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The `main.ts` of the dev page: the dev flag first, then the assets stamp, the page entry, the
|
|
261
|
+
* game, its config, one import per scenario, and, with agents, one per `.dev` module and one per
|
|
262
|
+
* agent. It calls `startPage` with the scenarios by file stem, the shell with the loaders of the
|
|
263
|
+
* named plugins when the game needs it, the agents and the `.dev` modules. The stamp import puts
|
|
264
|
+
* the stamp module on the page: when the keys watch writes a new stamp, the hot plugin hands it to
|
|
265
|
+
* the running game.
|
|
230
266
|
*
|
|
231
267
|
* @param settings - The resolved `config.ts`.
|
|
232
268
|
* @param sources - The scenario files, the `.dev` modules and the agents.
|
|
@@ -252,6 +288,7 @@ function devMain(settings, sources, toRoot = "../") {
|
|
|
252
288
|
return [
|
|
253
289
|
WRITTEN,
|
|
254
290
|
"import \"./dev.ts\";",
|
|
291
|
+
`import "${toStampFolder(toRoot)}${ASSETS_STAMP}";`,
|
|
255
292
|
...pageImports(settings, toRoot),
|
|
256
293
|
...scenarios.map((file, index) => importLine(`scenario${index}`, `${toRoot}tests/scenarios/${file}`)),
|
|
257
294
|
...devModules.map((file, index) => importLine(`* as devModule${index}`, `${toRoot}${file}`)),
|
|
@@ -302,6 +339,36 @@ function devFlag() {
|
|
|
302
339
|
return `${WRITTEN}\nglobalThis.__MOKU_GAME_DEV__ = true;\n`;
|
|
303
340
|
}
|
|
304
341
|
/**
|
|
342
|
+
* The `assets-stamp.ts` module the dev `main.ts` imports: the asset files of the game with their
|
|
343
|
+
* stamps, and the ones the last save changed. The hot plugin takes it for a module that swaps, so
|
|
344
|
+
* a new stamp reaches the running game, which reads the changed files again. The page starts with
|
|
345
|
+
* the empty stamp; the keys watch writes the files. Both lists are sorted here, and every path
|
|
346
|
+
* and stamp is written by `JSON.stringify`: a file name may hold a quote or a backslash.
|
|
347
|
+
*
|
|
348
|
+
* @param files - Every watched asset file: its path relative to the game with `/`, and its stamp
|
|
349
|
+
* `<size>:<mtimeMs>`. None before the first watch.
|
|
350
|
+
* @param changed - The paths whose stamp differs from the batch before, the new ones too. None in
|
|
351
|
+
* the first stamp of a run.
|
|
352
|
+
* @returns The text of `.moku/assets-stamp.ts`.
|
|
353
|
+
* @example
|
|
354
|
+
* ```ts
|
|
355
|
+
* assetsStamp([], []); // '// Written by moku-game dev. Do not edit.\nexport default {\n files: {},\n changed: []\n};\n'
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
function assetsStamp(files, changed) {
|
|
359
|
+
const entries = files.toSorted(([first], [second]) => compareText(first, second)).map(([file, stamp]) => ` ${JSON.stringify(file)}: ${JSON.stringify(stamp)}`);
|
|
360
|
+
const map = entries.length === 0 ? "{}" : `{\n${entries.join(",\n")}\n }`;
|
|
361
|
+
const list = changed.toSorted(compareText).map((file) => JSON.stringify(file)).join(", ");
|
|
362
|
+
return [
|
|
363
|
+
WRITTEN,
|
|
364
|
+
"export default {",
|
|
365
|
+
` files: ${map},`,
|
|
366
|
+
` changed: [${list}]`,
|
|
367
|
+
"};",
|
|
368
|
+
""
|
|
369
|
+
].join("\n");
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
305
372
|
* A TOML array of strings. `JSON.stringify` of a path is a valid TOML basic string, also for a
|
|
306
373
|
* Windows path.
|
|
307
374
|
*
|
|
@@ -338,12 +405,31 @@ function bunfigText(paths) {
|
|
|
338
405
|
].join("\n");
|
|
339
406
|
}
|
|
340
407
|
//#endregion
|
|
408
|
+
//#region src/app/files.ts
|
|
409
|
+
/**
|
|
410
|
+
* @file The file write of the files `moku-game` generates for a game. The dev page and the stamp
|
|
411
|
+
* of the keys watch go through it: a file the dev server watches is touched only when its text
|
|
412
|
+
* changed. Node and Bun only: the bin bundles it.
|
|
413
|
+
*/
|
|
414
|
+
/**
|
|
415
|
+
* Writes a file only when its text changed, so a second run wakes no watcher.
|
|
416
|
+
*
|
|
417
|
+
* @param file - The absolute path.
|
|
418
|
+
* @param text - The text.
|
|
419
|
+
*/
|
|
420
|
+
function writeIfChanged(file, text) {
|
|
421
|
+
if (existsSync(file) && readFileSync(file, "utf8") === text) return;
|
|
422
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
423
|
+
writeFileSync(file, text);
|
|
424
|
+
}
|
|
425
|
+
//#endregion
|
|
341
426
|
//#region src/app/serve.ts
|
|
342
427
|
/**
|
|
343
428
|
* @file `moku-game dev`: the game folder read and checked, the dev page written into
|
|
344
|
-
* `<game>/.moku/`, the parent that re-runs the bin under the generated
|
|
345
|
-
* serves the page with `Bun.serve`. The hot plugin comes from the
|
|
346
|
-
* the game writes no bunfig, no HTML and no dev file. Node and Bun
|
|
429
|
+
* `<game>/.moku/`, the parent that starts the keys watch and re-runs the bin under the generated
|
|
430
|
+
* bunfig, and the child that serves the page with `Bun.serve`. The hot plugin comes from the
|
|
431
|
+
* engine the game resolves, so the game writes no bunfig, no HTML and no dev file. Node and Bun
|
|
432
|
+
* only: the bin bundles it.
|
|
347
433
|
*/
|
|
348
434
|
/** The signals the dev parent hands to its child. */
|
|
349
435
|
const FORWARDED = [
|
|
@@ -356,8 +442,8 @@ const STOPPING = ["SIGINT", "SIGTERM"];
|
|
|
356
442
|
/** How often the dev child checks that its parent still runs. */
|
|
357
443
|
const PARENT_CHECK_MS = 1e3;
|
|
358
444
|
/**
|
|
359
|
-
* The dev manifest `moku-game keys`
|
|
360
|
-
* `generated/assets.ts`. The dev server answers `/manifest.json` with it.
|
|
445
|
+
* The dev manifest `moku-game keys` and the keys watch of `moku-game dev` write, relative to the
|
|
446
|
+
* game folder, next to `generated/assets.ts`. The dev server answers `/manifest.json` with it.
|
|
361
447
|
*/
|
|
362
448
|
const DEV_MANIFEST = "generated/manifest.json";
|
|
363
449
|
/** The folders a `.dev.ts` module is never looked for in. */
|
|
@@ -505,17 +591,6 @@ function devModulesOf(root, relative = "") {
|
|
|
505
591
|
});
|
|
506
592
|
}
|
|
507
593
|
/**
|
|
508
|
-
* Writes a file only when its text changed, so a second run wakes no watcher.
|
|
509
|
-
*
|
|
510
|
-
* @param file - The absolute path.
|
|
511
|
-
* @param text - The text.
|
|
512
|
-
*/
|
|
513
|
-
function writeIfChanged(file, text) {
|
|
514
|
-
if (existsSync(file) && readFileSync(file, "utf8") === text) return;
|
|
515
|
-
mkdirSync(path.dirname(file), { recursive: true });
|
|
516
|
-
writeFileSync(file, text);
|
|
517
|
-
}
|
|
518
|
-
/**
|
|
519
594
|
* The text of the dev `main.ts` for the game as it is on disk now.
|
|
520
595
|
*
|
|
521
596
|
* @param root - The game folder.
|
|
@@ -539,7 +614,9 @@ function mainOf(root, settings, agents, toRoot = "../") {
|
|
|
539
614
|
* Writes the dev page of a checked game into `<game>/.moku/`: `index.html`, `dev.ts`, `main.ts`
|
|
540
615
|
* and `bunfig.toml`. `moku-game visual` writes its own page into `.moku/visual/`, so the page of
|
|
541
616
|
* `dev` or of the editor stays as it is; the links of the page reach the game from its depth.
|
|
542
|
-
* Every text is made before the first write, so a refused value writes nothing.
|
|
617
|
+
* Every text is made before the first write, so a refused value writes nothing. A game without
|
|
618
|
+
* `.moku/assets-stamp.ts` gets the empty one, so the import of `main.ts` resolves with or without
|
|
619
|
+
* a keys watch; a stamp that is there is the watch's and stays.
|
|
543
620
|
*
|
|
544
621
|
* @param root - The game folder, absolute.
|
|
545
622
|
* @param settings - The resolved config.
|
|
@@ -573,15 +650,18 @@ function writePage(root, settings, options, resolve, page = ".moku") {
|
|
|
573
650
|
preload
|
|
574
651
|
})]
|
|
575
652
|
];
|
|
653
|
+
const stamp = path.join(root, ".moku", ASSETS_STAMP);
|
|
576
654
|
for (const [file, text] of texts) writeIfChanged(file, text);
|
|
655
|
+
if (!existsSync(stamp)) writeIfChanged(stamp, assetsStamp([], []));
|
|
577
656
|
return {
|
|
578
657
|
html,
|
|
579
658
|
bunfig
|
|
580
659
|
};
|
|
581
660
|
}
|
|
582
661
|
/**
|
|
583
|
-
* Reads and checks a game, then writes its dev page into `<game>/.moku
|
|
584
|
-
* the dev parent's step 3; it starts no server
|
|
662
|
+
* Reads and checks a game, then writes its dev page into `<game>/.moku/`, the first
|
|
663
|
+
* `assets-stamp.ts` with it. The editor's step and the dev parent's step 3; it starts no server
|
|
664
|
+
* and no watcher.
|
|
585
665
|
*
|
|
586
666
|
* @param root - The game folder, absolute.
|
|
587
667
|
* @param options - The agents, the preloads and the extra plugins, absolute.
|
|
@@ -656,9 +736,12 @@ function devChildCommand(run, page, deps) {
|
|
|
656
736
|
];
|
|
657
737
|
}
|
|
658
738
|
/**
|
|
659
|
-
* The dev parent: checks the game,
|
|
660
|
-
* with the game as its working directory, and watches
|
|
661
|
-
*
|
|
739
|
+
* The dev parent: checks the game, starts the keys watch on the raw files, writes the page,
|
|
740
|
+
* re-runs the bin under the generated bunfig with the game as its working directory, and watches
|
|
741
|
+
* the scenarios while the child runs. The first scan of the keys watch writes
|
|
742
|
+
* `generated/manifest.json` before the page is served, and a save of an asset or a strings file
|
|
743
|
+
* scans again; `--packed` serves the pack and watches no keys. It hands Ctrl+C and the other stop
|
|
744
|
+
* signals to the child and answers the child's exit code.
|
|
662
745
|
*
|
|
663
746
|
* @param run - The flags of the run.
|
|
664
747
|
* @param deps - The process seams.
|
|
@@ -668,15 +751,18 @@ function devChildCommand(run, page, deps) {
|
|
|
668
751
|
async function serveParent(run, deps) {
|
|
669
752
|
const settings = await loadGame(run.root);
|
|
670
753
|
const hasPack = isFile(path.join(run.root, "dist", "assets", "manifest.json"));
|
|
671
|
-
const hasManifest = isFile(path.join(run.root, DEV_MANIFEST));
|
|
672
754
|
if (run.packed && !hasPack) throw new Error(`[game] dev: no packed build in "${path.join(run.root, "dist", "assets")}".\n Run "moku-game pack" first.`);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
755
|
+
const keys = run.packed ? void 0 : await deps.watchKeys(run.root, settings);
|
|
756
|
+
try {
|
|
757
|
+
const page = writePage(run.root, settings, run, deps.resolve);
|
|
758
|
+
if (!ignoresMoku(run.root)) deps.ui.warn("[game] dev: add \".moku/\" to .gitignore. moku-game writes its dev page there.");
|
|
759
|
+
return await runChild(devChildCommand(run, page, deps), deps, {
|
|
760
|
+
cwd: run.root,
|
|
761
|
+
whileRunning: () => watchScenarios(run.root, settings, deps)
|
|
762
|
+
});
|
|
763
|
+
} finally {
|
|
764
|
+
keys?.close();
|
|
765
|
+
}
|
|
680
766
|
}
|
|
681
767
|
/**
|
|
682
768
|
* Runs the bin again as a marked child, `MOKU_GAME_CHILD=1`, in its own process group: Ctrl+C
|
|
@@ -749,21 +835,22 @@ function safeDecode(pathname) {
|
|
|
749
835
|
}
|
|
750
836
|
}
|
|
751
837
|
/**
|
|
752
|
-
* Answers a request with a file, or a 404 for a missing or empty one.
|
|
838
|
+
* Answers a request with a file, or a 404 for a missing or empty one. A file is never cached:
|
|
839
|
+
* after a reload the page shows the new bytes of an edited image.
|
|
753
840
|
*
|
|
754
841
|
* @param file - The absolute path, or `undefined` for a refused path.
|
|
755
842
|
* @returns The response.
|
|
756
843
|
*/
|
|
757
844
|
function fileResponse(file) {
|
|
758
845
|
const found = file === void 0 ? void 0 : statSync(file, { throwIfNoEntry: false });
|
|
759
|
-
return file !== void 0 && found?.isFile() === true && found.size > 0 ? new Response(Bun.file(file)) : new Response("Not found", { status: 404 });
|
|
846
|
+
return file !== void 0 && found?.isFile() === true && found.size > 0 ? new Response(Bun.file(file), { headers: { "Cache-Control": "no-store" } }) : new Response("Not found", { status: 404 });
|
|
760
847
|
}
|
|
761
848
|
/**
|
|
762
849
|
* Starts the dev server on `127.0.0.1` only, never on every interface: the page on `/`, the
|
|
763
|
-
* manifest on `/manifest.json`, and the files of the served folder as static files
|
|
764
|
-
* is the dev manifest `generated/manifest.json` of the
|
|
765
|
-
* with `--packed`; its paths are relative to the served
|
|
766
|
-
* `moku-game visual` serve the page with it.
|
|
850
|
+
* manifest on `/manifest.json`, and the files of the served folder as static files, each with
|
|
851
|
+
* `Cache-Control: no-store`. The manifest is the dev manifest `generated/manifest.json` of the
|
|
852
|
+
* game, or `manifest.json` of `dist/assets` with `--packed`; its paths are relative to the served
|
|
853
|
+
* folder. The dev child and `moku-game visual` serve the page with it.
|
|
767
854
|
*
|
|
768
855
|
* @param run - The flags of the run.
|
|
769
856
|
* @param page - The page bundle the process imported.
|
|
@@ -1311,9 +1398,9 @@ async function runVisual(command, deps) {
|
|
|
1311
1398
|
* @file The command line of `moku-game`: the arguments read per command with `node:util`
|
|
1312
1399
|
* `parseArgs`, then one of `dev`, `build`, `native <verb>`, `keys`, `pack`, `visual` or `help`.
|
|
1313
1400
|
* Every seam of the process (the console, the environment, spawning, signals, the watcher, the
|
|
1314
|
-
* asset scanner, native, Bun's resolver, the visual test runner) comes in as
|
|
1315
|
-
* runs a command with stubs. It returns the exit code and never exits itself. A
|
|
1316
|
-
* on no API.
|
|
1401
|
+
* keys watch, the asset scanner, native, Bun's resolver, the visual test runner) comes in as
|
|
1402
|
+
* `deps`, so a test runs a command with stubs. It returns the exit code and never exits itself. A
|
|
1403
|
+
* plain function, on no API.
|
|
1317
1404
|
*/
|
|
1318
1405
|
/** The flags every command takes. */
|
|
1319
1406
|
const GLOBAL_OPTIONS = [
|
|
@@ -1732,6 +1819,282 @@ async function runCommand(argv, deps) {
|
|
|
1732
1819
|
}
|
|
1733
1820
|
}
|
|
1734
1821
|
//#endregion
|
|
1822
|
+
//#region src/app/keys.ts
|
|
1823
|
+
/**
|
|
1824
|
+
* @file The keys watch of a game folder, behind `moku-game dev` and the editor's `watchKeys`: it
|
|
1825
|
+
* keeps `generated/` fresh while a dev page is open. A file event is only a trigger. When the
|
|
1826
|
+
* events stop for a quiet period, a batch walks the game and reads the size and the time stamp of
|
|
1827
|
+
* every asset file and every `strings/<locale>.json`; it runs the scan of `moku-game keys` only
|
|
1828
|
+
* when they differ from the batch before. After a scan it writes `.moku/assets-stamp.ts` from the
|
|
1829
|
+
* asset files: every file with its stamp, and the ones this batch found changed, so the dev page
|
|
1830
|
+
* reads only those again. One scan runs at a time, and a failed scan never stops the watch. A
|
|
1831
|
+
* batch that scanned is followed by one more walk: the platform reports only the first path of a
|
|
1832
|
+
* burst of writes, so a save made in the same instant as the scan's own writes under `generated/`
|
|
1833
|
+
* may never be reported. The scan, `fs.watch` and the console come in as seams, so a test drives
|
|
1834
|
+
* it. Node and Bun only: the bin bundles it.
|
|
1835
|
+
*/
|
|
1836
|
+
/** The quiet period that closes a batch of file events. */
|
|
1837
|
+
const QUIET_MS = 100;
|
|
1838
|
+
/** The asset files the scanner reads, by extension. */
|
|
1839
|
+
const ASSET_FILE = /\.(?:png|webp|fnt|mp3|m4a)$/i;
|
|
1840
|
+
/** A strings file: a `.json` directly in a `strings/` folder. */
|
|
1841
|
+
const STRINGS_FILE = /(?:^|\/)strings\/[^/]+\.json$/;
|
|
1842
|
+
/**
|
|
1843
|
+
* Lists one folder of the walk.
|
|
1844
|
+
*
|
|
1845
|
+
* @param folder - The absolute folder.
|
|
1846
|
+
* @returns Its entries; none when it cannot be read or vanished during the walk.
|
|
1847
|
+
*/
|
|
1848
|
+
function entriesOf(folder) {
|
|
1849
|
+
try {
|
|
1850
|
+
return readdirSync(folder, { withFileTypes: true });
|
|
1851
|
+
} catch {
|
|
1852
|
+
return [];
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Tells whether a file is an input of the scan: an asset file, or a strings file.
|
|
1857
|
+
*
|
|
1858
|
+
* @param file - The path relative to the game, with `/`.
|
|
1859
|
+
* @returns True for a file the watch stamps.
|
|
1860
|
+
* @example
|
|
1861
|
+
* ```ts
|
|
1862
|
+
* isInput("features/home/strings/en.json"); // true
|
|
1863
|
+
* ```
|
|
1864
|
+
*/
|
|
1865
|
+
function isInput(file) {
|
|
1866
|
+
return ASSET_FILE.test(file) || STRINGS_FILE.test(file);
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Reads one folder of the walk and the folders below it, outside the skipped ones: every asset
|
|
1870
|
+
* file and every strings file joins the inputs with its stamp. Symlinks are not followed.
|
|
1871
|
+
*
|
|
1872
|
+
* @param root - The real game folder.
|
|
1873
|
+
* @param folder - The folder, relative to the game; `""` is the game folder.
|
|
1874
|
+
* @param walk - The inputs and the folders so far.
|
|
1875
|
+
*/
|
|
1876
|
+
function readFolder(root, folder, walk) {
|
|
1877
|
+
walk.folders.push(folder);
|
|
1878
|
+
for (const entry of entriesOf(path.join(root, folder))) {
|
|
1879
|
+
const inGame = folder === "" ? entry.name : `${folder}/${entry.name}`;
|
|
1880
|
+
if (entry.isDirectory() && !isSkippedFolder$1(entry.name)) readFolder(root, inGame, walk);
|
|
1881
|
+
if (!entry.isFile() || !isInput(inGame)) continue;
|
|
1882
|
+
const stats = statSync(path.join(root, inGame), { throwIfNoEntry: false });
|
|
1883
|
+
if (stats !== void 0) walk.inputs.set(inGame, `${stats.size}:${stats.mtimeMs}`);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Walks a game folder for the inputs of the scan.
|
|
1888
|
+
*
|
|
1889
|
+
* @param root - The real game folder.
|
|
1890
|
+
* @returns The stamps of the inputs, and the folders on the way.
|
|
1891
|
+
*/
|
|
1892
|
+
function walkInputs(root) {
|
|
1893
|
+
const walk = {
|
|
1894
|
+
inputs: /* @__PURE__ */ new Map(),
|
|
1895
|
+
folders: []
|
|
1896
|
+
};
|
|
1897
|
+
readFolder(root, "", walk);
|
|
1898
|
+
return walk;
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Lists the inputs that differ between two walks: added, removed, or saved again.
|
|
1902
|
+
*
|
|
1903
|
+
* @param before - The stamps of the walk before.
|
|
1904
|
+
* @param after - The stamps of this walk.
|
|
1905
|
+
* @returns The root-relative paths, sorted.
|
|
1906
|
+
* @example
|
|
1907
|
+
* ```ts
|
|
1908
|
+
* changedPaths(new Map([["a.png", "3:1"]]), new Map([["a.png", "4:2"], ["b.png", "1:1"]])); // ["a.png", "b.png"]
|
|
1909
|
+
* ```
|
|
1910
|
+
*/
|
|
1911
|
+
function changedPaths(before, after) {
|
|
1912
|
+
return [.../* @__PURE__ */ new Set([...before.keys(), ...after.keys()])].filter((file) => before.get(file) !== after.get(file)).toSorted();
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* The text of the stamp module after one batch: every asset file of the walk with its stamp, and
|
|
1916
|
+
* the asset files that differ from the batch before. The strings files stay out, so a batch that
|
|
1917
|
+
* saved only strings has no stamp to write. A removed file is not listed as changed: it shows by
|
|
1918
|
+
* its absence from the files. The first batch of a run has no batch before it, so it lists none.
|
|
1919
|
+
*
|
|
1920
|
+
* @param inputs - The stamps of this walk.
|
|
1921
|
+
* @param changed - The inputs that differ from the batch before: added, removed, or saved again.
|
|
1922
|
+
* @param first - True for the first batch of the watch.
|
|
1923
|
+
* @returns The text, or `undefined` when no asset file changed in a later batch.
|
|
1924
|
+
* @example
|
|
1925
|
+
* ```ts
|
|
1926
|
+
* stampText(new Map([["a.png", "4:2"], ["strings/en.json", "9:1"]]), ["a.png", "strings/en.json"], false); // '// Written by moku-game dev. Do not edit.\nexport default {\n files: {\n "a.png": "4:2"\n },\n changed: ["a.png"]\n};\n'
|
|
1927
|
+
* ```
|
|
1928
|
+
*/
|
|
1929
|
+
function stampText(inputs, changed, first) {
|
|
1930
|
+
const moved = changed.filter((file) => ASSET_FILE.test(file));
|
|
1931
|
+
if (!first && moved.length === 0) return void 0;
|
|
1932
|
+
return assetsStamp([...inputs].filter(([file]) => ASSET_FILE.test(file)), first ? [] : moved.filter((file) => inputs.has(file)));
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* The line of a later scan that worked: the first changed input, and how many more.
|
|
1936
|
+
*
|
|
1937
|
+
* @param changed - The changed inputs, sorted, at least one.
|
|
1938
|
+
* @returns The line.
|
|
1939
|
+
* @example
|
|
1940
|
+
* ```ts
|
|
1941
|
+
* changeLine(["features/ui/assets/a.png", "features/ui/assets/b.png"]); // "keys: features/ui/assets/a.png and 1 more"
|
|
1942
|
+
* ```
|
|
1943
|
+
*/
|
|
1944
|
+
function changeLine(changed) {
|
|
1945
|
+
const more = changed.length - 1;
|
|
1946
|
+
return more === 0 ? `keys: ${changed[0]}` : `keys: ${changed[0]} and ${more} more`;
|
|
1947
|
+
}
|
|
1948
|
+
/**
|
|
1949
|
+
* The console a scan is handed. It records the scanner's errors and prints none: `onError` is
|
|
1950
|
+
* their only printer. The first scan prints its other lines as `moku-game keys` does; a later
|
|
1951
|
+
* scan drops them, the batch prints one line of its own.
|
|
1952
|
+
*
|
|
1953
|
+
* @param ui - The real console.
|
|
1954
|
+
* @param errors - Takes every error line of the scan.
|
|
1955
|
+
* @param first - True for the first scan of the watch.
|
|
1956
|
+
* @returns The console of one scan.
|
|
1957
|
+
*/
|
|
1958
|
+
function scanUi(ui, errors, first) {
|
|
1959
|
+
return {
|
|
1960
|
+
info: (message) => {
|
|
1961
|
+
if (first) ui.info(message);
|
|
1962
|
+
},
|
|
1963
|
+
warn: (message) => {
|
|
1964
|
+
if (first) ui.warn(message);
|
|
1965
|
+
},
|
|
1966
|
+
line: (text) => {
|
|
1967
|
+
if (first) ui.line(text);
|
|
1968
|
+
},
|
|
1969
|
+
error: (message) => {
|
|
1970
|
+
errors.push(message);
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* The message of a scan that answered an exit code other than 0: the error lines it printed, one
|
|
1976
|
+
* per line, or the code itself when it printed none.
|
|
1977
|
+
*
|
|
1978
|
+
* @param code - The exit code of the scan, not 0.
|
|
1979
|
+
* @param errors - The error lines the scan printed.
|
|
1980
|
+
* @returns The message for `onError`.
|
|
1981
|
+
* @example
|
|
1982
|
+
* ```ts
|
|
1983
|
+
* failureText(1, []); // "the scan exited with code 1."
|
|
1984
|
+
* ```
|
|
1985
|
+
*/
|
|
1986
|
+
function failureText(code, errors) {
|
|
1987
|
+
return errors.length > 0 ? errors.join("\n") : `the scan exited with code ${code}.`;
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Runs the scan of `moku-game keys` for the game as it is on disk now. The flags are read again
|
|
1991
|
+
* for every scan: `--pseudo` depends on a `strings/en.json` being there.
|
|
1992
|
+
*
|
|
1993
|
+
* @param watch - The watch.
|
|
1994
|
+
* @param first - True for the first scan of the watch.
|
|
1995
|
+
* @returns The message of a failed scan, or `undefined` when it worked.
|
|
1996
|
+
*/
|
|
1997
|
+
async function failureOf(watch, first) {
|
|
1998
|
+
const errors = [];
|
|
1999
|
+
try {
|
|
2000
|
+
const argv = keysArguments(watch.root, watch.settings, false);
|
|
2001
|
+
const code = await watch.seams.scan(argv, scanUi(watch.seams.ui, errors, first));
|
|
2002
|
+
return code === 0 ? void 0 : failureText(code, errors);
|
|
2003
|
+
} catch (error) {
|
|
2004
|
+
return error instanceof Error ? error.message : String(error);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* Runs the scan and says how it went: a failure goes to `onError`, a later scan that worked
|
|
2009
|
+
* prints one line. The first scan printed the scanner's own lines.
|
|
2010
|
+
*
|
|
2011
|
+
* @param watch - The watch.
|
|
2012
|
+
* @param changed - The changed inputs, sorted.
|
|
2013
|
+
* @param first - True for the first scan of the watch.
|
|
2014
|
+
*/
|
|
2015
|
+
async function scanAndReport(watch, changed, first) {
|
|
2016
|
+
const failure = await failureOf(watch, first);
|
|
2017
|
+
if (failure !== void 0) watch.onError(failure);
|
|
2018
|
+
else if (!first) watch.seams.ui.info(changeLine(changed));
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* One batch: walks the game, and scans when an input differs from the batch before; the first
|
|
2022
|
+
* batch always scans. Then it writes the stamp when an asset file changed, asks for one trailing
|
|
2023
|
+
* batch, and arms the folder watchers with the folders of the walk. A batch that did not scan
|
|
2024
|
+
* asks for nothing, so the trailing batch of a quiet game ends the chain.
|
|
2025
|
+
*
|
|
2026
|
+
* @param watch - The watch.
|
|
2027
|
+
*/
|
|
2028
|
+
async function runBatch(watch) {
|
|
2029
|
+
const walk = walkInputs(watch.root);
|
|
2030
|
+
const first = watch.inputs === void 0;
|
|
2031
|
+
const changed = changedPaths(watch.inputs ?? /* @__PURE__ */ new Map(), walk.inputs);
|
|
2032
|
+
watch.inputs = walk.inputs;
|
|
2033
|
+
if (first || changed.length > 0) {
|
|
2034
|
+
await scanAndReport(watch, changed, first);
|
|
2035
|
+
const stamp = stampText(walk.inputs, changed, first);
|
|
2036
|
+
if (stamp !== void 0) writeIfChanged(path.join(watch.root, ".moku", ASSETS_STAMP), stamp);
|
|
2037
|
+
watch.again();
|
|
2038
|
+
}
|
|
2039
|
+
if (!watch.closed) watch.tree.rearm(walk.folders);
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Runs one batch after the batch before it ended: two scans never write `generated/` at the same
|
|
2043
|
+
* time. A batch the batcher starts during the first scan waits here.
|
|
2044
|
+
*
|
|
2045
|
+
* @param watch - The watch.
|
|
2046
|
+
* @returns Resolves when the batch ended.
|
|
2047
|
+
*/
|
|
2048
|
+
function serialized(watch) {
|
|
2049
|
+
const batch = watch.scanning.then(() => runBatch(watch));
|
|
2050
|
+
watch.scanning = batch.catch(() => {});
|
|
2051
|
+
return batch;
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Starts the keys watch of a checked game: watches the folder, runs the scan of `moku-game keys`
|
|
2055
|
+
* once, and resolves after it, so `generated/` is fresh before a page is served. The watcher
|
|
2056
|
+
* starts first: a save the platform reports during the first scan starts a batch after it. Then
|
|
2057
|
+
* every batch of file events outside the skipped folders scans again when an asset file or a
|
|
2058
|
+
* strings file changed. A failed scan, the first one too, goes to `onError` and the watch goes on.
|
|
2059
|
+
*
|
|
2060
|
+
* @param root - The game folder, absolute.
|
|
2061
|
+
* @param settings - The resolved config.
|
|
2062
|
+
* @param seams - The scan, `fs.watch` and the console.
|
|
2063
|
+
* @param options - Where the message of a failed scan goes.
|
|
2064
|
+
* @returns The running watch.
|
|
2065
|
+
* @throws {Error} When the game folder is gone, or the first batch breaks outside the scan.
|
|
2066
|
+
*/
|
|
2067
|
+
async function watchKeysAt(root, settings, seams, options = {}) {
|
|
2068
|
+
const real = realpathSync(root);
|
|
2069
|
+
const onError = options.onError ?? ((message) => seams.ui.warn(`[game] keys: ${message}`));
|
|
2070
|
+
const batcher = createBatcher(QUIET_MS, () => serialized(watch));
|
|
2071
|
+
const tree = watchTree(real, () => batcher.poke(), seams.watch);
|
|
2072
|
+
const watch = {
|
|
2073
|
+
root: real,
|
|
2074
|
+
settings,
|
|
2075
|
+
seams,
|
|
2076
|
+
onError,
|
|
2077
|
+
tree,
|
|
2078
|
+
again: () => batcher.poke(),
|
|
2079
|
+
inputs: void 0,
|
|
2080
|
+
scanning: Promise.resolve(),
|
|
2081
|
+
closed: false
|
|
2082
|
+
};
|
|
2083
|
+
const close = () => {
|
|
2084
|
+
if (watch.closed) return;
|
|
2085
|
+
watch.closed = true;
|
|
2086
|
+
batcher.cancel();
|
|
2087
|
+
tree.close();
|
|
2088
|
+
};
|
|
2089
|
+
try {
|
|
2090
|
+
await serialized(watch);
|
|
2091
|
+
} catch (error) {
|
|
2092
|
+
close();
|
|
2093
|
+
throw error;
|
|
2094
|
+
}
|
|
2095
|
+
return { close };
|
|
2096
|
+
}
|
|
2097
|
+
//#endregion
|
|
1735
2098
|
//#region src/app/native.ts
|
|
1736
2099
|
/**
|
|
1737
2100
|
* @file The native verbs of `moku-game`: a game's `config.ts` as the config of `@moku-labs/native`,
|
|
@@ -1882,13 +2245,44 @@ async function runNative(verb, settings, where, options) {
|
|
|
1882
2245
|
* @file The `@moku-labs/game/cli` door, node and bun only: the `moku-game` bin and the editor's
|
|
1883
2246
|
* seam. `runCli` runs one command line of `moku-game` (dev, build, native, keys, pack, visual,
|
|
1884
2247
|
* help) in this process; `preparePage` writes a game's dev page into `<game>/.moku/` for a server the
|
|
1885
|
-
* caller starts itself, as the editor does
|
|
1886
|
-
*
|
|
2248
|
+
* caller starts itself, as the editor does, and `watchKeys` keeps the game's `generated/` fresh
|
|
2249
|
+
* while that page is open. It imports no Pixi, and the root entry never imports it. The code
|
|
2250
|
+
* lives in `app/`; this file wires the real process into it.
|
|
2251
|
+
*/
|
|
2252
|
+
/**
|
|
2253
|
+
* Runs the asset scanner's command line with the string tools of i18n: the scan behind
|
|
2254
|
+
* `moku-game keys`, `pack`, and the keys watch.
|
|
2255
|
+
*
|
|
2256
|
+
* @param argv - The flags of the scanner.
|
|
2257
|
+
* @param ui - Where its lines go.
|
|
2258
|
+
* @returns The exit code of the scanner.
|
|
1887
2259
|
*/
|
|
2260
|
+
function runScanner(argv, ui) {
|
|
2261
|
+
return runCli$1(argv, {
|
|
2262
|
+
compile: compileStrings,
|
|
2263
|
+
exportStrings,
|
|
2264
|
+
importStrings
|
|
2265
|
+
}, ui);
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* The real seams of the keys watch: the asset scanner with the string tools, as `moku-game keys`
|
|
2269
|
+
* runs it, `fs.watch`, and the console.
|
|
2270
|
+
*
|
|
2271
|
+
* @param ui - The console of the process.
|
|
2272
|
+
* @returns The seams.
|
|
2273
|
+
*/
|
|
2274
|
+
function keysSeams(ui) {
|
|
2275
|
+
return {
|
|
2276
|
+
scan: runScanner,
|
|
2277
|
+
watch,
|
|
2278
|
+
ui
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
1888
2281
|
/**
|
|
1889
2282
|
* The real process behind the command line: the branded console, the environment the bin
|
|
1890
|
-
* started with, Bun's spawn and resolver, the signals, `fs.watch`, the
|
|
1891
|
-
* string tools, native, and the visual test runner, loaded only by
|
|
2283
|
+
* started with, Bun's spawn and resolver, the signals, `fs.watch`, the keys watch, the asset
|
|
2284
|
+
* scanner with the string tools, native, and the visual test runner, loaded only by
|
|
2285
|
+
* `moku-game visual`.
|
|
1892
2286
|
*
|
|
1893
2287
|
* @returns The seams of this process.
|
|
1894
2288
|
*/
|
|
@@ -1914,11 +2308,8 @@ function processDeps() {
|
|
|
1914
2308
|
watcher.on("error", () => watcher.close());
|
|
1915
2309
|
return watcher;
|
|
1916
2310
|
},
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
exportStrings,
|
|
1920
|
-
importStrings
|
|
1921
|
-
}, ui),
|
|
2311
|
+
watchKeys: (root, settings) => watchKeysAt(root, settings, keysSeams(ui)),
|
|
2312
|
+
assets: (argv) => runScanner(argv, ui),
|
|
1922
2313
|
native: runNative,
|
|
1923
2314
|
resolve: (specifier, from) => Bun.resolveSync(specifier, from),
|
|
1924
2315
|
loadPage: (file) => import(file),
|
|
@@ -1946,8 +2337,11 @@ async function runCli(argv) {
|
|
|
1946
2337
|
/**
|
|
1947
2338
|
* Writes a game's dev page into `<root>/.moku/` for a server the caller starts: `index.html`,
|
|
1948
2339
|
* `dev.ts`, `main.ts` with the scenarios of `tests/scenarios/` (and, with agents, the agents and
|
|
1949
|
-
* the game's `.dev` modules), and `bunfig.toml` with the engine's hot plugin. It
|
|
1950
|
-
*
|
|
2340
|
+
* the game's `.dev` modules), and `bunfig.toml` with the engine's hot plugin. It also writes the
|
|
2341
|
+
* first `assets-stamp.ts`, the empty one, when the game has none: `main.ts` imports it, and
|
|
2342
|
+
* `watchKeys` rewrites it when an asset file changes. The page accepts the new stamp: new bytes
|
|
2343
|
+
* of a file swap in place, a new or removed file reloads the page. It starts no server and no
|
|
2344
|
+
* watcher, and throws the `[game]` errors of `moku-game dev`.
|
|
1951
2345
|
*
|
|
1952
2346
|
* @param root - The game folder, relative to the cwd or absolute.
|
|
1953
2347
|
* @param options - The agents, the preloads and the extra bundler plugins.
|
|
@@ -1970,5 +2364,36 @@ async function preparePage(root, options = {}) {
|
|
|
1970
2364
|
servePlugins: absolute(options.servePlugins)
|
|
1971
2365
|
}, (specifier, from) => Bun.resolveSync(specifier, from));
|
|
1972
2366
|
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Keeps `generated/` of a game fresh while its dev page is open, for a server the caller runs:
|
|
2369
|
+
* the editor calls it next to `preparePage`, and raw `moku-game dev` runs the same watch itself.
|
|
2370
|
+
* It watches the game folder, runs the scan of `moku-game keys` once and resolves after it. Then
|
|
2371
|
+
* a save of an asset file (`.png`, `.webp`, `.fnt`, `.mp3`, `.m4a`) or of a
|
|
2372
|
+
* `strings/<locale>.json` scans again, with the same flags, outputs and layers; an output that
|
|
2373
|
+
* did not change is not rewritten. On the page a rewritten `generated/strings.<locale>.ts` hot
|
|
2374
|
+
* swaps. An asset file that is new, gone or saved again rewrites `.moku/assets-stamp.ts`, which
|
|
2375
|
+
* the page imports and accepts as a hot swap: the new bytes of a file swap in place in the loaded
|
|
2376
|
+
* bundles, with no reload, and a new or removed file reloads the page. A failed scan goes to
|
|
2377
|
+
* `onError` and the watch goes on, the first scan too. The layers of `config.ts` are read once:
|
|
2378
|
+
* a changed layer needs a new `watchKeys`.
|
|
2379
|
+
*
|
|
2380
|
+
* @param root - The game folder, relative to the cwd or absolute.
|
|
2381
|
+
* @param options - Where the message of a failed scan goes.
|
|
2382
|
+
* @returns The running watch; `close()` stops it.
|
|
2383
|
+
* @throws {Error} When the game has no `config.ts` or `index.ts`, or a config value is refused.
|
|
2384
|
+
* @example
|
|
2385
|
+
* ```ts
|
|
2386
|
+
* // The editor bin, next to preparePage, started with --root games/timber:
|
|
2387
|
+
* const { preparePage, watchKeys } = await import(Bun.resolveSync("@moku-labs/game/cli", root));
|
|
2388
|
+
* await preparePage("games/timber", { agents: ["@moku-labs/editor/agent/page"] });
|
|
2389
|
+
* const keys = await watchKeys("games/timber", { onError: message => log.warn(message) });
|
|
2390
|
+
* // generated/ is fresh; a save of features/home/strings/en.json rewrites generated/strings.en.ts
|
|
2391
|
+
* keys.close(); // when the editor closes the game
|
|
2392
|
+
* ```
|
|
2393
|
+
*/
|
|
2394
|
+
async function watchKeys(root, options = {}) {
|
|
2395
|
+
const game = path.resolve(root);
|
|
2396
|
+
return watchKeysAt(game, await loadGame(game), keysSeams(createBrandConsole()), options);
|
|
2397
|
+
}
|
|
1973
2398
|
//#endregion
|
|
1974
|
-
export { preparePage, runCli };
|
|
2399
|
+
export { preparePage, runCli, watchKeys };
|