@moku-labs/game 0.14.12 → 0.14.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -12
- package/dist/app/page.d.mts +8 -3
- package/dist/app/page.mjs +57 -12
- package/dist/app/system.d.mts +5 -3
- package/dist/app/system.mjs +2 -0
- package/dist/app.d.mts +15 -7
- package/dist/app.mjs +131 -27
- package/dist/{assets-BrKSg1O6.mjs → assets-CEMfBogh.mjs} +471 -120
- package/dist/assets.d.mts +30 -13
- package/dist/assets.mjs +1 -1
- package/dist/cli.d.mts +105 -8
- package/dist/cli.mjs +1036 -128
- package/dist/{commands-BaCx2B9j.d.mts → commands-DAETmK86.d.mts} +43 -7
- package/dist/{config-CdARrrhU.mjs → config-B87aFTW-.mjs} +25 -4
- package/dist/control.d.mts +130 -17
- package/dist/control.mjs +4 -4
- package/dist/{define-Ce8SVEN-.d.mts → define-BASd9ZUh.d.mts} +32 -4
- package/dist/dev-DbHgx-R-.mjs +89 -0
- package/dist/{fake-MpzwZhIZ.mjs → fake-6mazmHK3.mjs} +20 -2
- package/dist/features-DowEilZm.mjs +266 -0
- package/dist/{headless-CMHNsKjW.d.mts → headless-cFveciuY.d.mts} +1 -1
- package/dist/hot.d.mts +4 -2
- package/dist/hot.mjs +7 -4
- package/dist/{index-CLQ8NOTw.d.mts → index-DedbED3T.d.mts} +317 -69
- package/dist/index.d.mts +5 -6
- package/dist/index.mjs +3 -3
- package/dist/inspect.d.mts +12 -7
- package/dist/inspect.mjs +2 -2
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/lint.d.mts +8 -3
- package/dist/lint.mjs +432 -20
- package/dist/project.d.mts +15 -1
- package/dist/project.mjs +1 -1
- package/dist/{run-Bf2hKyjZ.mjs → run-BbipFRc6.mjs} +250 -51
- package/dist/{runtime-BCnKn0xf.d.mts → runtime-DoSlRx8X.d.mts} +1 -1
- package/dist/{session-DgukhR_7.mjs → session-CeKgO2hy.mjs} +30 -2
- package/dist/{sources-Duzj6lhV.mjs → sources-Cdvb0rOS.mjs} +33 -6
- package/dist/{src-CtC8w3_z.mjs → src-BEbJ_sSx.mjs} +35536 -17789
- package/dist/testing.d.mts +23 -13
- package/dist/testing.mjs +10 -6
- package/dist/{types-DOlpeuGx.d.mts → types-7GXcXxxv.d.mts} +174 -242
- package/dist/{types-BjTtGSFb.d.mts → types-BGcda5Le.d.mts} +4459 -732
- package/dist/{types-gisvSHoC.d.mts → types-C73hbmxi.d.mts} +129 -17
- package/dist/{types-CAPv9l29.d.mts → types-DngFQJro.d.mts} +43 -5
- package/dist/{types-Y_rCuXtj.d.mts → types-GHE4J5Uf.d.mts} +1025 -191
- package/dist/{visual-dMW8iqAE.mjs → visual-2m4wJL_z.mjs} +111 -7
- package/dist/visual-_XhoGeIC.d.mts +345 -0
- package/dist/visual.d.mts +1 -332
- package/dist/visual.mjs +130 -20
- package/dist/{watch-CWI_on4T.mjs → watch-BEdrp80i.mjs} +149 -30
- package/llms.txt +99 -48
- package/package.json +8 -6
- package/dist/dev-Cc50Dk7H.mjs +0 -47
- package/dist/memory-pUQ7YZWC.d.mts +0 -50
- package/dist/types-BX4Ge_j5.d.mts +0 -656
package/dist/cli.mjs
CHANGED
|
@@ -1,14 +1,239 @@
|
|
|
1
|
-
import { t as resolveConfig } from "./config-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { t as resolveConfig } from "./config-B87aFTW-.mjs";
|
|
2
|
+
import { t as findFeatureOwners } from "./features-DowEilZm.mjs";
|
|
3
|
+
import { r as watchTree, t as createBatcher, x as isSkippedFolder$1 } from "./watch-BEdrp80i.mjs";
|
|
4
|
+
import { a as runCli$1, i as compileStrings, n as importStrings, t as exportStrings } from "./assets-CEMfBogh.mjs";
|
|
4
5
|
import { constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { createBrandConsole } from "@moku-labs/common/cli";
|
|
7
8
|
import { parseArgs } from "node:util";
|
|
8
9
|
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
//#region src/app/engine.ts
|
|
12
|
+
/**
|
|
13
|
+
* @file app — the engine a page is served or built with: its version, a hash of its bundled dist and of any declared
|
|
14
|
+
* patch. Imports `node:fs`, `node:path` and `node:crypto` only, nothing from a plugin. The dev page, the editor's
|
|
15
|
+
* `preparePage` and a debug build write its id into the page as `globalThis.__MOKU_GAME_ENGINE__`.
|
|
16
|
+
*/
|
|
17
|
+
/** The package name of the engine. */
|
|
18
|
+
const ENGINE_PACKAGE = "@moku-labs/game";
|
|
19
|
+
/** The lock files that mark the install root, where the patch walk stops. */
|
|
20
|
+
const LOCK_FILES$1 = ["bun.lock", "bun.lockb"];
|
|
21
|
+
/**
|
|
22
|
+
* Tells whether a path is a file.
|
|
23
|
+
*
|
|
24
|
+
* @param file - The path.
|
|
25
|
+
* @returns True for an existing file.
|
|
26
|
+
*/
|
|
27
|
+
function isFile$1(file) {
|
|
28
|
+
return statSync(file, { throwIfNoEntry: false })?.isFile() === true;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reads the `package.json` of a folder.
|
|
32
|
+
*
|
|
33
|
+
* @param folder - The folder.
|
|
34
|
+
* @returns Its fields, or `undefined` without a readable `package.json` object.
|
|
35
|
+
*/
|
|
36
|
+
function packageOf(folder) {
|
|
37
|
+
const file = path.join(folder, "package.json");
|
|
38
|
+
if (!isFile$1(file)) return void 0;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
41
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Tells whether a path lies inside a folder, the folder itself left out.
|
|
48
|
+
*
|
|
49
|
+
* @param file - The path.
|
|
50
|
+
* @param folder - The folder.
|
|
51
|
+
* @returns True when `file` is below `folder`.
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* isBelow("/g/dist/hot.mjs", "/g/dist"); // true
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
function isBelow(file, folder) {
|
|
58
|
+
const relative = path.relative(folder, file);
|
|
59
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Walks up from a folder to the first one whose `package.json` names the engine.
|
|
63
|
+
*
|
|
64
|
+
* @param start - The folder to start in.
|
|
65
|
+
* @returns The engine folder and its fields, or `undefined` at the top of the disk.
|
|
66
|
+
*/
|
|
67
|
+
function engineFolderOf(start) {
|
|
68
|
+
for (let folder = start;; folder = path.dirname(folder)) {
|
|
69
|
+
const fields = packageOf(folder);
|
|
70
|
+
if (fields?.name === ENGINE_PACKAGE) return {
|
|
71
|
+
folder,
|
|
72
|
+
fields
|
|
73
|
+
};
|
|
74
|
+
if (path.dirname(folder) === folder) return void 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Lists the files under a folder, at any depth.
|
|
79
|
+
*
|
|
80
|
+
* @param folder - The folder.
|
|
81
|
+
* @param relative - The folder under it to list, `/` separated.
|
|
82
|
+
* @returns The files, relative to `folder`, `/` separated, in no set order.
|
|
83
|
+
*/
|
|
84
|
+
function filesUnder$1(folder, relative = "") {
|
|
85
|
+
return readdirSync(path.join(folder, relative), { withFileTypes: true }).flatMap((entry) => {
|
|
86
|
+
const inner = relative === "" ? entry.name : `${relative}/${entry.name}`;
|
|
87
|
+
if (entry.isDirectory()) return filesUnder$1(folder, inner);
|
|
88
|
+
return entry.isFile() ? [inner] : [];
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Hashes every file under a `dist` folder: each fed as `<relative path>\0<bytes>\0`, in sorted
|
|
93
|
+
* order, so a renamed file changes the hash as a changed byte does.
|
|
94
|
+
*
|
|
95
|
+
* @param dist - The folder.
|
|
96
|
+
* @returns The SHA-256, hex.
|
|
97
|
+
*/
|
|
98
|
+
function distHash(dist) {
|
|
99
|
+
const hash = createHash("sha256");
|
|
100
|
+
for (const file of filesUnder$1(dist).toSorted()) {
|
|
101
|
+
hash.update(`${file}\0`);
|
|
102
|
+
hash.update(readFileSync(path.join(dist, file)));
|
|
103
|
+
hash.update("\0");
|
|
104
|
+
}
|
|
105
|
+
return hash.digest("hex");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The folders the patch walk reads, from the game up: it stops after the first folder that holds
|
|
109
|
+
* `bun.lock` or `bun.lockb`, or at the top of the disk.
|
|
110
|
+
*
|
|
111
|
+
* @param root - The game folder.
|
|
112
|
+
* @returns The folders, the game first.
|
|
113
|
+
*/
|
|
114
|
+
function foldersToInstallRoot(root) {
|
|
115
|
+
const folders = [];
|
|
116
|
+
for (let folder = root;; folder = path.dirname(folder)) {
|
|
117
|
+
folders.push(folder);
|
|
118
|
+
if (LOCK_FILES$1.some((lock) => isFile$1(path.join(folder, lock))) || path.dirname(folder) === folder) return folders;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The patch file a `package.json` declares for one dependency key.
|
|
123
|
+
*
|
|
124
|
+
* @param fields - The fields of the `package.json`.
|
|
125
|
+
* @param key - The key, `@moku-labs/game@<version>`.
|
|
126
|
+
* @returns The declared file, relative to the folder of the `package.json`, or `undefined`.
|
|
127
|
+
*/
|
|
128
|
+
function declaredPatch(fields, key) {
|
|
129
|
+
const declared = fields?.patchedDependencies;
|
|
130
|
+
if (typeof declared !== "object" || declared === null) return void 0;
|
|
131
|
+
const file = Reflect.get(declared, key);
|
|
132
|
+
return typeof file === "string" ? file : void 0;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Finds the patch the install root declares for the installed engine version: the first
|
|
136
|
+
* `package.json` on the walk whose `patchedDependencies` has the key decides.
|
|
137
|
+
*
|
|
138
|
+
* @param root - The game folder.
|
|
139
|
+
* @param version - The installed engine version.
|
|
140
|
+
* @returns The patch file and its hash; `undefined` when none is declared or the file is missing.
|
|
141
|
+
*/
|
|
142
|
+
function patchOf(root, version) {
|
|
143
|
+
const key = `${ENGINE_PACKAGE}@${version}`;
|
|
144
|
+
for (const folder of foldersToInstallRoot(root)) {
|
|
145
|
+
const declared = declaredPatch(packageOf(folder), key);
|
|
146
|
+
if (declared === void 0) continue;
|
|
147
|
+
const file = path.resolve(folder, declared);
|
|
148
|
+
if (!isFile$1(file)) return void 0;
|
|
149
|
+
return {
|
|
150
|
+
file,
|
|
151
|
+
hash: createHash("sha256").update(readFileSync(file)).digest("hex")
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The install root of a game: the first folder from the game up that holds `bun.lock` or
|
|
157
|
+
* `bun.lockb`, where the patch walk of `engineFingerprint` stops. The dev server watches it for an
|
|
158
|
+
* engine change.
|
|
159
|
+
*
|
|
160
|
+
* @param root - The game folder.
|
|
161
|
+
* @returns The folder, or `undefined` when no folder up to the top of the disk holds a lock file.
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* // A game in a workspace, installed at the workspace root.
|
|
165
|
+
* installRootOf("/w/games/timber"); // "/w"
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
function installRootOf(root) {
|
|
169
|
+
const last = foldersToInstallRoot(root).at(-1);
|
|
170
|
+
return last !== void 0 && LOCK_FILES$1.some((lock) => isFile$1(path.join(last, lock))) ? last : void 0;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Reads the fingerprint of the engine a game resolves: walks up from the hot plugin to the engine's package.json,
|
|
174
|
+
* hashes its `dist/`, and finds the patch the install root declares for that version. A hot plugin with no engine
|
|
175
|
+
* `package.json` above it (a stub path, a broken install) gives the id `"unknown"` and throws nothing.
|
|
176
|
+
*
|
|
177
|
+
* @param root - The game folder.
|
|
178
|
+
* @param hotPlugin - The resolved path of `@moku-labs/game/hot`.
|
|
179
|
+
* @returns The fingerprint.
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* engineFingerprint("/g", "/g/node_modules/@moku-labs/game/dist/hot.mjs").id; // "0.14.12+9c41e0"
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
function engineFingerprint(root, hotPlugin) {
|
|
186
|
+
const found = engineFolderOf(path.dirname(hotPlugin));
|
|
187
|
+
if (found === void 0) return {
|
|
188
|
+
id: "unknown",
|
|
189
|
+
version: "unknown",
|
|
190
|
+
dist: "unknown",
|
|
191
|
+
folder: path.dirname(hotPlugin)
|
|
192
|
+
};
|
|
193
|
+
const { folder, fields } = found;
|
|
194
|
+
const version = typeof fields.version === "string" ? fields.version : "unknown";
|
|
195
|
+
const distFolder = path.join(folder, "dist");
|
|
196
|
+
const dist = isBelow(hotPlugin, distFolder) ? distHash(distFolder) : "src";
|
|
197
|
+
const patch = patchOf(root, version);
|
|
198
|
+
const id = `${version}+${dist === "src" ? dist : dist.slice(0, 6)}${patch === void 0 ? "" : `.patch.${patch.hash.slice(0, 6)}`}`;
|
|
199
|
+
return patch === void 0 ? {
|
|
200
|
+
id,
|
|
201
|
+
version,
|
|
202
|
+
dist,
|
|
203
|
+
folder
|
|
204
|
+
} : {
|
|
205
|
+
id,
|
|
206
|
+
version,
|
|
207
|
+
dist,
|
|
208
|
+
patch,
|
|
209
|
+
folder
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The one line `dev` and `build` print about the engine: its id, and the patch file relative to the game when there is one.
|
|
214
|
+
*
|
|
215
|
+
* @param engine - The fingerprint.
|
|
216
|
+
* @param root - The game folder.
|
|
217
|
+
* @returns The line.
|
|
218
|
+
* @example
|
|
219
|
+
* ```ts
|
|
220
|
+
* engineLine({ id: "0.14.12+9c41e0.patch.3f2a7b", version: "0.14.12", dist: "9c41e0", folder: "/g/node_modules/@moku-labs/game", patch: { file: "/g/patches/@moku-labs%2Fgame@0.14.12.patch", hash: "3f2a7b" } }, "/g"); // "engine 0.14.12+9c41e0.patch.3f2a7b (patches/@moku-labs%2Fgame@0.14.12.patch)"
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
function engineLine(engine, root) {
|
|
224
|
+
if (engine.patch === void 0) return `engine ${engine.id}`;
|
|
225
|
+
const { file } = engine.patch;
|
|
226
|
+
const shown = isBelow(file, root) ? path.relative(root, file) : file;
|
|
227
|
+
return `engine ${engine.id} (${shown})`;
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
9
230
|
//#region src/app/generate.ts
|
|
10
231
|
/** The first line of a generated file a person may open. */
|
|
11
232
|
const WRITTEN = "// Written by moku-game dev. Do not edit.";
|
|
233
|
+
/** The startup mark every generated main writes first after its imports: they all evaluated. */
|
|
234
|
+
const MAIN_MARK = "performance.mark(\"moku:main\");";
|
|
235
|
+
/** The one agent a debug page carries: the editor's page agent, resolved from the game. */
|
|
236
|
+
const DEBUG_AGENT = "@moku-labs/editor/agent/page";
|
|
12
237
|
/**
|
|
13
238
|
* The stamp module of a game, a file of `<game>/.moku/`. One per game: the dev page and the page
|
|
14
239
|
* of `moku-game visual` import the same one.
|
|
@@ -257,12 +482,13 @@ function toStampFolder(toRoot) {
|
|
|
257
482
|
return toRoot.slice(3) || "./";
|
|
258
483
|
}
|
|
259
484
|
/**
|
|
260
|
-
* The `main.ts` of the dev page: the
|
|
261
|
-
* game, its config, one import per scenario, and, with agents,
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* the
|
|
265
|
-
* the
|
|
485
|
+
* The `main.ts` of the dev page: the startup mark module first, the dev flag second, then the
|
|
486
|
+
* assets stamp, the page entry, the game, its config, one import per scenario, and, with agents,
|
|
487
|
+
* one per `.dev` module and one per agent. Its first statement marks `moku:main`, then it calls
|
|
488
|
+
* `startPage` with the scenarios by file stem, the shell with the loaders of the named plugins
|
|
489
|
+
* when the game needs it, the agents and the `.dev` modules. The stamp import puts the stamp
|
|
490
|
+
* module on the page: when the keys watch writes a new stamp, the hot plugin hands it to the
|
|
491
|
+
* running game.
|
|
266
492
|
*
|
|
267
493
|
* @param settings - The resolved `config.ts`.
|
|
268
494
|
* @param sources - The scenario files, the `.dev` modules and the agents.
|
|
@@ -287,6 +513,7 @@ function devMain(settings, sources, toRoot = "../") {
|
|
|
287
513
|
];
|
|
288
514
|
return [
|
|
289
515
|
WRITTEN,
|
|
516
|
+
"import \"./script.ts\";",
|
|
290
517
|
"import \"./dev.ts\";",
|
|
291
518
|
`import "${toStampFolder(toRoot)}${ASSETS_STAMP}";`,
|
|
292
519
|
...pageImports(settings, toRoot),
|
|
@@ -294,6 +521,7 @@ function devMain(settings, sources, toRoot = "../") {
|
|
|
294
521
|
...devModules.map((file, index) => importLine(`* as devModule${index}`, `${toRoot}${file}`)),
|
|
295
522
|
...agents.map((agent, index) => importLine(`agent${index}`, agent)),
|
|
296
523
|
"",
|
|
524
|
+
MAIN_MARK,
|
|
297
525
|
"await startPage(game, config, {",
|
|
298
526
|
options.join(",\n"),
|
|
299
527
|
"});",
|
|
@@ -301,9 +529,10 @@ function devMain(settings, sources, toRoot = "../") {
|
|
|
301
529
|
].join("\n");
|
|
302
530
|
}
|
|
303
531
|
/**
|
|
304
|
-
* The `main.ts` of the production page: the page entry, the game and its
|
|
305
|
-
* with the loaders of the named plugins when the game needs it
|
|
306
|
-
* agent, no `.dev` module, no door: none
|
|
532
|
+
* The `main.ts` of the production page: the startup mark module, the page entry, the game and its
|
|
533
|
+
* config, and the shell with the loaders of the named plugins when the game needs it; its first
|
|
534
|
+
* statement marks `moku:main`. No dev flag, no scenario, no agent, no `.dev` module, no door: none
|
|
535
|
+
* of them ships. The release page: `moku-game build --debug` uses `debugMain`.
|
|
307
536
|
*
|
|
308
537
|
* @param settings - The resolved `config.ts`.
|
|
309
538
|
* @returns The text of the in-memory `.moku/build/main.ts`.
|
|
@@ -319,24 +548,111 @@ function buildMain(settings) {
|
|
|
319
548
|
"});"
|
|
320
549
|
] : ["await startPage(game, config);"];
|
|
321
550
|
return [
|
|
551
|
+
"import \"./script.ts\";",
|
|
322
552
|
...pageImports(settings, "../../"),
|
|
323
553
|
"",
|
|
554
|
+
MAIN_MARK,
|
|
324
555
|
...call,
|
|
325
556
|
""
|
|
326
557
|
].join("\n");
|
|
327
558
|
}
|
|
328
559
|
/**
|
|
329
|
-
* The `
|
|
330
|
-
*
|
|
560
|
+
* The `main.ts` of a debug page, `moku-game build --debug`: the startup mark module first, the
|
|
561
|
+
* agent flag module second, then the page entry, the game, its config, one import per `.dev`
|
|
562
|
+
* module, sorted, and the editor's page agent. Its first statement marks `moku:main`, then it
|
|
563
|
+
* calls `startPage` with the shell when the game needs it, the agent and the `.dev` modules. No
|
|
564
|
+
* scenario (`?player=` stays a dev feature), no assets stamp, no dev flag.
|
|
565
|
+
*
|
|
566
|
+
* @param settings - The resolved `config.ts`.
|
|
567
|
+
* @param devModules - The `.dev.ts` modules of the game, relative to the game folder, `/` separated.
|
|
568
|
+
* @returns The text of the in-memory `.moku/build/main.ts`.
|
|
569
|
+
* @example
|
|
570
|
+
* ```ts
|
|
571
|
+
* debugMain(resolveConfig({ page: { title: "T" } }), []).includes("agents: [agent0]"); // true
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
function debugMain(settings, devModules) {
|
|
575
|
+
const sorted = devModules.toSorted();
|
|
576
|
+
const options = [
|
|
577
|
+
...needsShell(settings) ? [shellOption(settings)] : [],
|
|
578
|
+
" agents: [agent0]",
|
|
579
|
+
...sorted.length === 0 ? [] : [` devModules: [${numbered("devModule", sorted.length)}]`]
|
|
580
|
+
];
|
|
581
|
+
return [
|
|
582
|
+
"import \"./script.ts\";",
|
|
583
|
+
"import \"./agent.ts\";",
|
|
584
|
+
...pageImports(settings, "../../"),
|
|
585
|
+
...sorted.map((file, index) => importLine(`* as devModule${index}`, `../../${file}`)),
|
|
586
|
+
importLine("agent0", DEBUG_AGENT),
|
|
587
|
+
"",
|
|
588
|
+
MAIN_MARK,
|
|
589
|
+
"await startPage(game, config, {",
|
|
590
|
+
options.join(",\n"),
|
|
591
|
+
"});",
|
|
592
|
+
""
|
|
593
|
+
].join("\n");
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* The `script.ts` module every generated main imports first: it marks `moku:script` with the URL
|
|
597
|
+
* of the chunk it lands in, the first statement the bundle runs. Two marks cost nothing, so no
|
|
598
|
+
* flag guards them.
|
|
331
599
|
*
|
|
600
|
+
* @returns The text of `.moku/script.ts` and of the in-memory `script.ts` of a build.
|
|
601
|
+
* @example
|
|
602
|
+
* ```ts
|
|
603
|
+
* scriptMark().startsWith('performance.mark("moku:script"'); // true
|
|
604
|
+
* ```
|
|
605
|
+
*/
|
|
606
|
+
function scriptMark() {
|
|
607
|
+
return "performance.mark(\"moku:script\", { detail: import.meta.url });\n";
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* The `dev.ts` module the dev `main.ts` imports after the startup mark: it sets on `globalThis`,
|
|
611
|
+
* for code that reads the globals, the build id `"dev"`, the engine id, the control flag, the
|
|
612
|
+
* agent flag and, last, the dev flag. The bunfig `define` already makes the three bare flags true
|
|
613
|
+
* in the bundle.
|
|
614
|
+
*
|
|
615
|
+
* @param engine - The id of the engine fingerprint the page was written for.
|
|
332
616
|
* @returns The text of `.moku/dev.ts`.
|
|
333
617
|
* @example
|
|
334
618
|
* ```ts
|
|
335
|
-
* devFlag().endsWith("globalThis.__MOKU_GAME_DEV__ = true;\n"); // true
|
|
619
|
+
* devFlag("0.0.0+src").endsWith("globalThis.__MOKU_GAME_DEV__ = true;\n"); // true
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
622
|
+
function devFlag(engine) {
|
|
623
|
+
return [
|
|
624
|
+
WRITTEN,
|
|
625
|
+
"globalThis.__MOKU_GAME_BUILD__ = \"dev\";",
|
|
626
|
+
`globalThis.__MOKU_GAME_ENGINE__ = ${JSON.stringify(engine)};`,
|
|
627
|
+
"globalThis.__MOKU_GAME_CONTROL__ = true;",
|
|
628
|
+
"globalThis.__MOKU_GAME_AGENT__ = true;",
|
|
629
|
+
"globalThis.__MOKU_GAME_DEV__ = true;",
|
|
630
|
+
""
|
|
631
|
+
].join("\n");
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* The in-memory `agent.ts` module of a debug page, imported after the startup mark: six lines
|
|
635
|
+
* that set the build id, the engine id and the editor address, each through `JSON.stringify`, the
|
|
636
|
+
* control flag of the profile, the agent flag, and the dev flag `false`. No header: nobody opens
|
|
637
|
+
* it.
|
|
638
|
+
*
|
|
639
|
+
* @param page - What the page is built with.
|
|
640
|
+
* @returns The text.
|
|
641
|
+
* @example
|
|
642
|
+
* ```ts
|
|
643
|
+
* agentFlag({ build: "debug-20261014T153012Z", control: false, engine: "0.14.12+9c41e0", editor: "127.0.0.1:47600" }).split("\n")[0]; // 'globalThis.__MOKU_GAME_BUILD__ = "debug-20261014T153012Z";'
|
|
336
644
|
* ```
|
|
337
645
|
*/
|
|
338
|
-
function
|
|
339
|
-
return
|
|
646
|
+
function agentFlag(page) {
|
|
647
|
+
return [
|
|
648
|
+
`globalThis.__MOKU_GAME_BUILD__ = ${JSON.stringify(page.build)};`,
|
|
649
|
+
`globalThis.__MOKU_GAME_ENGINE__ = ${JSON.stringify(page.engine)};`,
|
|
650
|
+
`globalThis.__MOKU_GAME_EDITOR__ = ${JSON.stringify(page.editor)};`,
|
|
651
|
+
`globalThis.__MOKU_GAME_CONTROL__ = ${String(page.control)};`,
|
|
652
|
+
"globalThis.__MOKU_GAME_AGENT__ = true;",
|
|
653
|
+
"globalThis.__MOKU_GAME_DEV__ = false;",
|
|
654
|
+
""
|
|
655
|
+
].join("\n");
|
|
340
656
|
}
|
|
341
657
|
/**
|
|
342
658
|
* The `assets-stamp.ts` module the dev `main.ts` imports: the asset files of the game with their
|
|
@@ -384,7 +700,7 @@ function tomlList(paths) {
|
|
|
384
700
|
}
|
|
385
701
|
/**
|
|
386
702
|
* The bunfig of the dev server: the preloads when given, the hot plugin first among the bundler
|
|
387
|
-
* plugins of the page, and the dev
|
|
703
|
+
* plugins of the page, and the dev, agent and control flags defined true.
|
|
388
704
|
*
|
|
389
705
|
* @param paths - The absolute paths of the hot plugin, the extra plugins and the preloads.
|
|
390
706
|
* @returns The text of `.moku/bunfig.toml`.
|
|
@@ -400,11 +716,171 @@ function bunfigText(paths) {
|
|
|
400
716
|
"",
|
|
401
717
|
"[serve.static]",
|
|
402
718
|
`plugins = ${tomlList([paths.hotPlugin, ...paths.servePlugins])}`,
|
|
403
|
-
"define = { \"__MOKU_GAME_DEV__\" = \"true\" }",
|
|
719
|
+
"define = { \"__MOKU_GAME_DEV__\" = \"true\", \"__MOKU_GAME_AGENT__\" = \"true\", \"__MOKU_GAME_CONTROL__\" = \"true\" }",
|
|
404
720
|
""
|
|
405
721
|
].join("\n");
|
|
406
722
|
}
|
|
407
723
|
//#endregion
|
|
724
|
+
//#region src/app/engine-watch.ts
|
|
725
|
+
/**
|
|
726
|
+
* @file app — watches the installed engine and tells when the served engine or its patch changed:
|
|
727
|
+
* the dev server restarts its child on a new engine, and the editor's dev server does the same
|
|
728
|
+
* through `watchEngine` of `@moku-labs/game/cli`. Node and Bun only; the fingerprint is the one of
|
|
729
|
+
* `./engine`.
|
|
730
|
+
*/
|
|
731
|
+
/** The quiet that closes one batch of engine events: an install writes many files. */
|
|
732
|
+
const ENGINE_QUIET_MS = 300;
|
|
733
|
+
/** The lock files of an install root, the first one found is watched. */
|
|
734
|
+
const LOCK_FILES = ["bun.lock", "bun.lockb"];
|
|
735
|
+
/** The length of a hash as an engine id shows it. */
|
|
736
|
+
const SHORT_HASH = 6;
|
|
737
|
+
/**
|
|
738
|
+
* Tells whether a path exists.
|
|
739
|
+
*
|
|
740
|
+
* @param file - The path.
|
|
741
|
+
* @returns True for an existing file or folder.
|
|
742
|
+
*/
|
|
743
|
+
function exists(file) {
|
|
744
|
+
return statSync(file, { throwIfNoEntry: false }) !== void 0;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Lists a folder and every folder under it.
|
|
748
|
+
*
|
|
749
|
+
* @param folder - The folder.
|
|
750
|
+
* @returns The folders, `folder` first; none when it is missing.
|
|
751
|
+
*/
|
|
752
|
+
function foldersUnder(folder) {
|
|
753
|
+
if (statSync(folder, { throwIfNoEntry: false })?.isDirectory() !== true) return [];
|
|
754
|
+
return [folder, ...readdirSync(folder, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => foldersUnder(path.join(folder, entry.name)))];
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* The paths the watch opens for an engine: the scope folder (an install replaces the engine
|
|
758
|
+
* folder), the engine folder, every folder of its `dist`, the folder of the patch, and the
|
|
759
|
+
* `package.json` and lock file of the install root. An engine from its sources or an unidentified
|
|
760
|
+
* one opens none.
|
|
761
|
+
*
|
|
762
|
+
* @param start - The fingerprint to watch.
|
|
763
|
+
* @param root - The game folder.
|
|
764
|
+
* @returns The existing paths, each once.
|
|
765
|
+
*/
|
|
766
|
+
function watchedPaths(start, root) {
|
|
767
|
+
if (start.dist === "src" || start.dist === "unknown") return [];
|
|
768
|
+
const installRoot = installRootOf(root);
|
|
769
|
+
const rootFiles = installRoot === void 0 ? [] : [path.join(installRoot, "package.json"), ...LOCK_FILES.map((file) => path.join(installRoot, file)).filter((file) => exists(file)).slice(0, 1)];
|
|
770
|
+
const paths = [
|
|
771
|
+
path.dirname(start.folder),
|
|
772
|
+
start.folder,
|
|
773
|
+
...foldersUnder(path.join(start.folder, "dist")),
|
|
774
|
+
...start.patch === void 0 ? [] : [path.dirname(start.patch.file)],
|
|
775
|
+
...rootFiles
|
|
776
|
+
];
|
|
777
|
+
return [...new Set(paths)].filter((file) => exists(file));
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* The short hash of a patch, as an engine id shows it.
|
|
781
|
+
*
|
|
782
|
+
* @param patch - The patch, or `undefined` without one.
|
|
783
|
+
* @returns The first six hex digits, or `none`.
|
|
784
|
+
* @example
|
|
785
|
+
* ```ts
|
|
786
|
+
* patchName(undefined); // "none"
|
|
787
|
+
* ```
|
|
788
|
+
*/
|
|
789
|
+
function patchName(patch) {
|
|
790
|
+
return patch === void 0 ? "none" : patch.hash.slice(0, SHORT_HASH);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* The warning lines `moku-game dev` prints for a change.
|
|
794
|
+
*
|
|
795
|
+
* @param change - The change.
|
|
796
|
+
* @returns The lines, the first one a `[game]` message.
|
|
797
|
+
* @example
|
|
798
|
+
* ```ts
|
|
799
|
+
* engineChangeLines({ before: { id: "0.14.12+1a2b3c", version: "0.14.12", dist: "1a2b3c", folder: "/e" }, after: { id: "0.14.12+9c41e0", version: "0.14.12", dist: "9c41e0", folder: "/e" }, restart: true }); // ["[game] dev: the engine changed (0.14.12+1a2b3c -> 0.14.12+9c41e0), restarting the dev server."]
|
|
800
|
+
* ```
|
|
801
|
+
*/
|
|
802
|
+
function engineChangeLines(change) {
|
|
803
|
+
const { before, after } = change;
|
|
804
|
+
if (!change.restart) return [`[game] dev: the engine's patch changed (${patchName(before.patch)} -> ${patchName(after.patch)}), but the installed engine did not.`, " Run bun install; the dev server restarts by itself when the engine changes."];
|
|
805
|
+
const changed = `[game] dev: the engine changed (${before.id} -> ${after.id}), restarting the dev server.`;
|
|
806
|
+
return before.version === after.version ? [changed] : [changed, ` This process keeps the tools of ${before.version} (the keys watch); restart moku-game dev to use ${after.version}'s.`];
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Closes every open watcher of the watch.
|
|
810
|
+
*
|
|
811
|
+
* @param watch - The running watch.
|
|
812
|
+
*/
|
|
813
|
+
function closeWatchers(watch) {
|
|
814
|
+
for (const watcher of watch.watchers.splice(0)) watcher.close();
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* One batch: fingerprints the engine again, calls `onChange` when the served engine or its patch
|
|
818
|
+
* changed, and opens the watchers again: a replaced folder killed its watcher, and a new `dist`
|
|
819
|
+
* folder needs one. An engine that cannot be read goes to `onError`.
|
|
820
|
+
*
|
|
821
|
+
* @param root - The game folder.
|
|
822
|
+
* @param watch - The running watch.
|
|
823
|
+
* @param seams - The resolver.
|
|
824
|
+
* @param onChange - Called with the change.
|
|
825
|
+
* @param onError - Called when the engine cannot be read.
|
|
826
|
+
*/
|
|
827
|
+
function checkEngine(root, watch, seams, onChange, onError) {
|
|
828
|
+
const before = watch.current;
|
|
829
|
+
let after;
|
|
830
|
+
try {
|
|
831
|
+
after = engineFingerprint(root, seams.resolve("@moku-labs/game/hot", root));
|
|
832
|
+
} catch (error) {
|
|
833
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
834
|
+
onError?.(`[game] dev: the engine cannot be read (${reason}). Waiting for the next change.`);
|
|
835
|
+
watch.open?.();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const restart = before.version !== after.version || before.dist !== after.dist;
|
|
839
|
+
const repatched = before.patch?.hash !== after.patch?.hash;
|
|
840
|
+
watch.current = after;
|
|
841
|
+
watch.open?.();
|
|
842
|
+
if (restart || repatched) onChange({
|
|
843
|
+
before,
|
|
844
|
+
after,
|
|
845
|
+
restart
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Watches the engine's folders, its patch and the install root's lock. Every event waits for 300 ms
|
|
850
|
+
* of quiet, then the engine is fingerprinted again: a new version or dist calls `onChange` with
|
|
851
|
+
* `restart: true`, a new patch alone with `restart: false`, no change calls nothing. An engine that
|
|
852
|
+
* cannot be read goes to `onError`, and the next event checks again. After each batch the watchers
|
|
853
|
+
* open again from the current fingerprint.
|
|
854
|
+
*
|
|
855
|
+
* @param root - The game folder.
|
|
856
|
+
* @param start - The fingerprint the server started with.
|
|
857
|
+
* @param seams - The watcher and the resolver.
|
|
858
|
+
* @param onChange - Called with each change.
|
|
859
|
+
* @param onError - Called when the engine cannot be read; nothing by default.
|
|
860
|
+
* @returns The running watch; `close()` ends it.
|
|
861
|
+
*/
|
|
862
|
+
function watchEngineAt(root, start, seams, onChange, onError) {
|
|
863
|
+
const watch = {
|
|
864
|
+
current: start,
|
|
865
|
+
watchers: [],
|
|
866
|
+
closed: false
|
|
867
|
+
};
|
|
868
|
+
const batcher = createBatcher(ENGINE_QUIET_MS, async () => {
|
|
869
|
+
if (!watch.closed) checkEngine(root, watch, seams, onChange, onError);
|
|
870
|
+
});
|
|
871
|
+
watch.open = () => {
|
|
872
|
+
closeWatchers(watch);
|
|
873
|
+
for (const file of watchedPaths(watch.current, root)) watch.watchers.push(seams.watch(file, () => batcher.poke()));
|
|
874
|
+
};
|
|
875
|
+
watch.open();
|
|
876
|
+
return { close: () => {
|
|
877
|
+
if (watch.closed) return;
|
|
878
|
+
watch.closed = true;
|
|
879
|
+
batcher.cancel();
|
|
880
|
+
closeWatchers(watch);
|
|
881
|
+
} };
|
|
882
|
+
}
|
|
883
|
+
//#endregion
|
|
408
884
|
//#region src/app/files.ts
|
|
409
885
|
/**
|
|
410
886
|
* @file The file write of the files `moku-game` generates for a game. The dev page and the stamp
|
|
@@ -535,7 +1011,8 @@ function checkFiles(flag, files) {
|
|
|
535
1011
|
}
|
|
536
1012
|
/**
|
|
537
1013
|
* Finds the hot plugin of the engine the game resolves: the installed `dist/hot.mjs`, or
|
|
538
|
-
* `src/hot.ts` through the tsconfig `paths` of the engine repo.
|
|
1014
|
+
* `src/hot.ts` through the tsconfig `paths` of the engine repo. A debug build resolves it the
|
|
1015
|
+
* same way, for the engine fingerprint.
|
|
539
1016
|
*
|
|
540
1017
|
* @param root - The game folder.
|
|
541
1018
|
* @param resolve - Bun's resolver.
|
|
@@ -576,7 +1053,7 @@ function isSkippedFolder(name) {
|
|
|
576
1053
|
}
|
|
577
1054
|
/**
|
|
578
1055
|
* Lists the `.dev.ts` modules under a folder of the game, outside the dot folders and the folders
|
|
579
|
-
* of tools and tests.
|
|
1056
|
+
* of tools and tests: what the editor's dev page and a debug page hand to their agents.
|
|
580
1057
|
*
|
|
581
1058
|
* @param root - The game folder.
|
|
582
1059
|
* @param relative - The folder under the game, `/` separated, `""` for the game itself.
|
|
@@ -611,19 +1088,21 @@ function mainOf(root, settings, agents, toRoot = "../") {
|
|
|
611
1088
|
}, toRoot);
|
|
612
1089
|
}
|
|
613
1090
|
/**
|
|
614
|
-
* Writes the dev page of a checked game into `<game>/.moku/`: `index.html`, `
|
|
615
|
-
*
|
|
616
|
-
* `
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
* a
|
|
1091
|
+
* Writes the dev page of a checked game into `<game>/.moku/`: `index.html`, `script.ts` (the
|
|
1092
|
+
* startup mark), `dev.ts` (the flags and the engine id), `main.ts` and `bunfig.toml`. `moku-game
|
|
1093
|
+
* visual` writes its own page into `.moku/visual/`, so the page of `dev` or of the editor stays as
|
|
1094
|
+
* it is; the links of the page reach the game from its depth. The hot plugin is resolved and the
|
|
1095
|
+
* engine fingerprinted before any text is made, and every text is made before the first write, so
|
|
1096
|
+
* a refused value writes nothing. A game without `.moku/assets-stamp.ts` gets the empty one, so
|
|
1097
|
+
* the import of `main.ts` resolves with or without a keys watch; a stamp that is there is the
|
|
1098
|
+
* watch's and stays.
|
|
620
1099
|
*
|
|
621
1100
|
* @param root - The game folder, absolute.
|
|
622
1101
|
* @param settings - The resolved config.
|
|
623
1102
|
* @param options - The agents, the preloads and the extra plugins, absolute.
|
|
624
1103
|
* @param resolve - Bun's resolver, to find the hot plugin.
|
|
625
1104
|
* @param page - The folder of the page under the game, `/` separated: `.moku` by default.
|
|
626
|
-
* @returns The paths of the HTML and the bunfig.
|
|
1105
|
+
* @returns The paths of the HTML and the bunfig, and the engine the page was written for.
|
|
627
1106
|
* @throws {Error} When the hot plugin does not resolve, a preload or plugin is not a file, or a
|
|
628
1107
|
* page value is refused.
|
|
629
1108
|
*/
|
|
@@ -637,15 +1116,18 @@ function writePage(root, settings, options, resolve, page = ".moku") {
|
|
|
637
1116
|
const folder = path.join(root, ...segments);
|
|
638
1117
|
const html = path.join(folder, "index.html");
|
|
639
1118
|
const bunfig = path.join(folder, "bunfig.toml");
|
|
1119
|
+
const hotPlugin = hotPluginOf(root, resolve);
|
|
1120
|
+
const engine = engineFingerprint(root, hotPlugin);
|
|
640
1121
|
const texts = [
|
|
641
1122
|
[html, pageHtml(settings.page, {
|
|
642
1123
|
toRoot,
|
|
643
1124
|
script: "./main.ts"
|
|
644
1125
|
})],
|
|
645
|
-
[path.join(folder, "
|
|
1126
|
+
[path.join(folder, "script.ts"), scriptMark()],
|
|
1127
|
+
[path.join(folder, "dev.ts"), devFlag(engine.id)],
|
|
646
1128
|
[path.join(folder, "main.ts"), mainOf(root, settings, options.agents, toRoot)],
|
|
647
1129
|
[bunfig, bunfigText({
|
|
648
|
-
hotPlugin
|
|
1130
|
+
hotPlugin,
|
|
649
1131
|
servePlugins,
|
|
650
1132
|
preload
|
|
651
1133
|
})]
|
|
@@ -655,7 +1137,8 @@ function writePage(root, settings, options, resolve, page = ".moku") {
|
|
|
655
1137
|
if (!existsSync(stamp)) writeIfChanged(stamp, assetsStamp([], []));
|
|
656
1138
|
return {
|
|
657
1139
|
html,
|
|
658
|
-
bunfig
|
|
1140
|
+
bunfig,
|
|
1141
|
+
engine
|
|
659
1142
|
};
|
|
660
1143
|
}
|
|
661
1144
|
/**
|
|
@@ -666,7 +1149,7 @@ function writePage(root, settings, options, resolve, page = ".moku") {
|
|
|
666
1149
|
* @param root - The game folder, absolute.
|
|
667
1150
|
* @param options - The agents, the preloads and the extra plugins, absolute.
|
|
668
1151
|
* @param resolve - Bun's resolver, to find the hot plugin.
|
|
669
|
-
* @returns The paths of the HTML and the bunfig.
|
|
1152
|
+
* @returns The paths of the HTML and the bunfig, and the engine the page was written for.
|
|
670
1153
|
* @throws {Error} The errors of `moku-game dev` before its server starts.
|
|
671
1154
|
*/
|
|
672
1155
|
async function preparePageAt(root, options, resolve) {
|
|
@@ -736,12 +1219,54 @@ function devChildCommand(run, page, deps) {
|
|
|
736
1219
|
];
|
|
737
1220
|
}
|
|
738
1221
|
/**
|
|
1222
|
+
* Hands one engine change to the dev parent: prints its lines, and when the served engine changed
|
|
1223
|
+
* marks the run for a restart and stops the child with SIGTERM, the way Ctrl+C stops it.
|
|
1224
|
+
*
|
|
1225
|
+
* @param change - The change.
|
|
1226
|
+
* @param live - The running child and the restart mark.
|
|
1227
|
+
* @param deps - The console.
|
|
1228
|
+
*/
|
|
1229
|
+
function restartOnEngineChange(change, live, deps) {
|
|
1230
|
+
for (const line of engineChangeLines(change)) deps.ui.warn(line);
|
|
1231
|
+
if (!change.restart) return;
|
|
1232
|
+
live.restart = true;
|
|
1233
|
+
live.child?.kill("SIGTERM");
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Serves a written page in a child until it exits without a restart: prints the engine line, runs
|
|
1237
|
+
* the child with the scenario watcher, and when the engine changed writes the page again (the new
|
|
1238
|
+
* engine id, the hot plugin of the new install) and runs a new child.
|
|
1239
|
+
*
|
|
1240
|
+
* @param run - The flags of the run.
|
|
1241
|
+
* @param page - The written page.
|
|
1242
|
+
* @param settings - The resolved config.
|
|
1243
|
+
* @param deps - The process seams.
|
|
1244
|
+
* @param live - The running child and the restart mark.
|
|
1245
|
+
* @returns The exit code of the last child.
|
|
1246
|
+
*/
|
|
1247
|
+
async function serveUntilStopped(run, page, settings, deps, live) {
|
|
1248
|
+
deps.ui.info(engineLine(page.engine, run.root));
|
|
1249
|
+
const code = await runChild(devChildCommand(run, page, deps), deps, {
|
|
1250
|
+
cwd: run.root,
|
|
1251
|
+
whileRunning: (child) => {
|
|
1252
|
+
live.child = child;
|
|
1253
|
+
return watchScenarios(run.root, settings, deps);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
live.child = void 0;
|
|
1257
|
+
if (!live.restart) return code;
|
|
1258
|
+
live.restart = false;
|
|
1259
|
+
return serveUntilStopped(run, writePage(run.root, settings, run, deps.resolve), settings, deps, live);
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
739
1262
|
* 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
|
|
741
|
-
* the scenarios while the child runs. The first scan of the keys watch writes
|
|
1263
|
+
* prints the engine line, re-runs the bin under the generated bunfig with the game as its working
|
|
1264
|
+
* directory, and watches the scenarios while the child runs. The first scan of the keys watch writes
|
|
742
1265
|
* `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.
|
|
744
|
-
*
|
|
1266
|
+
* scans again; `--packed` serves the pack and watches no keys. The installed engine is watched: a
|
|
1267
|
+
* new engine stops the child with SIGTERM, writes the page again and starts a new child; a patch
|
|
1268
|
+
* that changed while the engine did not is a warning. It hands Ctrl+C and the other stop signals to
|
|
1269
|
+
* the child and answers the exit code of a child that ends by itself.
|
|
745
1270
|
*
|
|
746
1271
|
* @param run - The flags of the run.
|
|
747
1272
|
* @param deps - The process seams.
|
|
@@ -753,14 +1278,18 @@ async function serveParent(run, deps) {
|
|
|
753
1278
|
const hasPack = isFile(path.join(run.root, "dist", "assets", "manifest.json"));
|
|
754
1279
|
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.`);
|
|
755
1280
|
const keys = run.packed ? void 0 : await deps.watchKeys(run.root, settings);
|
|
1281
|
+
const live = {
|
|
1282
|
+
child: void 0,
|
|
1283
|
+
restart: false
|
|
1284
|
+
};
|
|
1285
|
+
let engine;
|
|
756
1286
|
try {
|
|
757
1287
|
const page = writePage(run.root, settings, run, deps.resolve);
|
|
758
1288
|
if (!ignoresMoku(run.root)) deps.ui.warn("[game] dev: add \".moku/\" to .gitignore. moku-game writes its dev page there.");
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
whileRunning: () => watchScenarios(run.root, settings, deps)
|
|
762
|
-
});
|
|
1289
|
+
engine = watchEngineAt(run.root, page.engine, deps, (change) => restartOnEngineChange(change, live, deps), (message) => deps.ui.warn(message));
|
|
1290
|
+
return await serveUntilStopped(run, page, settings, deps, live);
|
|
763
1291
|
} finally {
|
|
1292
|
+
engine?.close();
|
|
764
1293
|
keys?.close();
|
|
765
1294
|
}
|
|
766
1295
|
}
|
|
@@ -792,7 +1321,7 @@ async function runChild(command, deps, child) {
|
|
|
792
1321
|
const removers = FORWARDED.map((signal) => deps.onSignal(signal, () => started.kill(signal)));
|
|
793
1322
|
let watcher;
|
|
794
1323
|
try {
|
|
795
|
-
watcher = child.whileRunning?.();
|
|
1324
|
+
watcher = child.whileRunning?.(started);
|
|
796
1325
|
return await started.exited;
|
|
797
1326
|
} finally {
|
|
798
1327
|
watcher?.close();
|
|
@@ -872,7 +1401,7 @@ function serveOn(run, page) {
|
|
|
872
1401
|
fetch: (request) => fileResponse(staticPath(base, new URL(request.url).pathname))
|
|
873
1402
|
});
|
|
874
1403
|
} catch (error) {
|
|
875
|
-
if (error.code === "EADDRINUSE") throw new Error(`[game] dev: port ${run.port} is in use.\n Pass --port 0 for a free port.`, { cause: error });
|
|
1404
|
+
if (error instanceof Error && "code" in error && error.code === "EADDRINUSE") throw new Error(`[game] dev: port ${run.port} is in use.\n Pass --port 0 for a free port.`, { cause: error });
|
|
876
1405
|
throw error;
|
|
877
1406
|
}
|
|
878
1407
|
}
|
|
@@ -932,11 +1461,21 @@ async function serveChild(run, deps) {
|
|
|
932
1461
|
//#region src/app/build.ts
|
|
933
1462
|
/**
|
|
934
1463
|
* @file `moku-game build`: the assets packed with the config's layers, then the production page
|
|
935
|
-
* bundled by `Bun.build` in the game folder, with its HTML and `
|
|
936
|
-
* `<game>/.moku/build/` that is never written, then the packed assets copied
|
|
937
|
-
*
|
|
938
|
-
*
|
|
1464
|
+
* bundled by `Bun.build` in the game folder, with its HTML, `main.ts` and `script.ts` in memory
|
|
1465
|
+
* under a virtual `<game>/.moku/build/` that is never written, then the packed assets copied
|
|
1466
|
+
* beside the page. A release page that carries the editor's agent is refused and removed. With
|
|
1467
|
+
* `--debug` the page is a debug page instead: the editor's page agent, the game's `.dev` modules,
|
|
1468
|
+
* the agent flag and a control profile, a marked title and a build id. The output works from any
|
|
1469
|
+
* http sub-path and from the Tauri protocol: every link starts with `./`, and the page reads
|
|
1470
|
+
* `manifest.json` next to itself. Node and Bun only: the bin bundles it.
|
|
1471
|
+
*/
|
|
1472
|
+
/** The address the page agent of a debug page dials without `--editor`. */
|
|
1473
|
+
const DEFAULT_EDITOR = "127.0.0.1:47600";
|
|
1474
|
+
/**
|
|
1475
|
+
* The two strings the editor's agent bundle always carries: the bridge's default path and the
|
|
1476
|
+
* prefix of every editor error. A contract between the engine, the editor and `@moku-labs/native`.
|
|
939
1477
|
*/
|
|
1478
|
+
const AGENT_MARKERS = ["/__editor/hello", "[moku-editor]"];
|
|
940
1479
|
/** The link prefix of the in-memory page, two folders under the game. */
|
|
941
1480
|
const TO_ROOT = "../../";
|
|
942
1481
|
/** Bytes in a kilobyte, for the size line of a run. */
|
|
@@ -963,27 +1502,32 @@ function scanArguments(root, settings) {
|
|
|
963
1502
|
];
|
|
964
1503
|
}
|
|
965
1504
|
/**
|
|
966
|
-
*
|
|
1505
|
+
* The folders of every feature owner of a game, at any depth (`src/project/features.ts`). A
|
|
1506
|
+
* feature name two folders share counts as no owner: the scan that follows finds the same clash
|
|
1507
|
+
* and reports it.
|
|
967
1508
|
*
|
|
968
|
-
* @param
|
|
969
|
-
* @returns Their absolute paths; none when the folder is missing.
|
|
1509
|
+
* @param root - The game folder.
|
|
1510
|
+
* @returns Their absolute paths; none when the features folder is missing.
|
|
970
1511
|
*/
|
|
971
|
-
function
|
|
972
|
-
|
|
973
|
-
|
|
1512
|
+
function featureFolders(root) {
|
|
1513
|
+
try {
|
|
1514
|
+
return findFeatureOwners(root).map((owner) => path.join(root, owner.dir));
|
|
1515
|
+
} catch {
|
|
1516
|
+
return [];
|
|
1517
|
+
}
|
|
974
1518
|
}
|
|
975
1519
|
/**
|
|
976
1520
|
* Tells whether a game has an English string file to derive the pseudo-locale from: a
|
|
977
|
-
* `strings/en.json` in a feature folder
|
|
1521
|
+
* `strings/en.json` in a feature folder at any depth (a feature owner of
|
|
1522
|
+
* `src/project/features.ts`) or in a layer folder of the config.
|
|
978
1523
|
*
|
|
979
1524
|
* @param root - The game folder.
|
|
980
1525
|
* @param settings - The resolved config, for its layer folders.
|
|
981
1526
|
* @returns True when at least one such file exists.
|
|
982
1527
|
*/
|
|
983
1528
|
function hasEnglishStrings(root, settings) {
|
|
984
|
-
const featureFolders = foldersIn(path.join(root, "features"));
|
|
985
1529
|
const layerFolders = Object.keys(settings.assets.layers).map((folder) => path.join(root, folder));
|
|
986
|
-
return [...featureFolders, ...layerFolders].some((folder) => statSync(path.join(folder, "strings", "en.json"), { throwIfNoEntry: false })?.isFile() === true);
|
|
1530
|
+
return [...featureFolders(root), ...layerFolders].some((folder) => statSync(path.join(folder, "strings", "en.json"), { throwIfNoEntry: false })?.isFile() === true);
|
|
987
1531
|
}
|
|
988
1532
|
/**
|
|
989
1533
|
* The flags of the asset scan for `moku-game keys`: the dev manifest in `generated/`, the
|
|
@@ -1085,32 +1629,106 @@ async function loadPlugins(files) {
|
|
|
1085
1629
|
}));
|
|
1086
1630
|
}
|
|
1087
1631
|
/**
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1632
|
+
* The id of a debug build: the profile and the UTC time of the run, `YYYYMMDDTHHMMSSZ`.
|
|
1633
|
+
*
|
|
1634
|
+
* @param debug - The profile.
|
|
1635
|
+
* @param now - The wall-clock time of the run, ms.
|
|
1636
|
+
* @returns The id.
|
|
1637
|
+
* @example
|
|
1638
|
+
* ```ts
|
|
1639
|
+
* buildIdOf("read", Date.UTC(2026, 9, 14, 15, 30, 12)); // "debug-20261014T153012Z"
|
|
1640
|
+
* ```
|
|
1641
|
+
*/
|
|
1642
|
+
function buildIdOf(debug, now) {
|
|
1643
|
+
const stamp = `${new Date(now).toISOString().slice(0, 19).replaceAll(/[-:]/g, "")}Z`;
|
|
1644
|
+
return debug === "full" ? `debug-full-${stamp}` : `debug-${stamp}`;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* The `define` map of a page: a release page defines the three flags `false`; a debug page the
|
|
1648
|
+
* dev flag `false`, the agent flag `true`, the control flag of its profile, and the build id, the
|
|
1649
|
+
* engine id and the editor address as strings.
|
|
1650
|
+
*
|
|
1651
|
+
* @param page - What a debug page is built with; `undefined` for a release page.
|
|
1652
|
+
* @returns The defines for `Bun.build`.
|
|
1653
|
+
* @example
|
|
1654
|
+
* ```ts
|
|
1655
|
+
* definesOf(undefined).__MOKU_GAME_AGENT__; // "false"
|
|
1656
|
+
* ```
|
|
1657
|
+
*/
|
|
1658
|
+
function definesOf(page) {
|
|
1659
|
+
if (page === void 0) return {
|
|
1660
|
+
__MOKU_GAME_DEV__: "false",
|
|
1661
|
+
__MOKU_GAME_AGENT__: "false",
|
|
1662
|
+
__MOKU_GAME_CONTROL__: "false"
|
|
1663
|
+
};
|
|
1664
|
+
return {
|
|
1665
|
+
__MOKU_GAME_DEV__: "false",
|
|
1666
|
+
__MOKU_GAME_AGENT__: "true",
|
|
1667
|
+
__MOKU_GAME_CONTROL__: page.level === "full" ? "true" : "false",
|
|
1668
|
+
__MOKU_GAME_BUILD__: JSON.stringify(page.build),
|
|
1669
|
+
__MOKU_GAME_ENGINE__: JSON.stringify(page.engine),
|
|
1670
|
+
__MOKU_GAME_EDITOR__: JSON.stringify(page.editor)
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* The in-memory files of the page under `<game>/.moku/build/`: a release page has `index.html`,
|
|
1675
|
+
* `main.ts` and `script.ts`; a debug page also `agent.ts`, its `main.ts` from `debugMain` with the
|
|
1676
|
+
* game's `.dev` modules, and its title marked `Debug`.
|
|
1677
|
+
*
|
|
1678
|
+
* @param run - The flags of the run.
|
|
1679
|
+
* @param settings - The resolved config.
|
|
1680
|
+
* @param page - What a debug page is built with; `undefined` for a release page.
|
|
1681
|
+
* @returns The texts by absolute path.
|
|
1682
|
+
*/
|
|
1683
|
+
function pageFiles(run, settings, page) {
|
|
1684
|
+
const folder = path.join(run.root, ".moku", "build");
|
|
1685
|
+
const links = {
|
|
1686
|
+
toRoot: TO_ROOT,
|
|
1687
|
+
script: "./main.ts"
|
|
1688
|
+
};
|
|
1689
|
+
const release = {
|
|
1690
|
+
[path.join(folder, "index.html")]: pageHtml(settings.page, links),
|
|
1691
|
+
[path.join(folder, "main.ts")]: buildMain(settings),
|
|
1692
|
+
[path.join(folder, "script.ts")]: scriptMark()
|
|
1693
|
+
};
|
|
1694
|
+
if (page === void 0) return release;
|
|
1695
|
+
const flags = {
|
|
1696
|
+
build: page.build,
|
|
1697
|
+
control: page.level === "full",
|
|
1698
|
+
engine: page.engine
|
|
1699
|
+
};
|
|
1700
|
+
return {
|
|
1701
|
+
...release,
|
|
1702
|
+
[path.join(folder, "index.html")]: pageHtml({
|
|
1703
|
+
...settings.page,
|
|
1704
|
+
title: `${settings.page.title} Debug`
|
|
1705
|
+
}, links),
|
|
1706
|
+
[path.join(folder, "main.ts")]: debugMain(settings, devModulesOf(run.root)),
|
|
1707
|
+
[path.join(folder, "agent.ts")]: agentFlag({
|
|
1708
|
+
...flags,
|
|
1709
|
+
editor: page.editor
|
|
1710
|
+
})
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Bundles the page into the output folder: its files in memory, every link `./`, the flags
|
|
1715
|
+
* defined, no hot plugin.
|
|
1090
1716
|
*
|
|
1091
1717
|
* @param run - The flags of the run.
|
|
1092
1718
|
* @param settings - The resolved config.
|
|
1719
|
+
* @param page - What a debug page is built with; `undefined` for a release page.
|
|
1093
1720
|
* @returns Resolves when the page is written.
|
|
1094
1721
|
* @throws {Error} With one Bun log per line when the page did not bundle.
|
|
1095
1722
|
*/
|
|
1096
|
-
async function bundlePage(run, settings) {
|
|
1097
|
-
const folder = path.join(run.root, ".moku", "build");
|
|
1098
|
-
const html = path.join(folder, "index.html");
|
|
1099
|
-
const main = path.join(folder, "main.ts");
|
|
1723
|
+
async function bundlePage(run, settings, page) {
|
|
1100
1724
|
const result = await Bun.build({
|
|
1101
|
-
entrypoints: [html],
|
|
1102
|
-
files:
|
|
1103
|
-
[html]: pageHtml(settings.page, {
|
|
1104
|
-
toRoot: TO_ROOT,
|
|
1105
|
-
script: "./main.ts"
|
|
1106
|
-
}),
|
|
1107
|
-
[main]: buildMain(settings)
|
|
1108
|
-
},
|
|
1725
|
+
entrypoints: [path.join(run.root, ".moku", "build", "index.html")],
|
|
1726
|
+
files: pageFiles(run, settings, page),
|
|
1109
1727
|
outdir: run.out,
|
|
1110
1728
|
publicPath: "./",
|
|
1111
1729
|
minify: true,
|
|
1112
1730
|
target: "browser",
|
|
1113
|
-
define:
|
|
1731
|
+
define: definesOf(page),
|
|
1114
1732
|
naming: {
|
|
1115
1733
|
entry: "[name].[ext]",
|
|
1116
1734
|
chunk: "page/[name]-[hash].[ext]",
|
|
@@ -1157,6 +1775,26 @@ function filesUnder(folder, relative = "") {
|
|
|
1157
1775
|
});
|
|
1158
1776
|
}
|
|
1159
1777
|
/**
|
|
1778
|
+
* Refuses a release page that carries the editor's agent: reads every `.js` file of the output
|
|
1779
|
+
* for the two marker strings of the agent bundle. On a hit the output folder is removed, so no
|
|
1780
|
+
* release page with an agent is left on disk.
|
|
1781
|
+
*
|
|
1782
|
+
* @param out - The output folder.
|
|
1783
|
+
* @throws {Error} Naming the marker and the file, when a page file carries one.
|
|
1784
|
+
*/
|
|
1785
|
+
function refuseAgent(out) {
|
|
1786
|
+
for (const file of filesUnder(out).filter((name) => name.endsWith(".js")).toSorted()) {
|
|
1787
|
+
const code = readFileSync(path.join(out, file), "utf8");
|
|
1788
|
+
const marker = AGENT_MARKERS.find((text) => code.includes(text));
|
|
1789
|
+
if (marker === void 0) continue;
|
|
1790
|
+
rmSync(out, {
|
|
1791
|
+
recursive: true,
|
|
1792
|
+
force: true
|
|
1793
|
+
});
|
|
1794
|
+
throw new Error(`[game] build: the release page carries the editor agent ("${marker}" in ${file}).\n Import @moku-labs/editor only behind __MOKU_GAME_DEV__, or build a debug page with --debug.`);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1160
1798
|
* Copies the packed assets beside the page, `manifest.json` next to `index.html`. File by file:
|
|
1161
1799
|
* the output folder and its page folders exist already, and a packed file never replaces a page
|
|
1162
1800
|
* file.
|
|
@@ -1193,28 +1831,91 @@ async function inFolder(folder, step) {
|
|
|
1193
1831
|
}
|
|
1194
1832
|
}
|
|
1195
1833
|
/**
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1834
|
+
* Refuses `--debug` when Tauri runs the web build of a store app: `MOKU_NATIVE_PROFILE=store`.
|
|
1835
|
+
*
|
|
1836
|
+
* @param run - The flags of the run.
|
|
1837
|
+
* @param env - The environment the bin started with.
|
|
1838
|
+
* @throws {Error} When the profile is `store` and the run is a debug run.
|
|
1839
|
+
*/
|
|
1840
|
+
function refuseStoreDebug(run, env) {
|
|
1841
|
+
if (run.debug === void 0 || env.MOKU_NATIVE_PROFILE !== "store") return;
|
|
1842
|
+
throw new Error("[game] build: MOKU_NATIVE_PROFILE is store: a store page must not carry the editor agent.\n Build the store app without --debug.");
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* What a debug run builds its page with: the editor's page agent must resolve from the game, and
|
|
1846
|
+
* the engine the page bundles is fingerprinted.
|
|
1847
|
+
*
|
|
1848
|
+
* @param run - The flags of a debug run.
|
|
1849
|
+
* @param level - Its profile.
|
|
1850
|
+
* @param deps - The resolver and the clock.
|
|
1851
|
+
* @returns The debug page and the engine fingerprint.
|
|
1852
|
+
* @throws {Error} With the install line when the game cannot resolve `@moku-labs/editor`, and
|
|
1853
|
+
* when it cannot resolve `@moku-labs/game/hot`.
|
|
1854
|
+
*/
|
|
1855
|
+
function debugPageOf(run, level, deps) {
|
|
1856
|
+
try {
|
|
1857
|
+
deps.resolve(DEBUG_AGENT, run.root);
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
throw new Error("[game] build --debug needs @moku-labs/editor in the game.\n Install it: bun add -d @moku-labs/editor.", { cause: error });
|
|
1860
|
+
}
|
|
1861
|
+
const engine = engineFingerprint(run.root, hotPluginOf(run.root, deps.resolve));
|
|
1862
|
+
return {
|
|
1863
|
+
page: {
|
|
1864
|
+
level,
|
|
1865
|
+
build: buildIdOf(level, deps.now()),
|
|
1866
|
+
engine: engine.id,
|
|
1867
|
+
editor: run.editor ?? "127.0.0.1:47600"
|
|
1868
|
+
},
|
|
1869
|
+
engine
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* The warning a debug build prints before its size line: the profile, the build id, the engine and
|
|
1874
|
+
* the editor address.
|
|
1875
|
+
*
|
|
1876
|
+
* @param run - The flags of the run.
|
|
1877
|
+
* @param debug - The debug page and the engine fingerprint.
|
|
1878
|
+
* @param debug.page - What the page was built with.
|
|
1879
|
+
* @param debug.engine - The engine it bundles.
|
|
1880
|
+
* @returns The line.
|
|
1881
|
+
*/
|
|
1882
|
+
function debugWarning(run, debug) {
|
|
1883
|
+
const { page, engine } = debug;
|
|
1884
|
+
const control = page.level === "full" ? "every command, the state-changing ones too" : "read and cosmetic commands";
|
|
1885
|
+
const engineText = engineLine(engine, run.root).slice(7);
|
|
1886
|
+
return `build: a DEBUG page with the editor agent, control: ${control}. Build ${page.build}, engine ${engineText}, editor ${page.editor}. Never ship it to a store.`;
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Builds the page of a game: packs the assets, replaces the output folder with the bundled page,
|
|
1890
|
+
* refuses a release page that carries the editor's agent, and copies the pack beside it. A debug
|
|
1891
|
+
* run first checks that the game resolves the editor's agent and fingerprints the engine, and
|
|
1892
|
+
* prints one warning before the size line.
|
|
1198
1893
|
*
|
|
1199
1894
|
* @param run - The flags of the run.
|
|
1200
|
-
* @param deps - The process seams: the asset scanner
|
|
1895
|
+
* @param deps - The process seams: the asset scanner, the resolver, the clock, the environment and
|
|
1896
|
+
* the console.
|
|
1201
1897
|
* @returns The exit code: the pack's when it failed, else `0`.
|
|
1202
|
-
* @throws {Error} When the game, the output folder or the bundle is refused
|
|
1898
|
+
* @throws {Error} When the game, the output folder, the profile or the bundle is refused, a debug
|
|
1899
|
+
* run cannot resolve the editor, or a release page carries the editor's agent.
|
|
1203
1900
|
*/
|
|
1204
1901
|
async function runBuild(run, deps) {
|
|
1902
|
+
refuseStoreDebug(run, deps.env);
|
|
1205
1903
|
const settings = await loadGame(run.root);
|
|
1206
1904
|
refuseOut(run.root, run.out);
|
|
1905
|
+
const debug = run.debug === void 0 ? void 0 : debugPageOf(run, run.debug, deps);
|
|
1207
1906
|
const packed = await deps.assets(packArguments(run.root, settings, false));
|
|
1208
1907
|
if (packed !== 0) return packed;
|
|
1209
1908
|
rmSync(run.out, {
|
|
1210
1909
|
recursive: true,
|
|
1211
1910
|
force: true
|
|
1212
1911
|
});
|
|
1213
|
-
await inFolder(run.root, () => bundlePage(run, settings));
|
|
1912
|
+
await inFolder(run.root, () => bundlePage(run, settings, debug?.page));
|
|
1214
1913
|
relinkIcons(run, settings);
|
|
1914
|
+
if (debug === void 0) refuseAgent(run.out);
|
|
1215
1915
|
copyPack(run);
|
|
1216
1916
|
const files = filesUnder(run.out);
|
|
1217
1917
|
const bytes = files.map((file) => statSync(path.join(run.out, file)).size).reduce((sum, size) => sum + size, 0);
|
|
1918
|
+
if (debug !== void 0) deps.ui.warn(debugWarning(run, debug));
|
|
1218
1919
|
deps.ui.info(`built "${run.out}": ${files.length} files, ${Math.round(bytes / BYTES_PER_KB)} KB.`);
|
|
1219
1920
|
return 0;
|
|
1220
1921
|
}
|
|
@@ -1438,17 +2139,28 @@ const ALL_OPTIONS = {
|
|
|
1438
2139
|
multiple: true
|
|
1439
2140
|
},
|
|
1440
2141
|
"no-pixels": { type: "boolean" },
|
|
2142
|
+
"no-verify-frames": { type: "boolean" },
|
|
1441
2143
|
pixels: { type: "boolean" },
|
|
1442
2144
|
dir: { type: "string" },
|
|
1443
|
-
webgl: { type: "boolean" }
|
|
2145
|
+
webgl: { type: "boolean" },
|
|
2146
|
+
debug: { type: "string" },
|
|
2147
|
+
editor: { type: "string" }
|
|
1444
2148
|
};
|
|
1445
2149
|
/** The flags of `visual` that `parseVisualArgv` reads, in the runner's words. */
|
|
1446
|
-
const VISUAL_FLAGS = /^(?:update|only|no-pixels|pixels|dir|webgl)$/;
|
|
2150
|
+
const VISUAL_FLAGS = /^(?:update|only|no-pixels|pixels|dir|webgl|no-verify-frames)$/;
|
|
1447
2151
|
/** The flags of one command besides the global ones. */
|
|
1448
2152
|
const COMMAND_OPTIONS = {
|
|
1449
2153
|
dev: ["port", "packed"],
|
|
1450
|
-
build: [
|
|
1451
|
-
|
|
2154
|
+
build: [
|
|
2155
|
+
"out",
|
|
2156
|
+
"debug",
|
|
2157
|
+
"editor"
|
|
2158
|
+
],
|
|
2159
|
+
native: [
|
|
2160
|
+
"simulator",
|
|
2161
|
+
"debug",
|
|
2162
|
+
"editor"
|
|
2163
|
+
],
|
|
1452
2164
|
keys: ["check"],
|
|
1453
2165
|
pack: ["no-cache"],
|
|
1454
2166
|
visual: [
|
|
@@ -1459,7 +2171,8 @@ const COMMAND_OPTIONS = {
|
|
|
1459
2171
|
"no-pixels",
|
|
1460
2172
|
"pixels",
|
|
1461
2173
|
"dir",
|
|
1462
|
-
"webgl"
|
|
2174
|
+
"webgl",
|
|
2175
|
+
"no-verify-frames"
|
|
1463
2176
|
]
|
|
1464
2177
|
};
|
|
1465
2178
|
/** The native verbs, in the order the messages name them. */
|
|
@@ -1477,19 +2190,21 @@ const VISUAL_TESTS = path.join("tests", "visual", "index.ts");
|
|
|
1477
2190
|
const DEFAULT_PORT = "3000";
|
|
1478
2191
|
/** The highest port number. */
|
|
1479
2192
|
const MAX_PORT = 65535;
|
|
2193
|
+
/** A host of `--editor`: a name or an IPv4 address, or an IPv6 address in brackets. */
|
|
2194
|
+
const EDITOR_HOST = /^(?:[\w.-]+|\[[\da-f:.]+\])$/i;
|
|
1480
2195
|
/** The text of `moku-game help`. */
|
|
1481
2196
|
const USAGE = [
|
|
1482
2197
|
"moku-game <command> [options]",
|
|
1483
2198
|
"",
|
|
1484
2199
|
" dev [--port 3000] [--packed] serve the game with hot reload",
|
|
1485
|
-
" build [--out dist/web]
|
|
1486
|
-
" native build <target> [--simulator]
|
|
2200
|
+
" build [--out dist/web] [--debug[=full] [--editor host:port]] pack the assets and build the page; --debug adds the editor agent",
|
|
2201
|
+
" native build <target> [--simulator] [--debug[=full] [--editor host:port]] build the native app (ios, macos, android)",
|
|
1487
2202
|
" native dev <target> run the native shell on the dev server",
|
|
1488
2203
|
" native doctor | native clean check or remove the native project",
|
|
1489
2204
|
" keys [--check] write generated/assets.ts and generated/manifest.json",
|
|
1490
2205
|
" pack [--no-cache] pack the assets into dist/assets",
|
|
1491
2206
|
" visual [--update] [--only <name>] run tests/visual/index.ts: headless, pixels on a Mac",
|
|
1492
|
-
" [--no-pixels | --pixels] [--webgl] [--dir tests/visual/baselines]",
|
|
2207
|
+
" [--no-pixels | --pixels] [--webgl] [--no-verify-frames] [--dir tests/visual/baselines]",
|
|
1493
2208
|
" [--tests tests/visual/index.ts] [--url <url>]",
|
|
1494
2209
|
" help print this text",
|
|
1495
2210
|
"",
|
|
@@ -1564,14 +2279,16 @@ function checkTokens(command, tokens) {
|
|
|
1564
2279
|
}
|
|
1565
2280
|
/**
|
|
1566
2281
|
* Reads the command line into one command with its flags, the paths resolved against the
|
|
1567
|
-
* working directory.
|
|
2282
|
+
* working directory. A bare `--debug` reads as `--debug=read`: a string flag without its value
|
|
2283
|
+
* would take the next word.
|
|
1568
2284
|
*
|
|
1569
|
-
* @param
|
|
2285
|
+
* @param given - The arguments after the bin.
|
|
1570
2286
|
* @param cwd - The working directory.
|
|
1571
2287
|
* @returns The command, `"help"`, or `undefined` when no command is named.
|
|
1572
2288
|
* @throws {Error} When the command is unknown or a flag is refused.
|
|
1573
2289
|
*/
|
|
1574
|
-
function parseCommand(
|
|
2290
|
+
function parseCommand(given, cwd) {
|
|
2291
|
+
const argv = given.map((word) => word === "--debug" ? "--debug=read" : word);
|
|
1575
2292
|
const loose = parseLoose(argv);
|
|
1576
2293
|
const word = loose.positionals[0];
|
|
1577
2294
|
if (loose.values.help === true || word === "help") return "help";
|
|
@@ -1598,7 +2315,47 @@ function parseCommand(argv, cwd) {
|
|
|
1598
2315
|
*/
|
|
1599
2316
|
function refuseExtra(parsed, count) {
|
|
1600
2317
|
const extra = parsed.rest[count];
|
|
1601
|
-
if (extra
|
|
2318
|
+
if (extra === void 0) return;
|
|
2319
|
+
const advice = parsed.values.debug !== void 0 && (extra === "full" || extra === "read") ? ` Write --debug=${extra}.` : "";
|
|
2320
|
+
throw new Error(`[game] ${parsed.command}: unexpected argument "${extra}".${advice}`);
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Reads `--debug`: no flag is a release page, a bare flag or `read` the read profile, `full`
|
|
2324
|
+
* every command.
|
|
2325
|
+
*
|
|
2326
|
+
* @param value - The value of the flag, `read` for a bare `--debug`.
|
|
2327
|
+
* @param command - The command word, for the message.
|
|
2328
|
+
* @returns The profile, or `undefined` without the flag.
|
|
2329
|
+
* @throws {Error} When the value is another word.
|
|
2330
|
+
* @example
|
|
2331
|
+
* ```ts
|
|
2332
|
+
* debugLevelOf("full"); // "full"
|
|
2333
|
+
* ```
|
|
2334
|
+
*/
|
|
2335
|
+
function debugLevelOf(value, command = "build") {
|
|
2336
|
+
if (value === void 0 || value === "read" || value === "full") return value;
|
|
2337
|
+
throw new Error(`[game] ${command}: --debug takes no value, "read" or "full", got "${value}".`);
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Reads `--editor`: the `host:port` the page agent of a debug page dials, 127.0.0.1:47600 when
|
|
2341
|
+
* left out.
|
|
2342
|
+
*
|
|
2343
|
+
* @param value - The value of the flag.
|
|
2344
|
+
* @param command - The command word, for the message.
|
|
2345
|
+
* @returns The address.
|
|
2346
|
+
* @throws {Error} When the value is not a host and a whole port from 1 to 65535.
|
|
2347
|
+
* @example
|
|
2348
|
+
* ```ts
|
|
2349
|
+
* editorOf("192.168.1.20:47600"); // "192.168.1.20:47600"
|
|
2350
|
+
* ```
|
|
2351
|
+
*/
|
|
2352
|
+
function editorOf(value, command = "build") {
|
|
2353
|
+
if (value === void 0) return DEFAULT_EDITOR;
|
|
2354
|
+
const colon = value.lastIndexOf(":");
|
|
2355
|
+
const host = value.slice(0, Math.max(colon, 0));
|
|
2356
|
+
const port = value.slice(colon + 1);
|
|
2357
|
+
if (!(colon > 0 && EDITOR_HOST.test(host) && /^\d+$/.test(port) && Number(port) >= 1 && Number(port) <= MAX_PORT)) throw new Error(`[game] ${command}: --editor takes host:port, got "${value}".`);
|
|
2358
|
+
return value;
|
|
1602
2359
|
}
|
|
1603
2360
|
/**
|
|
1604
2361
|
* Reads `--port`: an integer 0-65535, 3000 when left out.
|
|
@@ -1667,6 +2424,7 @@ async function runNativeVerb(parsed, deps) {
|
|
|
1667
2424
|
if (word === void 0) throw new Error("[game] native: name a verb: build, dev, doctor, clean.");
|
|
1668
2425
|
if (verb === void 0) throw new Error(`[game] native: no verb "${word}". Name one of build, dev, doctor, clean.`);
|
|
1669
2426
|
refuseExtra(parsed, 2);
|
|
2427
|
+
const debug = nativeDebugOf(parsed, verb, target);
|
|
1670
2428
|
const settings = await loadSettings(parsed.root);
|
|
1671
2429
|
const where = {
|
|
1672
2430
|
cwd: parsed.root,
|
|
@@ -1674,11 +2432,36 @@ async function runNativeVerb(parsed, deps) {
|
|
|
1674
2432
|
};
|
|
1675
2433
|
const options = {
|
|
1676
2434
|
...target === void 0 ? {} : { target },
|
|
1677
|
-
...verb === "build" && parsed.values.simulator === true ? { simulator: true } : {}
|
|
2435
|
+
...verb === "build" && parsed.values.simulator === true ? { simulator: true } : {},
|
|
2436
|
+
...debug
|
|
1678
2437
|
};
|
|
2438
|
+
if (debug.debug !== void 0 && target === "android") deps.ui.warn("native: a DEBUG app with the editor agent, installed beside the store app. Never ship it to a store.");
|
|
1679
2439
|
return await deps.native(verb, settings, where, options) ? 0 : 1;
|
|
1680
2440
|
}
|
|
1681
2441
|
/**
|
|
2442
|
+
* Reads `--debug` and `--editor` of `moku-game native`: they go with `native build android`
|
|
2443
|
+
* only in this release.
|
|
2444
|
+
*
|
|
2445
|
+
* @param parsed - The command line.
|
|
2446
|
+
* @param verb - The native verb.
|
|
2447
|
+
* @param target - The target the command line names, unchecked.
|
|
2448
|
+
* @returns The profile and the editor address as given, each only when given.
|
|
2449
|
+
* @throws {Error} When either goes with another verb, `--editor` comes without `--debug`, a value
|
|
2450
|
+
* is refused, or the target is not `android`.
|
|
2451
|
+
*/
|
|
2452
|
+
function nativeDebugOf(parsed, verb, target) {
|
|
2453
|
+
const debug = debugLevelOf(parsed.values.debug, "native");
|
|
2454
|
+
const editor = parsed.values.editor;
|
|
2455
|
+
if (debug === void 0 && editor === void 0) return {};
|
|
2456
|
+
if (verb !== "build") throw new Error("[game] native: --debug goes with native build only.");
|
|
2457
|
+
if (debug === void 0) throw new Error("[game] native: --editor goes with --debug.");
|
|
2458
|
+
if (target !== void 0 && target !== "android") throw new Error(`[game] native: --debug builds Android only in this release (got ${target}).`);
|
|
2459
|
+
return editor === void 0 ? { debug } : {
|
|
2460
|
+
debug,
|
|
2461
|
+
editor: editorOf(editor, "native")
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
1682
2465
|
* Runs `moku-game keys` or `pack` through the asset scanner, with the layers of `config.ts`.
|
|
1683
2466
|
*
|
|
1684
2467
|
* @param parsed - The command line.
|
|
@@ -1754,6 +2537,31 @@ function hop(argv, parsed, deps) {
|
|
|
1754
2537
|
}).exited;
|
|
1755
2538
|
}
|
|
1756
2539
|
/**
|
|
2540
|
+
* Runs `moku-game build`: a release page, or with `--debug[=full]` a debug page whose agent dials
|
|
2541
|
+
* `--editor`.
|
|
2542
|
+
*
|
|
2543
|
+
* @param parsed - The command line.
|
|
2544
|
+
* @param deps - The process seams.
|
|
2545
|
+
* @returns The exit code of the build.
|
|
2546
|
+
* @throws {Error} When an argument, `--debug` or `--editor` is refused.
|
|
2547
|
+
*/
|
|
2548
|
+
function runBuildCommand(parsed, deps) {
|
|
2549
|
+
refuseExtra(parsed, 0);
|
|
2550
|
+
const debug = debugLevelOf(parsed.values.debug);
|
|
2551
|
+
const editor = parsed.values.editor;
|
|
2552
|
+
if (debug === void 0 && editor !== void 0) throw new Error("[game] build: --editor goes with --debug.");
|
|
2553
|
+
const out = parsed.values.out ?? path.join(parsed.root, "dist", "web");
|
|
2554
|
+
return runBuild({
|
|
2555
|
+
root: parsed.root,
|
|
2556
|
+
out: path.resolve(deps.cwd, out),
|
|
2557
|
+
servePlugins: parsed.servePlugins,
|
|
2558
|
+
...debug === void 0 ? {} : {
|
|
2559
|
+
debug,
|
|
2560
|
+
editor: editorOf(editor)
|
|
2561
|
+
}
|
|
2562
|
+
}, deps);
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
1757
2565
|
* Runs one checked command.
|
|
1758
2566
|
*
|
|
1759
2567
|
* @param argv - The arguments after the bin.
|
|
@@ -1768,15 +2576,7 @@ async function runParsed(argv, parsed, deps) {
|
|
|
1768
2576
|
if (parsed.command !== "dev" && parsed.command !== "visual" && parsed.preload.length > 0 && deps.env.MOKU_GAME_CHILD !== "1") return hop(argv, parsed, deps);
|
|
1769
2577
|
switch (parsed.command) {
|
|
1770
2578
|
case "dev": return runServe(parsed, deps);
|
|
1771
|
-
case "build":
|
|
1772
|
-
refuseExtra(parsed, 0);
|
|
1773
|
-
const out = parsed.values.out ?? path.join(parsed.root, "dist", "web");
|
|
1774
|
-
return runBuild({
|
|
1775
|
-
root: parsed.root,
|
|
1776
|
-
out: path.resolve(deps.cwd, out),
|
|
1777
|
-
servePlugins: parsed.servePlugins
|
|
1778
|
-
}, deps);
|
|
1779
|
-
}
|
|
2579
|
+
case "build": return runBuildCommand(parsed, deps);
|
|
1780
2580
|
case "native": return runNativeVerb(parsed, deps);
|
|
1781
2581
|
case "keys":
|
|
1782
2582
|
case "pack": return runScan(parsed, deps);
|
|
@@ -1825,7 +2625,8 @@ async function runCommand(argv, deps) {
|
|
|
1825
2625
|
* keeps `generated/` fresh while a dev page is open. A file event is only a trigger. When the
|
|
1826
2626
|
* events stop for a quiet period, a batch walks the game and reads the size and the time stamp of
|
|
1827
2627
|
* 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.
|
|
2628
|
+
* when they differ from the batch before. A folder that becomes or stops being a sub-feature scans
|
|
2629
|
+
* too: the keys and bundles of its files move between owners. After a scan it writes `.moku/assets-stamp.ts` from the
|
|
1829
2630
|
* asset files: every file with its stamp, and the ones this batch found changed, so the dev page
|
|
1830
2631
|
* reads only those again. One scan runs at a time, and a failed scan never stops the watch. A
|
|
1831
2632
|
* batch that scanned is followed by one more walk: the platform reports only the first path of a
|
|
@@ -1884,6 +2685,22 @@ function readFolder(root, folder, walk) {
|
|
|
1884
2685
|
}
|
|
1885
2686
|
}
|
|
1886
2687
|
/**
|
|
2688
|
+
* Adds the feature owners below the top level to the inputs: a folder that becomes or stops being
|
|
2689
|
+
* a sub-feature moves keys and bundles between owners without any asset or strings file changing.
|
|
2690
|
+
* The trailing `/` keeps the entry apart from every file. A name two folders share adds one entry
|
|
2691
|
+
* with the message instead of throwing: the scan reports the clash, and a rename moves the entry.
|
|
2692
|
+
*
|
|
2693
|
+
* @param root - The real game folder.
|
|
2694
|
+
* @param walk - The inputs so far.
|
|
2695
|
+
*/
|
|
2696
|
+
function readOwners(root, walk) {
|
|
2697
|
+
try {
|
|
2698
|
+
for (const owner of findFeatureOwners(root)) if (owner.depth >= 1) walk.inputs.set(`${owner.dir}/`, `sub-feature of ${owner.parent}`);
|
|
2699
|
+
} catch (error) {
|
|
2700
|
+
walk.inputs.set("features/", `clash: ${error instanceof Error ? error.message : String(error)}`);
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
1887
2704
|
* Walks a game folder for the inputs of the scan.
|
|
1888
2705
|
*
|
|
1889
2706
|
* @param root - The real game folder.
|
|
@@ -1895,6 +2712,7 @@ function walkInputs(root) {
|
|
|
1895
2712
|
folders: []
|
|
1896
2713
|
};
|
|
1897
2714
|
readFolder(root, "", walk);
|
|
2715
|
+
readOwners(root, walk);
|
|
1898
2716
|
return walk;
|
|
1899
2717
|
}
|
|
1900
2718
|
/**
|
|
@@ -2129,13 +2947,34 @@ function systemRowsOf(settings) {
|
|
|
2129
2947
|
return names.map((name) => ({ name }));
|
|
2130
2948
|
}
|
|
2131
2949
|
/**
|
|
2950
|
+
* The web build command native runs: `moku-game build`, with the debug profile and the editor
|
|
2951
|
+
* address of a debug app.
|
|
2952
|
+
*
|
|
2953
|
+
* @param where - The game folder and the command that runs `moku-game`.
|
|
2954
|
+
* @param debug - The debug profile, or `undefined` for a store app.
|
|
2955
|
+
* @returns The command.
|
|
2956
|
+
* @example
|
|
2957
|
+
* ```ts
|
|
2958
|
+
* webBuildOf({ cwd: "/g", command: '"bun" "/g/bin/moku-game.mjs"' }, { level: "full", editor: "192.168.1.20:47600" }); // '"bun" "/g/bin/moku-game.mjs" build --debug=full --editor 192.168.1.20:47600'
|
|
2959
|
+
* ```
|
|
2960
|
+
*/
|
|
2961
|
+
function webBuildOf(where, debug) {
|
|
2962
|
+
if (debug === void 0) return `${where.command} build`;
|
|
2963
|
+
const level = debug.level === "full" ? "--debug=full" : "--debug";
|
|
2964
|
+
const editor = debug.editor === void 0 ? "" : ` --editor ${debug.editor}`;
|
|
2965
|
+
return `${where.command} build ${level}${editor}`;
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2132
2968
|
* Maps a game's resolved `config.ts` to the config of `@moku-labs/native`: the app from `native`
|
|
2133
2969
|
* and `page`, the web build through `moku-game`, the system rows, the targets, and the Tauri
|
|
2134
|
-
* project and output folders under the game.
|
|
2970
|
+
* project and output folders under the game. An Android app composes native's `shell` system
|
|
2971
|
+
* first, the one that sets `globalThis.__MOKU_NATIVE__`. A debug app builds its web page with the
|
|
2972
|
+
* same profile and `--editor`; its name and identifier stay the game's.
|
|
2135
2973
|
*
|
|
2136
2974
|
* @param settings - The resolved `config.ts` of the game.
|
|
2137
2975
|
* @param where - The game folder and the command that runs `moku-game`.
|
|
2138
2976
|
* @param target - The target of the verb, used when `config.ts` names no targets.
|
|
2977
|
+
* @param debug - The debug profile of a debug app; left out for a store app.
|
|
2139
2978
|
* @returns The config for `createApp({ config })` of `@moku-labs/native`.
|
|
2140
2979
|
* @throws {Error} When `config.ts` has no native section.
|
|
2141
2980
|
* @example
|
|
@@ -2144,8 +2983,9 @@ function systemRowsOf(settings) {
|
|
|
2144
2983
|
* nativeConfigOf(resolveConfig({ page: { title: "T" }, native: { name: "T", identifier: "com.x.t" } }), where, "ios").web.dist; // "/g/dist/web"
|
|
2145
2984
|
* ```
|
|
2146
2985
|
*/
|
|
2147
|
-
function nativeConfigOf(settings, where, target) {
|
|
2986
|
+
function nativeConfigOf(settings, where, target, debug) {
|
|
2148
2987
|
const native = nativeSectionOf(settings);
|
|
2988
|
+
const rows = systemRowsOf(settings);
|
|
2149
2989
|
const config = {
|
|
2150
2990
|
app: {
|
|
2151
2991
|
name: native.name,
|
|
@@ -2155,12 +2995,12 @@ function nativeConfigOf(settings, where, target) {
|
|
|
2155
2995
|
},
|
|
2156
2996
|
web: {
|
|
2157
2997
|
cwd: where.cwd,
|
|
2158
|
-
build:
|
|
2998
|
+
build: webBuildOf(where, debug),
|
|
2159
2999
|
devCommand: `${where.command} dev --port ${DEV_PORT}`,
|
|
2160
3000
|
devUrl: `http://127.0.0.1:${DEV_PORT}`,
|
|
2161
3001
|
dist: path.join(where.cwd, "dist/web")
|
|
2162
3002
|
},
|
|
2163
|
-
system:
|
|
3003
|
+
system: target === "android" ? [{ name: "shell" }, ...rows.filter((row) => row.name !== "shell")] : rows,
|
|
2164
3004
|
projectDir: path.join(where.cwd, ".moku/tauri"),
|
|
2165
3005
|
outDir: path.join(where.cwd, "dist-native")
|
|
2166
3006
|
};
|
|
@@ -2191,17 +3031,20 @@ function targetOf(known, verb, given) {
|
|
|
2191
3031
|
* @param cli - The cli of the native app.
|
|
2192
3032
|
* @param verb - The native verb.
|
|
2193
3033
|
* @param target - The checked target.
|
|
2194
|
-
* @param
|
|
3034
|
+
* @param options - The simulator flag, and the debug profile of a debug app.
|
|
2195
3035
|
* @returns The boolean of `doctor`, true for the other verbs once they finish.
|
|
2196
3036
|
*/
|
|
2197
|
-
async function runVerb(cli, verb, target,
|
|
3037
|
+
async function runVerb(cli, verb, target, options) {
|
|
2198
3038
|
switch (verb) {
|
|
2199
|
-
case "build":
|
|
2200
|
-
|
|
3039
|
+
case "build": {
|
|
3040
|
+
const build = {
|
|
2201
3041
|
target,
|
|
2202
|
-
simulator
|
|
2203
|
-
|
|
3042
|
+
simulator: options.simulator,
|
|
3043
|
+
...options.debug === void 0 ? {} : { debug: true }
|
|
3044
|
+
};
|
|
3045
|
+
await cli.build(build);
|
|
2204
3046
|
return true;
|
|
3047
|
+
}
|
|
2205
3048
|
case "dev":
|
|
2206
3049
|
await cli.dev({ target });
|
|
2207
3050
|
return true;
|
|
@@ -2218,7 +3061,7 @@ async function runVerb(cli, verb, target, simulator) {
|
|
|
2218
3061
|
* @param verb - The native verb: build, dev, doctor or clean.
|
|
2219
3062
|
* @param settings - The resolved `config.ts` of the game.
|
|
2220
3063
|
* @param where - The game folder and the command that runs `moku-game`.
|
|
2221
|
-
* @param options - The target and the
|
|
3064
|
+
* @param options - The target, the simulator flag, and the debug profile of a debug app.
|
|
2222
3065
|
* @returns True when the verb succeeded, false when it failed.
|
|
2223
3066
|
* @throws {Error} When `config.ts` has no native section, `@moku-labs/native` is not installed, or
|
|
2224
3067
|
* the target is unknown or missing.
|
|
@@ -2226,13 +3069,17 @@ async function runVerb(cli, verb, target, simulator) {
|
|
|
2226
3069
|
async function runNative(verb, settings, where, options) {
|
|
2227
3070
|
nativeSectionOf(settings);
|
|
2228
3071
|
const native = await import("@moku-labs/native").catch((error) => {
|
|
2229
|
-
throw new Error("[game] moku-game native needs @moku-labs/native.\n Install it: bun add -d @moku-labs/native@^0.3.
|
|
3072
|
+
throw new Error("[game] moku-game native needs @moku-labs/native.\n Install it: bun add -d @moku-labs/native@^0.3.9.", { cause: error });
|
|
2230
3073
|
});
|
|
2231
3074
|
const target = targetOf(native.TARGETS, verb, options.target);
|
|
2232
|
-
const
|
|
3075
|
+
const debug = options.debug === void 0 ? void 0 : {
|
|
3076
|
+
level: options.debug,
|
|
3077
|
+
editor: options.editor
|
|
3078
|
+
};
|
|
3079
|
+
const app = native.createApp({ config: nativeConfigOf(settings, where, target, debug) });
|
|
2233
3080
|
await app.start();
|
|
2234
3081
|
try {
|
|
2235
|
-
return await runVerb(app.cli, verb, target, options
|
|
3082
|
+
return await runVerb(app.cli, verb, target, options);
|
|
2236
3083
|
} catch {
|
|
2237
3084
|
return false;
|
|
2238
3085
|
} finally {
|
|
@@ -2245,11 +3092,35 @@ async function runNative(verb, settings, where, options) {
|
|
|
2245
3092
|
* @file The `@moku-labs/game/cli` door, node and bun only: the `moku-game` bin and the editor's
|
|
2246
3093
|
* seam. `runCli` runs one command line of `moku-game` (dev, build, native, keys, pack, visual,
|
|
2247
3094
|
* help) in this process; `preparePage` writes a game's dev page into `<game>/.moku/` for a server the
|
|
2248
|
-
* caller starts itself, as the editor does,
|
|
2249
|
-
* while that page is open. It imports no Pixi, and the root entry never imports it. The code
|
|
3095
|
+
* caller starts itself, as the editor does, `watchKeys` keeps the game's `generated/` fresh
|
|
3096
|
+
* while that page is open, and `watchEngine` tells when the installed engine changed under it. It imports no Pixi, and the root entry never imports it. The code
|
|
2250
3097
|
* lives in `app/`; this file wires the real process into it.
|
|
2251
3098
|
*/
|
|
2252
3099
|
/**
|
|
3100
|
+
* Watches a folder with `fs.watch`: a watched folder that disappears ends that watch, and the
|
|
3101
|
+
* process runs on.
|
|
3102
|
+
*
|
|
3103
|
+
* @param folder - The folder or file.
|
|
3104
|
+
* @param listener - Gets the event name.
|
|
3105
|
+
* @returns The watcher.
|
|
3106
|
+
*/
|
|
3107
|
+
function watchFolder(folder, listener) {
|
|
3108
|
+
const watcher = watch(folder, (event) => listener(event));
|
|
3109
|
+
watcher.on("error", () => watcher.close());
|
|
3110
|
+
return watcher;
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Bun's resolver: the file a specifier gives from a folder.
|
|
3114
|
+
*
|
|
3115
|
+
* @param specifier - The specifier.
|
|
3116
|
+
* @param from - The folder.
|
|
3117
|
+
* @returns The absolute path.
|
|
3118
|
+
* @throws {Error} When it does not resolve.
|
|
3119
|
+
*/
|
|
3120
|
+
function bunResolve(specifier, from) {
|
|
3121
|
+
return Bun.resolveSync(specifier, from);
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
2253
3124
|
* Runs the asset scanner's command line with the string tools of i18n: the scan behind
|
|
2254
3125
|
* `moku-game keys`, `pack`, and the keys watch.
|
|
2255
3126
|
*
|
|
@@ -2303,17 +3174,14 @@ function processDeps() {
|
|
|
2303
3174
|
process.off(signal, handler);
|
|
2304
3175
|
};
|
|
2305
3176
|
},
|
|
2306
|
-
watch:
|
|
2307
|
-
const watcher = watch(folder, (event) => listener(event));
|
|
2308
|
-
watcher.on("error", () => watcher.close());
|
|
2309
|
-
return watcher;
|
|
2310
|
-
},
|
|
3177
|
+
watch: watchFolder,
|
|
2311
3178
|
watchKeys: (root, settings) => watchKeysAt(root, settings, keysSeams(ui)),
|
|
2312
3179
|
assets: (argv) => runScanner(argv, ui),
|
|
2313
3180
|
native: runNative,
|
|
2314
|
-
resolve:
|
|
3181
|
+
resolve: bunResolve,
|
|
2315
3182
|
loadPage: (file) => import(file),
|
|
2316
|
-
visual: () => import("./visual.mjs")
|
|
3183
|
+
visual: () => import("./visual.mjs"),
|
|
3184
|
+
now: () => Date.now()
|
|
2317
3185
|
};
|
|
2318
3186
|
}
|
|
2319
3187
|
/**
|
|
@@ -2328,6 +3196,7 @@ function processDeps() {
|
|
|
2328
3196
|
* // bin/moku-game.mjs in a game folder:
|
|
2329
3197
|
* await runCli(["dev", "--port", "0"]); // 0 after Ctrl+C; stdout had "http://localhost:<port>/"
|
|
2330
3198
|
* await runCli(["visual", "--no-pixels"]); // 0 when every checkpoint of tests/visual/index.ts is the same
|
|
3199
|
+
* await runCli(["build", "--debug"]); // 0: dist/web carries the editor agent; one warning names build debug-20261014T153012Z
|
|
2331
3200
|
* await runCli(["help"]); // 0
|
|
2332
3201
|
* ```
|
|
2333
3202
|
*/
|
|
@@ -2336,8 +3205,9 @@ async function runCli(argv) {
|
|
|
2336
3205
|
}
|
|
2337
3206
|
/**
|
|
2338
3207
|
* Writes a game's dev page into `<root>/.moku/` for a server the caller starts: `index.html`,
|
|
2339
|
-
* `
|
|
2340
|
-
* the game's `.dev` modules), and
|
|
3208
|
+
* `script.ts`, `dev.ts` with the flags and the engine id, `main.ts` with the scenarios of
|
|
3209
|
+
* `tests/scenarios/` (and, with agents, the agents and the game's `.dev` modules), and
|
|
3210
|
+
* `bunfig.toml` with the engine's hot plugin. It also writes the
|
|
2341
3211
|
* first `assets-stamp.ts`, the empty one, when the game has none: `main.ts` imports it, and
|
|
2342
3212
|
* `watchKeys` rewrites it when an asset file changes. The page accepts the new stamp: new bytes
|
|
2343
3213
|
* of a file swap in place, a new or removed file reloads the page. It starts no server and no
|
|
@@ -2345,15 +3215,16 @@ async function runCli(argv) {
|
|
|
2345
3215
|
*
|
|
2346
3216
|
* @param root - The game folder, relative to the cwd or absolute.
|
|
2347
3217
|
* @param options - The agents, the preloads and the extra bundler plugins.
|
|
2348
|
-
* @returns The absolute paths of the HTML and the bunfig.
|
|
3218
|
+
* @returns The absolute paths of the HTML and the bunfig, and the engine the page was written for.
|
|
2349
3219
|
* @throws {Error} When the game has no `config.ts` or `index.ts`, a config value is refused, the
|
|
2350
3220
|
* game does not resolve `@moku-labs/game/hot`, or a preload or plugin is not a file.
|
|
2351
3221
|
* @example
|
|
2352
3222
|
* ```ts
|
|
2353
3223
|
* // The editor bin, started with --root games/timber:
|
|
2354
3224
|
* const { preparePage } = await import(Bun.resolveSync("@moku-labs/game/cli", root));
|
|
2355
|
-
* await preparePage("games/timber", { agents: ["@moku-labs/editor/agent/page"] });
|
|
2356
|
-
* // { html: "<cwd>/games/timber/.moku/index.html", bunfig: "<cwd>/games/timber/.moku/bunfig.toml" }
|
|
3225
|
+
* const page = await preparePage("games/timber", { agents: ["@moku-labs/editor/agent/page"] });
|
|
3226
|
+
* // { html: "<cwd>/games/timber/.moku/index.html", bunfig: "<cwd>/games/timber/.moku/bunfig.toml", engine: { … } }
|
|
3227
|
+
* // page.engine.id: "0.14.12+9c41e0"
|
|
2357
3228
|
* ```
|
|
2358
3229
|
*/
|
|
2359
3230
|
async function preparePage(root, options = {}) {
|
|
@@ -2362,7 +3233,7 @@ async function preparePage(root, options = {}) {
|
|
|
2362
3233
|
...options.agents === void 0 ? {} : { agents: options.agents },
|
|
2363
3234
|
preload: absolute(options.preload),
|
|
2364
3235
|
servePlugins: absolute(options.servePlugins)
|
|
2365
|
-
},
|
|
3236
|
+
}, bunResolve);
|
|
2366
3237
|
}
|
|
2367
3238
|
/**
|
|
2368
3239
|
* Keeps `generated/` of a game fresh while its dev page is open, for a server the caller runs:
|
|
@@ -2395,5 +3266,42 @@ async function watchKeys(root, options = {}) {
|
|
|
2395
3266
|
const game = path.resolve(root);
|
|
2396
3267
|
return watchKeysAt(game, await loadGame(game), keysSeams(createBrandConsole()), options);
|
|
2397
3268
|
}
|
|
3269
|
+
/**
|
|
3270
|
+
* Watches the engine a game resolves, for a page server the caller runs: the editor calls it next
|
|
3271
|
+
* to `preparePage`, and raw `moku-game dev` runs the same watch itself. It takes the fingerprint
|
|
3272
|
+
* of the installed engine now (the id `preparePage(...).engine` has), watches the engine's
|
|
3273
|
+
* folders, its patch and the install root's `package.json` and lock, and after 300 ms of quiet
|
|
3274
|
+
* fingerprints again: a new version or dist calls `onChange` with `restart: true` (the page server
|
|
3275
|
+
* must restart, Bun caches nothing across a restart), a changed patch alone with `restart: false`
|
|
3276
|
+
* (the install did not happen yet). An engine run from its sources is not watched. Sync: it
|
|
3277
|
+
* scans nothing.
|
|
3278
|
+
*
|
|
3279
|
+
* @param root - The game folder, relative to the cwd or absolute.
|
|
3280
|
+
* @param options - Where the changes and the errors go.
|
|
3281
|
+
* @returns The running watch; `close()` stops it.
|
|
3282
|
+
* @throws {Error} When the game does not resolve `@moku-labs/game/hot`.
|
|
3283
|
+
* @example
|
|
3284
|
+
* ```ts
|
|
3285
|
+
* // The editor bin, next to preparePage, started with --root games/timber:
|
|
3286
|
+
* const { preparePage, watchEngine } = await import(Bun.resolveSync("@moku-labs/game/cli", root));
|
|
3287
|
+
* const page = await preparePage("games/timber", { agents: ["@moku-labs/editor/agent/page"] });
|
|
3288
|
+
* page.engine.id; // "0.14.12+9c41e0"
|
|
3289
|
+
* const engine = watchEngine("games/timber", { onChange: change => change.restart && restartPageServer() });
|
|
3290
|
+
* // after `bun patch --commit` in the game: onChange({ before: { id: "0.14.12+9c41e0", … }, after: { id: "0.14.12+1a2b3c.patch.3f2a7b", … }, restart: true })
|
|
3291
|
+
* engine.close(); // when the editor closes the game
|
|
3292
|
+
* ```
|
|
3293
|
+
*/
|
|
3294
|
+
function watchEngine(root, options = {}) {
|
|
3295
|
+
const game = path.resolve(root);
|
|
3296
|
+
const start = engineFingerprint(game, hotPluginOf(game, bunResolve));
|
|
3297
|
+
const ui = createBrandConsole();
|
|
3298
|
+
const warn = (change) => {
|
|
3299
|
+
for (const line of engineChangeLines(change)) ui.warn(line);
|
|
3300
|
+
};
|
|
3301
|
+
return watchEngineAt(game, start, {
|
|
3302
|
+
watch: watchFolder,
|
|
3303
|
+
resolve: bunResolve
|
|
3304
|
+
}, options.onChange ?? warn, options.onError ?? ((message) => ui.warn(message)));
|
|
3305
|
+
}
|
|
2398
3306
|
//#endregion
|
|
2399
|
-
export { preparePage, runCli, watchKeys };
|
|
3307
|
+
export { preparePage, runCli, watchEngine, watchKeys };
|