@moku-labs/game 0.10.0 → 0.12.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 +1 -0
- package/dist/app/page.d.mts +6 -3
- package/dist/app/page.mjs +5 -2
- package/dist/app/system.d.mts +39 -24
- package/dist/app/system.mjs +71 -49
- package/dist/app.d.mts +1 -1
- package/dist/app.mjs +1 -1
- package/dist/{assets-BHwTLVta.mjs → assets-DYiKxPBX.mjs} +27 -63
- package/dist/assets.mjs +1 -1
- package/dist/cli.d.mts +3 -2
- package/dist/cli.mjs +368 -52
- package/dist/{commands-C4QlqijJ.d.mts → commands-DvFr8Uwp.d.mts} +2 -2
- package/dist/control.d.mts +1 -1
- package/dist/{index-xxSxKJJW.d.mts → index-rnOYAriI.d.mts} +57 -4
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +2 -2
- package/dist/inspect.d.mts +3 -3
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/lint.mjs +3 -1
- package/dist/{runtime-DOBYM6g4.d.mts → runtime-B276Gg0s.d.mts} +1 -1
- package/dist/{src-DSl1eNB1.mjs → src-SmT-XxVf.mjs} +78 -15
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/{types-CefgYzFt.d.mts → types-Bcdo5-2w.d.mts} +1 -1
- package/dist/{types-BUwDHKXm.d.mts → types-BrTpz9oL.d.mts} +1 -1
- package/dist/{types-hQgI_wrq.d.mts → types-DLghaGEy.d.mts} +51 -9
- package/dist/{types-BM4shh8K.d.mts → types-D_2G6Q68.d.mts} +1 -1
- package/dist/visual.d.mts +9 -6
- package/dist/visual.mjs +7 -3
- package/llms.txt +9 -7
- package/package.json +3 -3
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as resolveConfig } from "./config-CdARrrhU.mjs";
|
|
2
|
-
import { a as runCli$1, i as compileStrings, n as importStrings, t as exportStrings } from "./assets-
|
|
2
|
+
import { a as runCli$1, i as compileStrings, n as importStrings, t as exportStrings } from "./assets-DYiKxPBX.mjs";
|
|
3
3
|
import { constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createBrandConsole } from "@moku-labs/common/cli";
|
|
@@ -13,6 +13,17 @@ const SAFE_VALUE = /^[#\w(),.%\s-]+$/;
|
|
|
13
13
|
/** The files of `tests/scenarios/` that are not scenarios. */
|
|
14
14
|
const NOT_SCENARIO = /\.(?:test|spec|d)\.ts$|^index\.ts$/;
|
|
15
15
|
/**
|
|
16
|
+
* The entry of `@moku-labs/system` each system plugin loads from, in the order the shell composes
|
|
17
|
+
* them.
|
|
18
|
+
*/
|
|
19
|
+
const SYSTEM_ENTRIES = [
|
|
20
|
+
["lifecycle", "lifecycle"],
|
|
21
|
+
["back", "back"],
|
|
22
|
+
["haptics", "haptics"],
|
|
23
|
+
["keepAwake", "keep-awake"],
|
|
24
|
+
["store", "store"]
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
16
27
|
* Escapes the five characters HTML reads as markup.
|
|
17
28
|
*
|
|
18
29
|
* @param text - The text.
|
|
@@ -133,7 +144,7 @@ function scenarioFiles(files) {
|
|
|
133
144
|
* Whether the page needs the system shell: a system plugin is named, or the save is the store.
|
|
134
145
|
*
|
|
135
146
|
* @param settings - The resolved `config.ts`.
|
|
136
|
-
* @returns True when `main.ts` imports `
|
|
147
|
+
* @returns True when `main.ts` imports `systemShellOf`.
|
|
137
148
|
* @example
|
|
138
149
|
* ```ts
|
|
139
150
|
* needsShell(resolveConfig({ page: { title: "T" }, save: "store" })); // true
|
|
@@ -143,6 +154,26 @@ function needsShell(settings) {
|
|
|
143
154
|
return settings.system.length > 0 || settings.save === "store";
|
|
144
155
|
}
|
|
145
156
|
/**
|
|
157
|
+
* The `system` option of `startPage`: `systemShellOf` over the loader of the package and one
|
|
158
|
+
* loader per plugin the shell composes, in the fixed order. The store is one when `config.system`
|
|
159
|
+
* names it or the save is the store. Only these `import()`s are written, so the page bundles no
|
|
160
|
+
* other plugin and never needs its native package.
|
|
161
|
+
*
|
|
162
|
+
* @param settings - The resolved `config.ts`.
|
|
163
|
+
* @returns The option, indented two spaces, with no comma after it.
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* shellOption(resolveConfig({ page: { title: "T" }, system: ["haptics"] })).split("\n")[2]; // ' haptics: () => import("@moku-labs/system/haptics")'
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
function shellOption(settings) {
|
|
170
|
+
return [
|
|
171
|
+
" system: systemShellOf({",
|
|
172
|
+
[" system: () => import(\"@moku-labs/system\")", ...SYSTEM_ENTRIES.filter(([name]) => settings.system.includes(name) || name === "store" && settings.save === "store").map(([name, entry]) => ` ${name}: () => import("@moku-labs/system/${entry}")`)].join(",\n"),
|
|
173
|
+
" })"
|
|
174
|
+
].join("\n");
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
146
177
|
* The imports of `main.ts` every page has: the page entry, the shell when needed, the game and its
|
|
147
178
|
* config.
|
|
148
179
|
*
|
|
@@ -157,7 +188,7 @@ function needsShell(settings) {
|
|
|
157
188
|
function pageImports(settings, toRoot) {
|
|
158
189
|
return [
|
|
159
190
|
"import { startPage } from \"@moku-labs/game/app/page\";",
|
|
160
|
-
...needsShell(settings) ? ["import {
|
|
191
|
+
...needsShell(settings) ? ["import { systemShellOf } from \"@moku-labs/game/app/system\";"] : [],
|
|
161
192
|
`import game from "${toRoot}index.ts";`,
|
|
162
193
|
`import config from "${toRoot}config.ts";`
|
|
163
194
|
];
|
|
@@ -194,34 +225,36 @@ function importLine(binding, specifier) {
|
|
|
194
225
|
/**
|
|
195
226
|
* The `main.ts` of the dev page: the dev flag first, then the page entry, the game, its config,
|
|
196
227
|
* one import per scenario, and, with agents, one per `.dev` module and one per agent. It calls
|
|
197
|
-
* `startPage` with the scenarios by file stem, the shell
|
|
198
|
-
* the `.dev` modules.
|
|
228
|
+
* `startPage` with the scenarios by file stem, the shell with the loaders of the named plugins
|
|
229
|
+
* when the game needs it, the agents and the `.dev` modules.
|
|
199
230
|
*
|
|
200
231
|
* @param settings - The resolved `config.ts`.
|
|
201
232
|
* @param sources - The scenario files, the `.dev` modules and the agents.
|
|
233
|
+
* @param toRoot - The path from `main.ts` to the game folder: `"../"` for `.moku/`, `"../../"`
|
|
234
|
+
* for the page of `moku-game visual` in `.moku/visual/`.
|
|
202
235
|
* @returns The text of `.moku/main.ts`.
|
|
203
236
|
* @example
|
|
204
237
|
* ```ts
|
|
205
238
|
* devMain(resolveConfig({ page: { title: "T" } }), { scenarios: ["ready.ts"], devModules: [] }).includes('scenarios: { "ready": scenario0 }'); // true
|
|
206
239
|
* ```
|
|
207
240
|
*/
|
|
208
|
-
function devMain(settings, sources) {
|
|
241
|
+
function devMain(settings, sources, toRoot = "../") {
|
|
209
242
|
const scenarios = scenarioFiles(sources.scenarios);
|
|
210
243
|
const agents = sources.agents ?? [];
|
|
211
244
|
const devModules = agents.length === 0 ? [] : sources.devModules.toSorted();
|
|
212
245
|
const keys = scenarios.map((file, index) => `${JSON.stringify(file.slice(0, -3))}: scenario${index}`);
|
|
213
246
|
const options = [
|
|
214
247
|
` scenarios: ${keys.length === 0 ? "{}" : `{ ${keys.join(", ")} }`}`,
|
|
215
|
-
...needsShell(settings) ? [
|
|
248
|
+
...needsShell(settings) ? [shellOption(settings)] : [],
|
|
216
249
|
...agents.length === 0 ? [] : [` agents: [${numbered("agent", agents.length)}]`],
|
|
217
250
|
...devModules.length === 0 ? [] : [` devModules: [${numbered("devModule", devModules.length)}]`]
|
|
218
251
|
];
|
|
219
252
|
return [
|
|
220
253
|
WRITTEN,
|
|
221
254
|
"import \"./dev.ts\";",
|
|
222
|
-
...pageImports(settings,
|
|
223
|
-
...scenarios.map((file, index) => importLine(`scenario${index}`,
|
|
224
|
-
...devModules.map((file, index) => importLine(`* as devModule${index}`,
|
|
255
|
+
...pageImports(settings, toRoot),
|
|
256
|
+
...scenarios.map((file, index) => importLine(`scenario${index}`, `${toRoot}tests/scenarios/${file}`)),
|
|
257
|
+
...devModules.map((file, index) => importLine(`* as devModule${index}`, `${toRoot}${file}`)),
|
|
225
258
|
...agents.map((agent, index) => importLine(`agent${index}`, agent)),
|
|
226
259
|
"",
|
|
227
260
|
"await startPage(game, config, {",
|
|
@@ -232,8 +265,8 @@ function devMain(settings, sources) {
|
|
|
232
265
|
}
|
|
233
266
|
/**
|
|
234
267
|
* The `main.ts` of the production page: the page entry, the game and its config, and the shell
|
|
235
|
-
* when the game needs it. No dev flag, no scenario, no
|
|
236
|
-
* them ships.
|
|
268
|
+
* with the loaders of the named plugins when the game needs it. No dev flag, no scenario, no
|
|
269
|
+
* agent, no `.dev` module, no door: none of them ships.
|
|
237
270
|
*
|
|
238
271
|
* @param settings - The resolved `config.ts`.
|
|
239
272
|
* @returns The text of the in-memory `.moku/build/main.ts`.
|
|
@@ -243,11 +276,15 @@ function devMain(settings, sources) {
|
|
|
243
276
|
* ```
|
|
244
277
|
*/
|
|
245
278
|
function buildMain(settings) {
|
|
246
|
-
const call = needsShell(settings) ?
|
|
279
|
+
const call = needsShell(settings) ? [
|
|
280
|
+
"await startPage(game, config, {",
|
|
281
|
+
shellOption(settings),
|
|
282
|
+
"});"
|
|
283
|
+
] : ["await startPage(game, config);"];
|
|
247
284
|
return [
|
|
248
285
|
...pageImports(settings, "../../"),
|
|
249
286
|
"",
|
|
250
|
-
call,
|
|
287
|
+
...call,
|
|
251
288
|
""
|
|
252
289
|
].join("\n");
|
|
253
290
|
}
|
|
@@ -479,47 +516,52 @@ function writeIfChanged(file, text) {
|
|
|
479
516
|
* @param root - The game folder.
|
|
480
517
|
* @param settings - The resolved config.
|
|
481
518
|
* @param agents - The agent modules, when the page has any.
|
|
519
|
+
* @param toRoot - The path from `main.ts` to the game folder.
|
|
482
520
|
* @returns The text.
|
|
483
521
|
*/
|
|
484
|
-
function mainOf(root, settings, agents) {
|
|
522
|
+
function mainOf(root, settings, agents, toRoot = "../") {
|
|
485
523
|
const scenarios = scenarioNamesOf(root);
|
|
486
524
|
return agents === void 0 || agents.length === 0 ? devMain(settings, {
|
|
487
525
|
scenarios,
|
|
488
526
|
devModules: []
|
|
489
|
-
}) : devMain(settings, {
|
|
527
|
+
}, toRoot) : devMain(settings, {
|
|
490
528
|
scenarios,
|
|
491
529
|
devModules: devModulesOf(root),
|
|
492
530
|
agents
|
|
493
|
-
});
|
|
531
|
+
}, toRoot);
|
|
494
532
|
}
|
|
495
533
|
/**
|
|
496
534
|
* Writes the dev page of a checked game into `<game>/.moku/`: `index.html`, `dev.ts`, `main.ts`
|
|
497
|
-
* and `bunfig.toml`.
|
|
498
|
-
*
|
|
535
|
+
* and `bunfig.toml`. `moku-game visual` writes its own page into `.moku/visual/`, so the page of
|
|
536
|
+
* `dev` or of the editor stays as it is; the links of the page reach the game from its depth.
|
|
537
|
+
* Every text is made before the first write, so a refused value writes nothing.
|
|
499
538
|
*
|
|
500
539
|
* @param root - The game folder, absolute.
|
|
501
540
|
* @param settings - The resolved config.
|
|
502
541
|
* @param options - The agents, the preloads and the extra plugins, absolute.
|
|
503
542
|
* @param resolve - Bun's resolver, to find the hot plugin.
|
|
543
|
+
* @param page - The folder of the page under the game, `/` separated: `.moku` by default.
|
|
504
544
|
* @returns The paths of the HTML and the bunfig.
|
|
505
545
|
* @throws {Error} When the hot plugin does not resolve, a preload or plugin is not a file, or a
|
|
506
546
|
* page value is refused.
|
|
507
547
|
*/
|
|
508
|
-
function writePage(root, settings, options, resolve) {
|
|
548
|
+
function writePage(root, settings, options, resolve, page = ".moku") {
|
|
509
549
|
const preload = options.preload ?? [];
|
|
510
550
|
const servePlugins = options.servePlugins ?? [];
|
|
511
551
|
checkFiles("--preload", preload);
|
|
512
552
|
checkFiles("--serve-plugin", servePlugins);
|
|
513
|
-
const
|
|
553
|
+
const segments = page.split("/");
|
|
554
|
+
const toRoot = "../".repeat(segments.length);
|
|
555
|
+
const folder = path.join(root, ...segments);
|
|
514
556
|
const html = path.join(folder, "index.html");
|
|
515
557
|
const bunfig = path.join(folder, "bunfig.toml");
|
|
516
558
|
const texts = [
|
|
517
559
|
[html, pageHtml(settings.page, {
|
|
518
|
-
toRoot
|
|
560
|
+
toRoot,
|
|
519
561
|
script: "./main.ts"
|
|
520
562
|
})],
|
|
521
563
|
[path.join(folder, "dev.ts"), devFlag()],
|
|
522
|
-
[path.join(folder, "main.ts"), mainOf(root, settings, options.agents)],
|
|
564
|
+
[path.join(folder, "main.ts"), mainOf(root, settings, options.agents, toRoot)],
|
|
523
565
|
[bunfig, bunfigText({
|
|
524
566
|
hotPlugin: hotPluginOf(root, resolve),
|
|
525
567
|
servePlugins,
|
|
@@ -626,8 +668,25 @@ async function serveParent(run, deps) {
|
|
|
626
668
|
if (!run.packed && !hasManifest) deps.ui.warn(`[game] dev: no manifest.json in "${run.root}".\n Run "moku-game keys" first.`);
|
|
627
669
|
const page = writePage(run.root, settings, run, deps.resolve);
|
|
628
670
|
if (!ignoresMoku(run.root)) deps.ui.warn("[game] dev: add \".moku/\" to .gitignore. moku-game writes its dev page there.");
|
|
629
|
-
|
|
671
|
+
return runChild(devChildCommand(run, page, deps), deps, {
|
|
630
672
|
cwd: run.root,
|
|
673
|
+
whileRunning: () => watchScenarios(run.root, settings, deps)
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Runs the bin again as a marked child, `MOKU_GAME_CHILD=1`, in its own process group: Ctrl+C
|
|
678
|
+
* reaches the parent, which hands it and the other stop signals on. `dev` and `visual` run their
|
|
679
|
+
* page server this way, under the page's bunfig.
|
|
680
|
+
*
|
|
681
|
+
* @param command - The words of the command: the Bun, its flags, the bin and its arguments.
|
|
682
|
+
* @param deps - The spawn, the signals and the environment.
|
|
683
|
+
* @param child - The working directory, and what lives as long as the child.
|
|
684
|
+
* @returns The exit code of the child.
|
|
685
|
+
* @throws {Error} When the child does not start.
|
|
686
|
+
*/
|
|
687
|
+
async function runChild(command, deps, child) {
|
|
688
|
+
const started = deps.spawn(command, {
|
|
689
|
+
cwd: child.cwd,
|
|
631
690
|
env: {
|
|
632
691
|
...deps.env,
|
|
633
692
|
MOKU_GAME_CHILD: "1"
|
|
@@ -639,11 +698,11 @@ async function serveParent(run, deps) {
|
|
|
639
698
|
],
|
|
640
699
|
detached: true
|
|
641
700
|
});
|
|
642
|
-
const removers = FORWARDED.map((signal) => deps.onSignal(signal, () =>
|
|
701
|
+
const removers = FORWARDED.map((signal) => deps.onSignal(signal, () => started.kill(signal)));
|
|
643
702
|
let watcher;
|
|
644
703
|
try {
|
|
645
|
-
watcher =
|
|
646
|
-
return await
|
|
704
|
+
watcher = child.whileRunning?.();
|
|
705
|
+
return await started.exited;
|
|
647
706
|
} finally {
|
|
648
707
|
watcher?.close();
|
|
649
708
|
for (const remove of removers) remove();
|
|
@@ -696,10 +755,11 @@ function fileResponse(file) {
|
|
|
696
755
|
}
|
|
697
756
|
/**
|
|
698
757
|
* Starts the dev server on `127.0.0.1` only, never on every interface: the page on `/`, the
|
|
699
|
-
* manifest of the served folder on `/manifest.json`, and its files as static files.
|
|
758
|
+
* manifest of the served folder on `/manifest.json`, and its files as static files. The dev child
|
|
759
|
+
* and `moku-game visual` serve the page with it.
|
|
700
760
|
*
|
|
701
761
|
* @param run - The flags of the run.
|
|
702
|
-
* @param page - The page bundle the
|
|
762
|
+
* @param page - The page bundle the process imported.
|
|
703
763
|
* @returns The server.
|
|
704
764
|
* @throws {Error} When the port is in use.
|
|
705
765
|
*/
|
|
@@ -722,6 +782,22 @@ function serveOn(run, page) {
|
|
|
722
782
|
}
|
|
723
783
|
}
|
|
724
784
|
/**
|
|
785
|
+
* Watches the parent of a child run: a parent that died hands the child to another process, so
|
|
786
|
+
* its parent id changes. The dev child and the visual child stop their server then.
|
|
787
|
+
*
|
|
788
|
+
* @param onGone - Called once when the parent is gone.
|
|
789
|
+
* @returns Stops watching.
|
|
790
|
+
*/
|
|
791
|
+
function watchParent(onGone) {
|
|
792
|
+
const parent = process.ppid;
|
|
793
|
+
const timer = setInterval(() => {
|
|
794
|
+
if (process.ppid === parent) return;
|
|
795
|
+
clearInterval(timer);
|
|
796
|
+
onGone();
|
|
797
|
+
}, PARENT_CHECK_MS);
|
|
798
|
+
return () => clearInterval(timer);
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
725
801
|
* Waits until the dev child should stop: Ctrl+C, a stop signal, or a parent that died (its
|
|
726
802
|
* parent id changed). Then stops the server.
|
|
727
803
|
*
|
|
@@ -730,17 +806,14 @@ function serveOn(run, page) {
|
|
|
730
806
|
* @returns Resolves to the exit code `0` once the server stopped.
|
|
731
807
|
*/
|
|
732
808
|
function untilStopped(server, deps) {
|
|
733
|
-
const parent = process.ppid;
|
|
734
809
|
return new Promise((resolve) => {
|
|
735
810
|
const removers = [];
|
|
736
|
-
const timer = setInterval(() => {
|
|
737
|
-
if (process.ppid !== parent) stop();
|
|
738
|
-
}, PARENT_CHECK_MS);
|
|
739
811
|
const stop = () => {
|
|
740
|
-
|
|
812
|
+
unwatch();
|
|
741
813
|
for (const remove of removers.splice(0)) remove();
|
|
742
814
|
resolve(server.stop(true).then(() => 0));
|
|
743
815
|
};
|
|
816
|
+
const unwatch = watchParent(stop);
|
|
744
817
|
for (const signal of STOPPING) removers.push(deps.onSignal(signal, stop));
|
|
745
818
|
});
|
|
746
819
|
}
|
|
@@ -1051,13 +1124,188 @@ async function runBuild(run, deps) {
|
|
|
1051
1124
|
return 0;
|
|
1052
1125
|
}
|
|
1053
1126
|
//#endregion
|
|
1127
|
+
//#region src/app/visual.ts
|
|
1128
|
+
/**
|
|
1129
|
+
* @file `moku-game visual`: the visual tests of a game from the bin. It imports the tests module,
|
|
1130
|
+
* `tests/visual/index.ts` by default, whose default export is `{ app, tests }`, the two arguments
|
|
1131
|
+
* of `runVisualTests`: `app` is a `VisualSetup`. The flags of the runner are read by
|
|
1132
|
+
* `parseVisualArgv`. When the pixel leg runs without `--url`, the command writes its own dev page
|
|
1133
|
+
* into `.moku/visual/`, so the page of `dev` or of the editor stays as it is, runs the bin again
|
|
1134
|
+
* under that page's bunfig as `dev` does, and the child serves the page with the server of `dev`
|
|
1135
|
+
* on a free port and stops it after. Node and Bun only: the bin bundles it.
|
|
1136
|
+
*/
|
|
1137
|
+
/** The baselines of a game, under the game folder. */
|
|
1138
|
+
const BASELINES = path.join("tests", "visual", "baselines");
|
|
1139
|
+
/** The folder of the visual page under the game: its own, beside the page of `dev`. */
|
|
1140
|
+
const VISUAL_PAGE = ".moku/visual";
|
|
1141
|
+
/**
|
|
1142
|
+
* Tells whether a value is a setup of the runner: an object with an `app` function.
|
|
1143
|
+
*
|
|
1144
|
+
* @param value - The `app` of the module.
|
|
1145
|
+
* @returns True for a `VisualSetup`.
|
|
1146
|
+
*/
|
|
1147
|
+
function isSetup(value) {
|
|
1148
|
+
return typeof value === "object" && value !== null && "app" in value && typeof value.app === "function";
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Reads the default export of a tests module: `app` is the setup of the runner, `{ app, page? }`,
|
|
1152
|
+
* and `tests` is a list. Each test is checked by the runner.
|
|
1153
|
+
*
|
|
1154
|
+
* @param value - The default export.
|
|
1155
|
+
* @returns The suite, or `undefined` for another shape.
|
|
1156
|
+
*/
|
|
1157
|
+
function suiteOf(value) {
|
|
1158
|
+
if (typeof value !== "object" || value === null || !("app" in value) || !("tests" in value)) return;
|
|
1159
|
+
const { app, tests } = value;
|
|
1160
|
+
return Array.isArray(tests) && isSetup(app) ? {
|
|
1161
|
+
setup: app,
|
|
1162
|
+
tests
|
|
1163
|
+
} : void 0;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Imports the tests module of the run.
|
|
1167
|
+
*
|
|
1168
|
+
* @param file - The module, absolute.
|
|
1169
|
+
* @param cwd - The working directory, to name the module in the message.
|
|
1170
|
+
* @returns The setup and the tests.
|
|
1171
|
+
* @throws {Error} When the module is missing or its default export is not `{ app, tests }`.
|
|
1172
|
+
*/
|
|
1173
|
+
async function loadSuite(file, cwd) {
|
|
1174
|
+
const suite = suiteOf((statSync(file, { throwIfNoEntry: false })?.isFile() === true ? await import(pathToFileURL(file).href) : void 0)?.default);
|
|
1175
|
+
if (suite === void 0) throw new Error(`[game] visual: ${path.relative(cwd, file)} must export default { app, tests }.\n app is a VisualSetup { app, page? }: export default { app: { app: () => game.screen().app }, tests }.`);
|
|
1176
|
+
return suite;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Writes the visual page of the game into `.moku/visual/`: `index.html`, `dev.ts`, `main.ts` and
|
|
1180
|
+
* `bunfig.toml`, the hot plugin, the serve plugins and the preloads in it, as `dev` writes its page.
|
|
1181
|
+
*
|
|
1182
|
+
* @param page - The game folder, the preloads and the serve plugins.
|
|
1183
|
+
* @param resolve - Bun's resolver, to find the hot plugin.
|
|
1184
|
+
* @returns The paths of the HTML and the bunfig.
|
|
1185
|
+
* @throws {Error} The errors of `moku-game dev` before its server starts, and a game without
|
|
1186
|
+
* `manifest.json`.
|
|
1187
|
+
*/
|
|
1188
|
+
async function writeVisualPage(page, resolve) {
|
|
1189
|
+
const settings = await loadGame(page.root);
|
|
1190
|
+
const manifest = path.join(page.root, "manifest.json");
|
|
1191
|
+
if (statSync(manifest, { throwIfNoEntry: false })?.isFile() !== true) throw new Error(`[game] visual: no manifest.json in "${page.root}".\n Run "moku-game keys" first.`);
|
|
1192
|
+
return writePage(page.root, settings, {
|
|
1193
|
+
preload: page.preload,
|
|
1194
|
+
servePlugins: page.servePlugins
|
|
1195
|
+
}, resolve, VISUAL_PAGE);
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Writes the visual page and serves it in this process on a free port, as the dev child serves
|
|
1199
|
+
* its page. It runs in the child that runs under the page's bunfig, so the hot plugin, the serve
|
|
1200
|
+
* plugins and the preloads apply.
|
|
1201
|
+
*
|
|
1202
|
+
* @param page - The game folder, the preloads and the serve plugins.
|
|
1203
|
+
* @param deps - Bun's resolver and the page loader.
|
|
1204
|
+
* @returns The running server.
|
|
1205
|
+
* @throws {Error} The errors of `writeVisualPage`.
|
|
1206
|
+
*/
|
|
1207
|
+
async function serveVisualPage(page, deps) {
|
|
1208
|
+
const files = await writeVisualPage(page, deps.resolve);
|
|
1209
|
+
const loaded = await deps.loadPage(files.html);
|
|
1210
|
+
return serveOn({
|
|
1211
|
+
root: page.root,
|
|
1212
|
+
port: 0,
|
|
1213
|
+
packed: false,
|
|
1214
|
+
preload: page.preload,
|
|
1215
|
+
servePlugins: page.servePlugins
|
|
1216
|
+
}, loaded.default);
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Runs the bin again for a run that has to: under the visual page's bunfig when the page is
|
|
1220
|
+
* served, so it is bundled as `dev` bundles its page; with Bun's preloads otherwise.
|
|
1221
|
+
*
|
|
1222
|
+
* @param command - The run.
|
|
1223
|
+
* @param serves - True when the page is served for the run.
|
|
1224
|
+
* @param deps - The process seams.
|
|
1225
|
+
* @returns The exit code of the child.
|
|
1226
|
+
* @throws {Error} The errors of `writeVisualPage`.
|
|
1227
|
+
*/
|
|
1228
|
+
async function rerun(command, serves, deps) {
|
|
1229
|
+
const page = serves ? await writeVisualPage(command, deps.resolve) : void 0;
|
|
1230
|
+
const bun = page === void 0 ? command.preload.map((file) => `--preload=${file}`) : [`--config=${page.bunfig}`];
|
|
1231
|
+
return runChild([
|
|
1232
|
+
deps.execPath,
|
|
1233
|
+
...bun,
|
|
1234
|
+
...deps.self,
|
|
1235
|
+
...command.argv
|
|
1236
|
+
], deps, { cwd: deps.cwd });
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Plays the tests in this process: the headless leg, then the pixel leg against `--url` or the
|
|
1240
|
+
* page served here, which stops when the run ends or the parent of the run is gone.
|
|
1241
|
+
*
|
|
1242
|
+
* @param command - The run.
|
|
1243
|
+
* @param run - The runner, its flags, and whether the pixel leg runs.
|
|
1244
|
+
* @param deps - The process seams.
|
|
1245
|
+
* @returns `0` when the report is ok, `1` otherwise.
|
|
1246
|
+
* @throws {Error} When the tests module is refused, the page cannot be served, or the runner
|
|
1247
|
+
* throws.
|
|
1248
|
+
*/
|
|
1249
|
+
async function play(command, run, deps) {
|
|
1250
|
+
const suite = await loadSuite(command.tests, deps.cwd);
|
|
1251
|
+
const dir = path.resolve(deps.cwd, run.flags.dir ?? path.join(command.root, BASELINES));
|
|
1252
|
+
const server = run.serves ? await serveVisualPage(command, deps) : void 0;
|
|
1253
|
+
const unwatch = server === void 0 ? void 0 : watchParent(() => server.stop(true));
|
|
1254
|
+
const url = command.url ?? server?.url.href;
|
|
1255
|
+
try {
|
|
1256
|
+
const setup = url === void 0 ? suite.setup : {
|
|
1257
|
+
...suite.setup,
|
|
1258
|
+
page: {
|
|
1259
|
+
...suite.setup.page,
|
|
1260
|
+
url
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
return (await run.visual.runVisualTests(setup, suite.tests, {
|
|
1264
|
+
...run.flags,
|
|
1265
|
+
pixels: run.pixels,
|
|
1266
|
+
dir,
|
|
1267
|
+
argv: []
|
|
1268
|
+
})).ok ? 0 : 1;
|
|
1269
|
+
} finally {
|
|
1270
|
+
unwatch?.();
|
|
1271
|
+
await server?.stop(true);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Runs `moku-game visual`: the headless leg, then the pixel leg on a Mac (or with `--pixels`)
|
|
1276
|
+
* against `--url` or the page served for the run. A run that serves the page runs the bin again
|
|
1277
|
+
* under `.moku/visual/bunfig.toml`, as `dev` does, and a run with `--preload` with the preloads;
|
|
1278
|
+
* the marked child plays the tests. The baselines live in `<game>/tests/visual/baselines/` unless
|
|
1279
|
+
* `--dir` names another folder.
|
|
1280
|
+
*
|
|
1281
|
+
* @param command - The run.
|
|
1282
|
+
* @param deps - The process seams.
|
|
1283
|
+
* @returns `0` when the report is ok, `1` otherwise, or the exit code of the child.
|
|
1284
|
+
* @throws {Error} When the tests module is refused, a flag misses its value, the page cannot be
|
|
1285
|
+
* served, or the runner throws.
|
|
1286
|
+
*/
|
|
1287
|
+
async function runVisual(command, deps) {
|
|
1288
|
+
const visual = await deps.visual();
|
|
1289
|
+
const flags = visual.parseVisualArgv(command.flags);
|
|
1290
|
+
const pixels = flags.pixels ?? process.platform === "darwin";
|
|
1291
|
+
const serves = pixels && command.url === void 0;
|
|
1292
|
+
if (!(deps.env.MOKU_GAME_CHILD === "1") && (serves || command.preload.length > 0)) return rerun(command, serves, deps);
|
|
1293
|
+
return play(command, {
|
|
1294
|
+
visual,
|
|
1295
|
+
flags,
|
|
1296
|
+
pixels,
|
|
1297
|
+
serves
|
|
1298
|
+
}, deps);
|
|
1299
|
+
}
|
|
1300
|
+
//#endregion
|
|
1054
1301
|
//#region src/app/cli.ts
|
|
1055
1302
|
/**
|
|
1056
1303
|
* @file The command line of `moku-game`: the arguments read per command with `node:util`
|
|
1057
|
-
* `parseArgs`, then one of `dev`, `build`, `native <verb>`, `keys`, `pack` or `help`.
|
|
1058
|
-
* of the process (the console, the environment, spawning, signals, the watcher, the
|
|
1059
|
-
* scanner, native, Bun's resolver) comes in as `deps`, so a test
|
|
1060
|
-
* returns the exit code and never exits itself. A plain function,
|
|
1304
|
+
* `parseArgs`, then one of `dev`, `build`, `native <verb>`, `keys`, `pack`, `visual` or `help`.
|
|
1305
|
+
* Every seam of the process (the console, the environment, spawning, signals, the watcher, the
|
|
1306
|
+
* asset scanner, native, Bun's resolver, the visual test runner) comes in as `deps`, so a test
|
|
1307
|
+
* runs a command with stubs. It returns the exit code and never exits itself. A plain function,
|
|
1308
|
+
* on no API.
|
|
1061
1309
|
*/
|
|
1062
1310
|
/** The flags every command takes. */
|
|
1063
1311
|
const GLOBAL_OPTIONS = [
|
|
@@ -1086,15 +1334,38 @@ const ALL_OPTIONS = {
|
|
|
1086
1334
|
out: { type: "string" },
|
|
1087
1335
|
simulator: { type: "boolean" },
|
|
1088
1336
|
check: { type: "boolean" },
|
|
1089
|
-
"no-cache": { type: "boolean" }
|
|
1337
|
+
"no-cache": { type: "boolean" },
|
|
1338
|
+
tests: { type: "string" },
|
|
1339
|
+
url: { type: "string" },
|
|
1340
|
+
update: { type: "boolean" },
|
|
1341
|
+
only: {
|
|
1342
|
+
type: "string",
|
|
1343
|
+
multiple: true
|
|
1344
|
+
},
|
|
1345
|
+
"no-pixels": { type: "boolean" },
|
|
1346
|
+
pixels: { type: "boolean" },
|
|
1347
|
+
dir: { type: "string" },
|
|
1348
|
+
webgl: { type: "boolean" }
|
|
1090
1349
|
};
|
|
1350
|
+
/** The flags of `visual` that `parseVisualArgv` reads, in the runner's words. */
|
|
1351
|
+
const VISUAL_FLAGS = /^(?:update|only|no-pixels|pixels|dir|webgl)$/;
|
|
1091
1352
|
/** The flags of one command besides the global ones. */
|
|
1092
1353
|
const COMMAND_OPTIONS = {
|
|
1093
1354
|
dev: ["port", "packed"],
|
|
1094
1355
|
build: ["out"],
|
|
1095
1356
|
native: ["simulator"],
|
|
1096
1357
|
keys: ["check"],
|
|
1097
|
-
pack: ["no-cache"]
|
|
1358
|
+
pack: ["no-cache"],
|
|
1359
|
+
visual: [
|
|
1360
|
+
"tests",
|
|
1361
|
+
"url",
|
|
1362
|
+
"update",
|
|
1363
|
+
"only",
|
|
1364
|
+
"no-pixels",
|
|
1365
|
+
"pixels",
|
|
1366
|
+
"dir",
|
|
1367
|
+
"webgl"
|
|
1368
|
+
]
|
|
1098
1369
|
};
|
|
1099
1370
|
/** The native verbs, in the order the messages name them. */
|
|
1100
1371
|
const NATIVE_VERBS = [
|
|
@@ -1103,6 +1374,10 @@ const NATIVE_VERBS = [
|
|
|
1103
1374
|
"doctor",
|
|
1104
1375
|
"clean"
|
|
1105
1376
|
];
|
|
1377
|
+
/** The command words, in the order the messages name them. */
|
|
1378
|
+
const COMMAND_NAMES = "dev, build, native, keys, pack, visual, help";
|
|
1379
|
+
/** The tests module of a game, under the game folder. */
|
|
1380
|
+
const VISUAL_TESTS = path.join("tests", "visual", "index.ts");
|
|
1106
1381
|
/** The port of `moku-game dev` without `--port`. */
|
|
1107
1382
|
const DEFAULT_PORT = "3000";
|
|
1108
1383
|
/** The highest port number. */
|
|
@@ -1118,6 +1393,9 @@ const USAGE = [
|
|
|
1118
1393
|
" native doctor | native clean check or remove the native project",
|
|
1119
1394
|
" keys [--check] write generated/assets.ts and manifest.json",
|
|
1120
1395
|
" pack [--no-cache] pack the assets into dist/assets",
|
|
1396
|
+
" visual [--update] [--only <name>] run tests/visual/index.ts: headless, pixels on a Mac",
|
|
1397
|
+
" [--no-pixels | --pixels] [--webgl] [--dir tests/visual/baselines]",
|
|
1398
|
+
" [--tests tests/visual/index.ts] [--url <url>]",
|
|
1121
1399
|
" help print this text",
|
|
1122
1400
|
"",
|
|
1123
1401
|
"Every command: --root <dir> (default .), --preload <path>, --serve-plugin <path>."
|
|
@@ -1126,7 +1404,7 @@ const USAGE = [
|
|
|
1126
1404
|
* Tells whether a word is a command of the line that runs something.
|
|
1127
1405
|
*
|
|
1128
1406
|
* @param word - The first positional argument.
|
|
1129
|
-
* @returns True for `dev`, `build`, `native`, `keys` and `
|
|
1407
|
+
* @returns True for `dev`, `build`, `native`, `keys`, `pack` and `visual`.
|
|
1130
1408
|
*/
|
|
1131
1409
|
function isCommand(word) {
|
|
1132
1410
|
return Object.hasOwn(COMMAND_OPTIONS, word);
|
|
@@ -1203,7 +1481,7 @@ function parseCommand(argv, cwd) {
|
|
|
1203
1481
|
const word = loose.positionals[0];
|
|
1204
1482
|
if (loose.values.help === true || word === "help") return "help";
|
|
1205
1483
|
if (word === void 0) return void 0;
|
|
1206
|
-
if (!isCommand(word)) throw new Error(`[game] moku-game: no command "${word}". Name one of
|
|
1484
|
+
if (!isCommand(word)) throw new Error(`[game] moku-game: no command "${word}". Name one of ${COMMAND_NAMES}.`);
|
|
1207
1485
|
checkTokens(word, loose.tokens);
|
|
1208
1486
|
const { values, positionals } = parseStrict(argv);
|
|
1209
1487
|
const resolveAll = (files) => (files ?? []).map((file) => path.resolve(cwd, file));
|
|
@@ -1319,6 +1597,41 @@ async function runScan(parsed, deps) {
|
|
|
1319
1597
|
return deps.assets(argv);
|
|
1320
1598
|
}
|
|
1321
1599
|
/**
|
|
1600
|
+
* The flags of `visual` that `parseVisualArgv` reads, in the order given, each value after its
|
|
1601
|
+
* flag: `--dir=shots` becomes `--dir shots`.
|
|
1602
|
+
*
|
|
1603
|
+
* @param argv - The arguments after the bin, their flags checked.
|
|
1604
|
+
* @returns The flags of the runner.
|
|
1605
|
+
*/
|
|
1606
|
+
function visualFlags(argv) {
|
|
1607
|
+
return parseLoose(argv).tokens.flatMap((token) => {
|
|
1608
|
+
if (token.kind !== "option" || !VISUAL_FLAGS.test(token.name)) return [];
|
|
1609
|
+
return token.value === void 0 ? [`--${token.name}`] : [`--${token.name}`, token.value];
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Runs `moku-game visual` with the tests module and the baselines of the game.
|
|
1614
|
+
*
|
|
1615
|
+
* @param argv - The arguments after the bin.
|
|
1616
|
+
* @param parsed - The command line.
|
|
1617
|
+
* @param deps - The process seams.
|
|
1618
|
+
* @returns `0` when every checkpoint passed, else `1`.
|
|
1619
|
+
*/
|
|
1620
|
+
function runVisualCommand(argv, parsed, deps) {
|
|
1621
|
+
refuseExtra(parsed, 0);
|
|
1622
|
+
const tests = parsed.values.tests ?? path.join(parsed.root, VISUAL_TESTS);
|
|
1623
|
+
const url = parsed.values.url;
|
|
1624
|
+
return runVisual({
|
|
1625
|
+
argv,
|
|
1626
|
+
root: parsed.root,
|
|
1627
|
+
tests: path.resolve(deps.cwd, tests),
|
|
1628
|
+
...url === void 0 ? {} : { url },
|
|
1629
|
+
flags: visualFlags(argv),
|
|
1630
|
+
preload: parsed.preload,
|
|
1631
|
+
servePlugins: parsed.servePlugins
|
|
1632
|
+
}, deps);
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1322
1635
|
* Runs the bin once more with every `--preload` as a Bun preload, so the preload runs before the
|
|
1323
1636
|
* engine loads. The re-run is marked, so it does not hop again.
|
|
1324
1637
|
*
|
|
@@ -1357,7 +1670,7 @@ async function runParsed(argv, parsed, deps) {
|
|
|
1357
1670
|
const usesPlugins = parsed.command !== "keys" && parsed.command !== "pack";
|
|
1358
1671
|
checkFiles("--preload", parsed.preload);
|
|
1359
1672
|
if (usesPlugins) checkFiles("--serve-plugin", parsed.servePlugins);
|
|
1360
|
-
if (parsed.command !== "dev" && parsed.preload.length > 0 && deps.env.MOKU_GAME_CHILD !== "1") return hop(argv, parsed, deps);
|
|
1673
|
+
if (parsed.command !== "dev" && parsed.command !== "visual" && parsed.preload.length > 0 && deps.env.MOKU_GAME_CHILD !== "1") return hop(argv, parsed, deps);
|
|
1361
1674
|
switch (parsed.command) {
|
|
1362
1675
|
case "dev": return runServe(parsed, deps);
|
|
1363
1676
|
case "build": {
|
|
@@ -1372,6 +1685,7 @@ async function runParsed(argv, parsed, deps) {
|
|
|
1372
1685
|
case "native": return runNativeVerb(parsed, deps);
|
|
1373
1686
|
case "keys":
|
|
1374
1687
|
case "pack": return runScan(parsed, deps);
|
|
1688
|
+
case "visual": return runVisualCommand(argv, parsed, deps);
|
|
1375
1689
|
}
|
|
1376
1690
|
}
|
|
1377
1691
|
/**
|
|
@@ -1384,8 +1698,8 @@ function printUsage(ui) {
|
|
|
1384
1698
|
}
|
|
1385
1699
|
/**
|
|
1386
1700
|
* Runs one `moku-game` command line: reads the command and its flags, then runs `dev`, `build`,
|
|
1387
|
-
* `native <verb>`, `keys`, `pack` or `help`. Every failure is printed through the
|
|
1388
|
-
* `[game] …` message; nothing exits the process.
|
|
1701
|
+
* `native <verb>`, `keys`, `pack`, `visual` or `help`. Every failure is printed through the
|
|
1702
|
+
* console as a `[game] …` message; nothing exits the process.
|
|
1389
1703
|
*
|
|
1390
1704
|
* @param argv - The arguments after the bin.
|
|
1391
1705
|
* @param deps - The process seams.
|
|
@@ -1400,7 +1714,7 @@ async function runCommand(argv, deps) {
|
|
|
1400
1714
|
}
|
|
1401
1715
|
if (parsed === void 0) {
|
|
1402
1716
|
printUsage(deps.ui);
|
|
1403
|
-
deps.ui.error(
|
|
1717
|
+
deps.ui.error(`[game] moku-game: name a command: ${COMMAND_NAMES}.`);
|
|
1404
1718
|
return 1;
|
|
1405
1719
|
}
|
|
1406
1720
|
return await runParsed(argv, parsed, deps);
|
|
@@ -1558,15 +1872,15 @@ async function runNative(verb, settings, where, options) {
|
|
|
1558
1872
|
//#region src/cli.ts
|
|
1559
1873
|
/**
|
|
1560
1874
|
* @file The `@moku-labs/game/cli` door, node and bun only: the `moku-game` bin and the editor's
|
|
1561
|
-
* seam. `runCli` runs one command line of `moku-game` (dev, build, native, keys, pack,
|
|
1562
|
-
* this process; `preparePage` writes a game's dev page into `<game>/.moku/` for a server the
|
|
1875
|
+
* seam. `runCli` runs one command line of `moku-game` (dev, build, native, keys, pack, visual,
|
|
1876
|
+
* help) in this process; `preparePage` writes a game's dev page into `<game>/.moku/` for a server the
|
|
1563
1877
|
* caller starts itself, as the editor does. It imports no Pixi, and the root entry never imports
|
|
1564
1878
|
* it. The code lives in `app/`; this file wires the real process into it.
|
|
1565
1879
|
*/
|
|
1566
1880
|
/**
|
|
1567
1881
|
* The real process behind the command line: the branded console, the environment the bin
|
|
1568
1882
|
* started with, Bun's spawn and resolver, the signals, `fs.watch`, the asset scanner with the
|
|
1569
|
-
* string tools, and
|
|
1883
|
+
* string tools, native, and the visual test runner, loaded only by `moku-game visual`.
|
|
1570
1884
|
*
|
|
1571
1885
|
* @returns The seams of this process.
|
|
1572
1886
|
*/
|
|
@@ -1599,13 +1913,14 @@ function processDeps() {
|
|
|
1599
1913
|
}, ui),
|
|
1600
1914
|
native: runNative,
|
|
1601
1915
|
resolve: (specifier, from) => Bun.resolveSync(specifier, from),
|
|
1602
|
-
loadPage: (file) => import(file)
|
|
1916
|
+
loadPage: (file) => import(file),
|
|
1917
|
+
visual: () => import("./visual.mjs")
|
|
1603
1918
|
};
|
|
1604
1919
|
}
|
|
1605
1920
|
/**
|
|
1606
1921
|
* Runs one `moku-game` command line in this process: `dev`, `build`, `native <verb>`, `keys`,
|
|
1607
|
-
* `pack` or `help`, with `--root`, `--preload` and `--serve-plugin` before or after the
|
|
1608
|
-
* word. Every line goes through the branded console; nothing exits the process.
|
|
1922
|
+
* `pack`, `visual` or `help`, with `--root`, `--preload` and `--serve-plugin` before or after the
|
|
1923
|
+
* command word. Every line goes through the branded console; nothing exits the process.
|
|
1609
1924
|
*
|
|
1610
1925
|
* @param argv - The arguments after the bin.
|
|
1611
1926
|
* @returns The exit code.
|
|
@@ -1613,6 +1928,7 @@ function processDeps() {
|
|
|
1613
1928
|
* ```ts
|
|
1614
1929
|
* // bin/moku-game.mjs in a game folder:
|
|
1615
1930
|
* await runCli(["dev", "--port", "0"]); // 0 after Ctrl+C; stdout had "http://localhost:<port>/"
|
|
1931
|
+
* await runCli(["visual", "--no-pixels"]); // 0 when every checkpoint of tests/visual/index.ts is the same
|
|
1616
1932
|
* await runCli(["help"]); // 0
|
|
1617
1933
|
* ```
|
|
1618
1934
|
*/
|