@forgeax/engine-devkit 0.1.6 → 0.1.19
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 +155 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/engine-binding.unit.test.d.ts +2 -0
- package/dist/__tests__/engine-binding.unit.test.d.ts.map +1 -0
- package/dist/__tests__/native-preview-dispatch.test.d.ts +2 -0
- package/dist/__tests__/native-preview-dispatch.test.d.ts.map +1 -0
- package/dist/__tests__/native-preview-subject-draw.unit.test.d.ts +2 -0
- package/dist/__tests__/native-preview-subject-draw.unit.test.d.ts.map +1 -0
- package/dist/__tests__/single-html-runtime.e2e.test.d.ts +2 -0
- package/dist/__tests__/single-html-runtime.e2e.test.d.ts.map +1 -0
- package/dist/__tests__/single-html.test.d.ts +2 -0
- package/dist/__tests__/single-html.test.d.ts.map +1 -0
- package/dist/__tests__/software-browser-session.unit.test.d.ts +2 -0
- package/dist/__tests__/software-browser-session.unit.test.d.ts.map +1 -0
- package/dist/__tests__/software-capture-pixels.unit.test.d.ts +2 -0
- package/dist/__tests__/software-capture-pixels.unit.test.d.ts.map +1 -0
- package/dist/__tests__/software-capture.unit.test.d.ts +2 -0
- package/dist/__tests__/software-capture.unit.test.d.ts.map +1 -0
- package/dist/bootstrap-commands.d.ts.map +1 -1
- package/dist/cli-output.d.ts.map +1 -1
- package/dist/cli.mjs +3350 -475
- package/dist/cli.mjs.map +1 -1
- package/dist/commands.d.ts +2 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/dist.d.ts +2 -2
- package/dist/dist.d.ts.map +1 -1
- package/dist/engine-binding.d.ts +58 -0
- package/dist/engine-binding.d.ts.map +1 -0
- package/dist/host.d.ts +6 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +3679 -1368
- package/dist/index.mjs.map +1 -1
- package/dist/rhi-debug/cli-context.d.ts.map +1 -1
- package/dist/rhi-debug/operations.d.ts +3 -3
- package/dist/rhi-debug/operations.d.ts.map +1 -1
- package/dist/sdk-bootstrap.d.ts +6 -1
- package/dist/sdk-bootstrap.d.ts.map +1 -1
- package/dist/sdk-cli.mjs +35 -15
- package/dist/sdk-cli.mjs.map +1 -1
- package/dist/sdk.d.ts +1 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/single-html.d.ts +43 -0
- package/dist/single-html.d.ts.map +1 -0
- package/dist/software-capture.d.ts +161 -0
- package/dist/software-capture.d.ts.map +1 -0
- package/dist/tools/browser-host.d.ts.map +1 -1
- package/dist/tools/client.d.ts.map +1 -1
- package/dist/tools/commands.d.ts +5 -0
- package/dist/tools/commands.d.ts.map +1 -1
- package/dist/tools/native-preview.d.ts.map +1 -1
- package/dist/tools/preview-contributions.d.ts +0 -5
- package/dist/tools/preview-contributions.d.ts.map +1 -1
- package/dist/types.d.ts +23 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +34 -30
package/dist/cli.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { deflateRawSync } from 'zlib';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
4
|
-
import { readFile, mkdtemp, rm, readdir, mkdir, cp, rename, writeFile, copyFile, stat, symlink, unlink,
|
|
5
|
-
import { resolve, extname, dirname, basename, relative, isAbsolute, sep } from 'path';
|
|
3
|
+
import { createHash, randomUUID } from 'crypto';
|
|
4
|
+
import { readFile, mkdtemp, rm, readdir, mkdir, cp, rename, writeFile, realpath, copyFile, stat, symlink, unlink, access, readlink, lstat, rmdir } from 'fs/promises';
|
|
5
|
+
import { resolve, extname, dirname, basename, relative, isAbsolute, sep, join } from 'path';
|
|
6
|
+
import { GameProjectSchema } from '@forgeax/engine-project';
|
|
6
7
|
import { readFileSync, existsSync } from 'fs';
|
|
7
8
|
import { createRequire } from 'module';
|
|
8
9
|
import { pathToFileURL, fileURLToPath } from 'url';
|
|
@@ -13,31 +14,35 @@ import { gltfImporter } from '@forgeax/engine-gltf';
|
|
|
13
14
|
import { imageImporter } from '@forgeax/engine-image/image-importer';
|
|
14
15
|
import { BUILTIN_MESH_ASSETS } from '@forgeax/engine-pack/builtin';
|
|
15
16
|
import { scanInventory } from '@forgeax/engine-pack/scanner';
|
|
16
|
-
import { describeResourcePreviewFailure, validateCanonicalKitReceipt,
|
|
17
|
+
import { describeResourcePreviewFailure, validateCanonicalKitReceipt, RESOURCE_PREVIEW_DEFAULT_SIZE, createNativePreviewHost, bindPreviewHost, createResourcePreviewReport, validatePreviewArtifactManifest as validatePreviewArtifactManifest$1 } from '@forgeax/engine-preview';
|
|
17
18
|
import { createMaterialPackCooker } from '@forgeax/engine-shader-compiler';
|
|
18
19
|
import { createStandaloneRuntimeAssetBinding, ok, err } from '@forgeax/engine-types';
|
|
19
20
|
import { createParticleCodeNativeCookerFromRoots } from '@forgeax/engine-vfx-compiler';
|
|
20
21
|
import { pluginPack, reloadAssetHost } from '@forgeax/engine-vite-plugin-pack';
|
|
22
|
+
import { vitePluginRhiDebug } from '@forgeax/engine-vite-plugin-rhi-debug';
|
|
21
23
|
import { forgeaxShader } from '@forgeax/engine-vite-plugin-shader';
|
|
22
|
-
import {
|
|
24
|
+
import { tmpdir } from 'os';
|
|
25
|
+
import { parse } from 'parse5';
|
|
26
|
+
import { build, preview, parseAst, Visitor, createServer as createServer$1 } from 'vite';
|
|
23
27
|
import { runCliGltf } from '@forgeax/engine-gltf/cli-gltf';
|
|
24
28
|
import { scanEntries } from '@forgeax/engine-pack/cli-asset';
|
|
25
29
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
26
|
-
import { execFile } from 'child_process';
|
|
27
|
-
import { tmpdir } from 'os';
|
|
30
|
+
import { spawn, execFile } from 'child_process';
|
|
28
31
|
import { promisify } from 'util';
|
|
29
|
-
import { decodeTape, openReplay, buildFrameModel, createRhiDebugError } from '@forgeax/engine-rhi-debug';
|
|
30
|
-
import { rhi, createShaderModule } from '@forgeax/engine-rhi-
|
|
31
|
-
import { build, preview, createServer as createServer$1 } from 'vite';
|
|
32
|
+
import { decodeTape, openReplay, buildFrameModel, replayDeviceRequest, createRhiDebugError } from '@forgeax/engine-rhi-debug';
|
|
33
|
+
import { rhi, createShaderModule } from '@forgeax/engine-rhi-webgpu';
|
|
32
34
|
import { createServer } from 'net';
|
|
35
|
+
import { FORGEAX_FRAME_SUBMITTED_DATASET, createToolPreviewRecipe } from '@forgeax/engine-app';
|
|
36
|
+
import { parseImage } from '@forgeax/engine-image/parse-image';
|
|
37
|
+
import { chromium } from 'playwright';
|
|
33
38
|
import { startVitest } from 'vitest/node';
|
|
34
39
|
import materialPreviewPlugin from '@forgeax/engine-preview/material';
|
|
35
40
|
import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
|
|
36
41
|
import texturePreviewPlugin from '@forgeax/engine-preview/texture';
|
|
37
42
|
import vfxPreviewPlugin from '@forgeax/engine-preview/vfx';
|
|
38
|
-
import { isSerializableValue, domainFailureError, createToolRuntime, capabilityUnavailableError, defineTool, validateArtifactManifest } from '@forgeax/engine-tool-runtime';
|
|
43
|
+
import { isSerializableValue, domainFailureError, createToolRuntime, capabilityUnavailableError, defineTool, createPreviewArtifactManifest, validatePreviewArtifactManifest, validateArtifactManifest } from '@forgeax/engine-tool-runtime';
|
|
39
44
|
import { Context, isToolPlugin } from '@forgeax/engine-plugin';
|
|
40
|
-
import { installCatalogLoader, projectPluginEntries } from '@forgeax/engine-plugin/loader';
|
|
45
|
+
import { installCatalogLoader, projectPluginEntries, createContextCapabilityResolver } from '@forgeax/engine-plugin/loader';
|
|
41
46
|
|
|
42
47
|
var __defProp = Object.defineProperty;
|
|
43
48
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -145,6 +150,20 @@ function mediaType(path) {
|
|
|
145
150
|
if (path.endsWith(".wasm")) return "application/wasm";
|
|
146
151
|
if (path.endsWith(".png")) return "image/png";
|
|
147
152
|
if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
|
|
153
|
+
if (path.endsWith(".css")) return "text/css";
|
|
154
|
+
if (path.endsWith(".svg")) return "image/svg+xml";
|
|
155
|
+
if (path.endsWith(".webp")) return "image/webp";
|
|
156
|
+
if (path.endsWith(".gif")) return "image/gif";
|
|
157
|
+
if (path.endsWith(".mp3")) return "audio/mpeg";
|
|
158
|
+
if (path.endsWith(".wav")) return "audio/wav";
|
|
159
|
+
if (path.endsWith(".ogg")) return "audio/ogg";
|
|
160
|
+
if (path.endsWith(".mp4")) return "video/mp4";
|
|
161
|
+
if (path.endsWith(".webm")) return "video/webm";
|
|
162
|
+
if (path.endsWith(".woff")) return "font/woff";
|
|
163
|
+
if (path.endsWith(".woff2")) return "font/woff2";
|
|
164
|
+
if (path.endsWith(".ttf")) return "font/ttf";
|
|
165
|
+
if (path.endsWith(".otf")) return "font/otf";
|
|
166
|
+
if (path.endsWith(".wgsl") || path.endsWith(".glsl")) return "text/plain";
|
|
148
167
|
return "application/octet-stream";
|
|
149
168
|
}
|
|
150
169
|
async function filesUnder(root, directory = root) {
|
|
@@ -301,146 +320,668 @@ var init_dist = __esm({
|
|
|
301
320
|
"src/dist.ts"() {
|
|
302
321
|
}
|
|
303
322
|
});
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
var host_exports = {};
|
|
307
|
-
__export(host_exports, {
|
|
308
|
-
createViteConfig: () => createViteConfig,
|
|
309
|
-
devKitDdcRoots: () => devKitDdcRoots,
|
|
310
|
-
ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
|
|
311
|
-
});
|
|
312
|
-
function ignoreDevKitCatalogPath(path) {
|
|
313
|
-
const normalized = path.replace(/\\/g, "/");
|
|
314
|
-
return normalized.split("/").includes("shaders");
|
|
323
|
+
function projectError(code, expected, hint, detail) {
|
|
324
|
+
return { ok: false, error: { code, expected, hint, detail } };
|
|
315
325
|
}
|
|
316
|
-
function
|
|
317
|
-
return
|
|
318
|
-
buildCacheRoot: resolve(projectRoot2, ".forgeax", "ddc", "build-cache"),
|
|
319
|
-
projectDdcRoot: resolve(projectRoot2, ".forgeax", "ddc", "v2")
|
|
320
|
-
};
|
|
326
|
+
async function readJson(path) {
|
|
327
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
321
328
|
}
|
|
322
|
-
function
|
|
323
|
-
|
|
329
|
+
function firstUnsupportedStandaloneRealm(entries2, inheritedRealm = "engine") {
|
|
330
|
+
for (const entry of entries2) {
|
|
331
|
+
const realm = entry.realm ?? inheritedRealm ?? "engine";
|
|
332
|
+
if (realm !== "engine") return { id: entry.id, realm };
|
|
333
|
+
if (entry.group === true) {
|
|
334
|
+
const unsupported = firstUnsupportedStandaloneRealm(
|
|
335
|
+
entry.config,
|
|
336
|
+
realm
|
|
337
|
+
);
|
|
338
|
+
if (unsupported !== void 0) return unsupported;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return void 0;
|
|
324
342
|
}
|
|
325
|
-
function
|
|
326
|
-
return
|
|
343
|
+
function pluginModuleNames(entries2) {
|
|
344
|
+
return entries2.flatMap(
|
|
345
|
+
(entry) => entry.group === true ? pluginModuleNames(entry.config) : [entry.name]
|
|
346
|
+
);
|
|
327
347
|
}
|
|
328
|
-
async function
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
348
|
+
async function readProjectFacts(rootInput = process.cwd()) {
|
|
349
|
+
const root = resolve(rootInput);
|
|
350
|
+
let forgeValue;
|
|
351
|
+
let packageValue;
|
|
352
|
+
try {
|
|
353
|
+
[forgeValue, packageValue] = await Promise.all([
|
|
354
|
+
readJson(resolve(root, "forge.json")),
|
|
355
|
+
readJson(resolve(root, "package.json"))
|
|
356
|
+
]);
|
|
357
|
+
} catch (cause) {
|
|
358
|
+
return projectError(
|
|
359
|
+
"project-manifest-unreadable",
|
|
360
|
+
"readable forge.json and package.json files",
|
|
361
|
+
"Run the command from a ForgeaX game root or pass its directory.",
|
|
362
|
+
{ root, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (forgeValue === null || typeof forgeValue !== "object") {
|
|
366
|
+
return projectError(
|
|
367
|
+
"project-manifest-invalid",
|
|
368
|
+
"forge.json to contain an object",
|
|
369
|
+
"Repair forge.json before running DevKit.",
|
|
370
|
+
{ root }
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
if (packageValue === null || typeof packageValue !== "object") {
|
|
374
|
+
return projectError(
|
|
375
|
+
"package-manifest-invalid",
|
|
376
|
+
"package.json to contain an object",
|
|
377
|
+
"Repair package.json before running DevKit.",
|
|
378
|
+
{ root }
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const parsedForge = GameProjectSchema.safeParse(forgeValue);
|
|
382
|
+
if (!parsedForge.success) {
|
|
383
|
+
return projectError(
|
|
384
|
+
"project-manifest-invalid",
|
|
385
|
+
"forge.json to satisfy @forgeax/engine-project GameProjectSchema",
|
|
386
|
+
"Repair the fields reported by the authoritative project schema.",
|
|
387
|
+
{ root, issues: parsedForge.error.issues }
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const forge = parsedForge.data;
|
|
391
|
+
if (forge.id.length === 0 || forge.name.length === 0 || forge.entry === void 0 || forge.entry.length === 0) {
|
|
392
|
+
return projectError(
|
|
393
|
+
"project-manifest-invalid",
|
|
394
|
+
"forge.json to declare id, name, and entry",
|
|
395
|
+
"Restore the project entry; plugin Entries remain optional additions.",
|
|
396
|
+
{ root }
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const plugins = forge.plugins ?? [];
|
|
400
|
+
const unsupportedRealm = firstUnsupportedStandaloneRealm(plugins);
|
|
401
|
+
if (unsupportedRealm !== void 0) {
|
|
402
|
+
return projectError(
|
|
403
|
+
"project-plugin-realm-unsupported",
|
|
404
|
+
"the standalone Devkit host to contain only engine-realm plugin Entries",
|
|
405
|
+
"Move Host or build plugins to a host that owns that physical realm.",
|
|
406
|
+
{ root, ...unsupportedRealm }
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const entryPath = isAbsolute(forge.entry) ? forge.entry : resolve(root, forge.entry);
|
|
410
|
+
try {
|
|
411
|
+
await readFile(entryPath);
|
|
412
|
+
} catch {
|
|
413
|
+
return projectError(
|
|
414
|
+
"project-entry-missing",
|
|
415
|
+
"forge.json#entry to resolve to a readable module",
|
|
416
|
+
"Restore the game entry or update forge.json#entry.",
|
|
417
|
+
{ root, entry: forge.entry }
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const packageJson = packageValue;
|
|
421
|
+
const forgeax = packageJson.forgeax;
|
|
422
|
+
const configuredRoots = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.roots : void 0;
|
|
423
|
+
const assetRoots = Array.isArray(configuredRoots) && configuredRoots.every((value) => typeof value === "string") ? configuredRoots : ["assets"];
|
|
424
|
+
const configuredImporters = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.importers : void 0;
|
|
425
|
+
const assetImporters = Array.isArray(configuredImporters) && configuredImporters.every((value) => typeof value === "string") ? configuredImporters : [];
|
|
426
|
+
const configuredPublicDir = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.publicDir : void 0;
|
|
427
|
+
const assetPublicDir = typeof configuredPublicDir === "string" ? configuredPublicDir : void 0;
|
|
428
|
+
const physics = forge.physics === "2d" || forge.physics === "3d" ? forge.physics : void 0;
|
|
429
|
+
const defaultScene = typeof forge.defaultScene === "string" && forge.defaultScene.length > 0 ? forge.defaultScene : void 0;
|
|
430
|
+
const normalizedEntry = forge.entry.startsWith("./") ? forge.entry : `./${forge.entry}`;
|
|
431
|
+
const bootstrapEntry = pluginModuleNames(plugins).map((name) => name.startsWith("./") ? name : `./${name}`).includes(normalizedEntry) ? void 0 : forge.entry;
|
|
432
|
+
return {
|
|
433
|
+
ok: true,
|
|
434
|
+
value: {
|
|
435
|
+
root,
|
|
436
|
+
id: forge.id,
|
|
437
|
+
name: forge.name,
|
|
438
|
+
entry: forge.entry,
|
|
439
|
+
...bootstrapEntry === void 0 ? {} : { bootstrapEntry },
|
|
440
|
+
plugins,
|
|
441
|
+
...physics === void 0 ? {} : { physics },
|
|
442
|
+
...defaultScene === void 0 ? {} : { defaultScene },
|
|
443
|
+
assetRoots,
|
|
444
|
+
...assetImporters.length === 0 ? {} : { assetImporters },
|
|
445
|
+
...assetPublicDir === void 0 ? {} : { assetPublicDir },
|
|
446
|
+
packageJson
|
|
336
447
|
}
|
|
337
|
-
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function commandError(cause, fallbackCode) {
|
|
451
|
+
if (cause !== null && typeof cause === "object" && "code" in cause && "expected" in cause && "hint" in cause && "detail" in cause) {
|
|
452
|
+
return cause;
|
|
338
453
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
{
|
|
346
|
-
schemaVersion: "2.0.0",
|
|
347
|
-
kind: "internal-text-package",
|
|
348
|
-
assets: missing.map((asset) => ({
|
|
349
|
-
guid: asset.guid,
|
|
350
|
-
kind: "mesh",
|
|
351
|
-
payload: { geometry: asset.geometry },
|
|
352
|
-
refs: [],
|
|
353
|
-
artifacts: {}
|
|
354
|
-
}))
|
|
355
|
-
},
|
|
356
|
-
null,
|
|
357
|
-
2
|
|
358
|
-
)}
|
|
359
|
-
`,
|
|
360
|
-
"utf8"
|
|
361
|
-
);
|
|
362
|
-
return packPath;
|
|
454
|
+
return {
|
|
455
|
+
code: fallbackCode,
|
|
456
|
+
expected: "the ForgeaX command to complete",
|
|
457
|
+
hint: "Inspect the underlying diagnostic and repair the owning input.",
|
|
458
|
+
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
459
|
+
};
|
|
363
460
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
for (; ; ) {
|
|
367
|
-
if (existsSync(resolve(cursor, "pnpm-workspace.yaml"))) return cursor;
|
|
368
|
-
const parent = dirname(cursor);
|
|
369
|
-
if (parent === cursor) return void 0;
|
|
370
|
-
cursor = parent;
|
|
461
|
+
var init_project = __esm({
|
|
462
|
+
"src/project.ts"() {
|
|
371
463
|
}
|
|
464
|
+
});
|
|
465
|
+
function bindingError(code, expected, hint, detail = {}) {
|
|
466
|
+
return { ok: false, error: { code, expected, hint, detail } };
|
|
372
467
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
468
|
+
function isMissing(cause) {
|
|
469
|
+
return cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT";
|
|
470
|
+
}
|
|
471
|
+
async function pathExists(path) {
|
|
472
|
+
try {
|
|
473
|
+
await access(path);
|
|
474
|
+
return true;
|
|
475
|
+
} catch {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function parseBinding(value, path) {
|
|
480
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
481
|
+
return bindingError(
|
|
482
|
+
"engine-binding-invalid",
|
|
483
|
+
"engine-binding.json to contain one object",
|
|
484
|
+
"Run forgeax engine unlink, then select a binding again.",
|
|
485
|
+
{ path }
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
const candidate = value;
|
|
489
|
+
if (candidate.schemaVersion !== ENGINE_BINDING_SCHEMA_VERSION) {
|
|
490
|
+
return bindingError(
|
|
491
|
+
"engine-binding-version-unsupported",
|
|
492
|
+
`engine-binding.json schemaVersion ${ENGINE_BINDING_SCHEMA_VERSION}`,
|
|
493
|
+
"Upgrade the SDK or remove the stale binding with forgeax engine unlink.",
|
|
494
|
+
{ path, schemaVersion: candidate.schemaVersion ?? null }
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
if (typeof candidate.path !== "string" || candidate.path.length === 0) {
|
|
498
|
+
return bindingError(
|
|
499
|
+
"engine-binding-path-missing",
|
|
500
|
+
"engine-binding.json to contain one non-empty local Engine path",
|
|
501
|
+
"Run forgeax engine use-local <engine-directory>.",
|
|
502
|
+
{ path }
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
ok: true,
|
|
507
|
+
value: {
|
|
508
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
509
|
+
path: resolve(candidate.path)
|
|
395
510
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
async function readEngineBinding(rootInput = process.cwd()) {
|
|
514
|
+
const root = resolve(rootInput);
|
|
515
|
+
const path = bindingFile(root);
|
|
516
|
+
try {
|
|
517
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
518
|
+
return parseBinding(value, path);
|
|
519
|
+
} catch (cause) {
|
|
520
|
+
if (isMissing(cause)) return { ok: true, value: null };
|
|
521
|
+
return bindingError(
|
|
522
|
+
"engine-binding-unreadable",
|
|
523
|
+
"engine-binding.json to be readable JSON",
|
|
524
|
+
"Repair or remove .forgeax/engine-binding.json, then select a binding again.",
|
|
525
|
+
{ path, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function writeEngineBinding(root, binding) {
|
|
530
|
+
const path = bindingFile(root);
|
|
531
|
+
const partial = `${path}.partial-${process.pid}`;
|
|
532
|
+
await mkdir(dirname(path), { recursive: true });
|
|
533
|
+
await writeFile(partial, `${JSON.stringify(binding, null, 2)}
|
|
534
|
+
`, "utf8");
|
|
535
|
+
await rename(partial, path);
|
|
399
536
|
}
|
|
400
537
|
function conditionalExport(value) {
|
|
401
538
|
if (typeof value === "string") return value;
|
|
402
|
-
if (value === null || Array.isArray(value)) return void 0;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
539
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
540
|
+
const record = value;
|
|
541
|
+
for (const key of ["import", "browser", "node", "default", "require", "types"]) {
|
|
542
|
+
const candidate = record[key];
|
|
406
543
|
const selected = conditionalExport(candidate);
|
|
407
544
|
if (selected !== void 0) return selected;
|
|
408
545
|
}
|
|
409
546
|
return void 0;
|
|
410
547
|
}
|
|
411
|
-
function
|
|
548
|
+
function packageEntry(manifest) {
|
|
412
549
|
const exportsValue = manifest.exports;
|
|
413
|
-
if (exportsValue
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
550
|
+
if (exportsValue !== void 0) {
|
|
551
|
+
if (typeof exportsValue === "object" && exportsValue !== null && !Array.isArray(exportsValue)) {
|
|
552
|
+
const root = exportsValue["."];
|
|
553
|
+
const selected2 = conditionalExport(root ?? exportsValue);
|
|
554
|
+
if (selected2 !== void 0) return selected2;
|
|
555
|
+
}
|
|
556
|
+
const selected = conditionalExport(exportsValue);
|
|
557
|
+
if (selected !== void 0) return selected;
|
|
417
558
|
}
|
|
418
|
-
|
|
419
|
-
|
|
559
|
+
return typeof manifest.module === "string" ? manifest.module : typeof manifest.main === "string" ? manifest.main : void 0;
|
|
560
|
+
}
|
|
561
|
+
async function readManifest(path) {
|
|
562
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
563
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
564
|
+
throw new Error(`engine-package-manifest-invalid: ${path}`);
|
|
420
565
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
566
|
+
return value;
|
|
567
|
+
}
|
|
568
|
+
async function inspectEngineWorkspace(workspaceInput) {
|
|
569
|
+
const root = resolve(workspaceInput);
|
|
570
|
+
const packageRoot = resolve(root, "packages");
|
|
571
|
+
try {
|
|
572
|
+
await access(resolve(root, "pnpm-workspace.yaml"));
|
|
573
|
+
const entries2 = await readdir(packageRoot, { withFileTypes: true });
|
|
574
|
+
const packages = [];
|
|
575
|
+
for (const entry of entries2) {
|
|
576
|
+
if (!entry.isDirectory()) continue;
|
|
577
|
+
const packagePath = resolve(packageRoot, entry.name);
|
|
578
|
+
let manifest;
|
|
579
|
+
try {
|
|
580
|
+
manifest = await readManifest(resolve(packagePath, "package.json"));
|
|
581
|
+
} catch {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (typeof manifest.name !== "string" || !manifest.name.startsWith("@forgeax/engine"))
|
|
585
|
+
continue;
|
|
586
|
+
if (typeof manifest.version !== "string") {
|
|
587
|
+
return bindingError(
|
|
588
|
+
"engine-package-version-missing",
|
|
589
|
+
"every local Engine package to declare a version",
|
|
590
|
+
"Restore the package manifest version before selecting a local Engine.",
|
|
591
|
+
{ package: manifest.name, root: packagePath }
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const target = packageEntry(manifest);
|
|
595
|
+
const entryPath = target === void 0 ? void 0 : resolve(packagePath, target);
|
|
596
|
+
let entryDigest = null;
|
|
597
|
+
let builtAt2 = null;
|
|
598
|
+
if (entryPath !== void 0 && await pathExists(entryPath)) {
|
|
599
|
+
const [bytes, metadata] = await Promise.all([readFile(entryPath), stat(entryPath)]);
|
|
600
|
+
entryDigest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
601
|
+
builtAt2 = metadata.mtime.toISOString();
|
|
602
|
+
}
|
|
603
|
+
packages.push({
|
|
604
|
+
name: manifest.name,
|
|
605
|
+
root: packagePath,
|
|
606
|
+
version: manifest.version,
|
|
607
|
+
entry: entryPath ?? null,
|
|
608
|
+
built: entryDigest !== null,
|
|
609
|
+
entryDigest,
|
|
610
|
+
builtAt: builtAt2
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
614
|
+
if (packages.length === 0) {
|
|
615
|
+
return bindingError(
|
|
616
|
+
"engine-workspace-empty",
|
|
617
|
+
"the local Engine workspace to contain @forgeax/engine packages",
|
|
618
|
+
"Pass the Engine repository or its source/engine SDK directory.",
|
|
619
|
+
{ root, packageRoot }
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
const versions = [...new Set(packages.map((item) => item.version))].sort();
|
|
623
|
+
const missingBuilds = packages.filter((item) => !item.built).map((item) => item.name);
|
|
624
|
+
const builtAt = packages.flatMap((item) => item.builtAt === null ? [] : [item.builtAt]).sort().at(-1) ?? null;
|
|
625
|
+
const digest = `sha256:${createHash("sha256").update(
|
|
626
|
+
packages.map((item) => `${item.name}\0${item.version}\0${item.entryDigest ?? "unbuilt"}`).join("\n")
|
|
627
|
+
).digest("hex")}`;
|
|
628
|
+
return {
|
|
629
|
+
ok: true,
|
|
630
|
+
value: {
|
|
631
|
+
root,
|
|
632
|
+
packageCount: packages.length,
|
|
633
|
+
builtPackages: packages.length - missingBuilds.length,
|
|
634
|
+
missingBuilds,
|
|
635
|
+
versions,
|
|
636
|
+
packages,
|
|
637
|
+
digest,
|
|
638
|
+
builtAt
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
} catch (cause) {
|
|
642
|
+
return bindingError(
|
|
643
|
+
isMissing(cause) ? "engine-workspace-missing" : "engine-workspace-unreadable",
|
|
644
|
+
"a readable Engine workspace with pnpm-workspace.yaml and packages/",
|
|
645
|
+
"Pass the Engine repository root, not its packages/ directory.",
|
|
646
|
+
{ root, packageRoot, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function projectDependencyMode(packageJson) {
|
|
651
|
+
let workspace = false;
|
|
652
|
+
let registry = false;
|
|
653
|
+
for (const section of [
|
|
654
|
+
"dependencies",
|
|
655
|
+
"devDependencies",
|
|
656
|
+
"optionalDependencies",
|
|
657
|
+
"peerDependencies"
|
|
658
|
+
]) {
|
|
659
|
+
const value = packageJson[section];
|
|
660
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) continue;
|
|
661
|
+
for (const version of Object.values(value)) {
|
|
662
|
+
if (typeof version !== "string") continue;
|
|
663
|
+
if (version.startsWith("workspace:") || version.startsWith("file:")) workspace = true;
|
|
664
|
+
else registry = true;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return workspace ? registry ? "mixed" : "workspace" : registry ? "registry" : "none";
|
|
668
|
+
}
|
|
669
|
+
async function sdkResolution(root) {
|
|
670
|
+
const packageJson = resolve(root, "node_modules", "@forgeax", "engine", "package.json");
|
|
671
|
+
try {
|
|
672
|
+
const manifest = await readManifest(packageJson);
|
|
673
|
+
const target = packageEntry(manifest);
|
|
674
|
+
const entry = target === void 0 ? null : resolve(dirname(packageJson), target);
|
|
675
|
+
return {
|
|
676
|
+
root: dirname(packageJson),
|
|
677
|
+
version: typeof manifest.version === "string" ? manifest.version : null,
|
|
678
|
+
entry,
|
|
679
|
+
built: entry !== null && await pathExists(entry),
|
|
680
|
+
source: "sdk"
|
|
681
|
+
};
|
|
682
|
+
} catch {
|
|
683
|
+
return { root: null, version: null, entry: null, built: false, source: "unresolved" };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
async function engineStatusCommand(options = {}) {
|
|
687
|
+
const facts = await readProjectFacts(options.root);
|
|
688
|
+
if (!facts.ok) return facts;
|
|
689
|
+
const bindingResult = await readEngineBinding(facts.value.root);
|
|
690
|
+
if (!bindingResult.ok) return bindingResult;
|
|
691
|
+
const binding = bindingResult.value;
|
|
692
|
+
const mode = binding === null ? "sdk" : "local";
|
|
693
|
+
const projectDependencies = projectDependencyMode(facts.value.packageJson);
|
|
694
|
+
if (binding !== null) {
|
|
695
|
+
const localPath = binding.path;
|
|
696
|
+
const localBinding = {
|
|
697
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
698
|
+
path: localPath
|
|
699
|
+
};
|
|
700
|
+
const workspaceResult = await inspectEngineWorkspace(localPath);
|
|
701
|
+
if (!workspaceResult.ok) {
|
|
702
|
+
const resolved3 = {
|
|
703
|
+
root: localPath,
|
|
704
|
+
version: null,
|
|
705
|
+
entry: null,
|
|
706
|
+
built: false,
|
|
707
|
+
source: "local"
|
|
708
|
+
};
|
|
709
|
+
return {
|
|
710
|
+
ok: true,
|
|
711
|
+
value: {
|
|
712
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
713
|
+
root: facts.value.root,
|
|
714
|
+
binding: localBinding,
|
|
715
|
+
mode,
|
|
716
|
+
resolved: resolved3,
|
|
717
|
+
workspace: null,
|
|
718
|
+
projectDependencies,
|
|
719
|
+
npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
|
|
720
|
+
healthy: false,
|
|
721
|
+
next: ["forgeax engine unlink", "forgeax engine use-local <engine-directory>"],
|
|
722
|
+
diagnostic: workspaceResult.error
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const workspace = workspaceResult.value;
|
|
727
|
+
const version = workspace.versions.length === 1 ? workspace.versions[0] ?? null : null;
|
|
728
|
+
const umbrella = workspace.packages.find((item) => item.name === "@forgeax/engine");
|
|
729
|
+
const resolved2 = {
|
|
730
|
+
root: localPath,
|
|
731
|
+
version,
|
|
732
|
+
entry: umbrella?.entry ?? null,
|
|
733
|
+
built: umbrella?.built ?? false,
|
|
734
|
+
source: "local"
|
|
735
|
+
};
|
|
736
|
+
const healthy = workspace.versions.length === 1 && workspace.missingBuilds.length === 0;
|
|
737
|
+
return {
|
|
738
|
+
ok: true,
|
|
739
|
+
value: {
|
|
740
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
741
|
+
root: facts.value.root,
|
|
742
|
+
binding: localBinding,
|
|
743
|
+
mode,
|
|
744
|
+
resolved: resolved2,
|
|
745
|
+
workspace,
|
|
746
|
+
projectDependencies,
|
|
747
|
+
npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
|
|
748
|
+
healthy,
|
|
749
|
+
next: healthy ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm build:engine", "forgeax engine doctor"]
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
const resolved = await sdkResolution(facts.value.root);
|
|
754
|
+
return {
|
|
755
|
+
ok: true,
|
|
756
|
+
value: {
|
|
757
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
758
|
+
root: facts.value.root,
|
|
759
|
+
binding,
|
|
760
|
+
mode,
|
|
761
|
+
resolved,
|
|
762
|
+
workspace: null,
|
|
763
|
+
projectDependencies,
|
|
764
|
+
npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
|
|
765
|
+
healthy: resolved.source === "sdk" && resolved.built,
|
|
766
|
+
next: resolved.source === "sdk" && resolved.built ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm install", "forgeax doctor"]
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
async function engineUseLocalCommand(options) {
|
|
771
|
+
const root = resolve(options.root ?? process.cwd());
|
|
772
|
+
if (options.path === void 0 || options.path.trim().length === 0) {
|
|
773
|
+
return bindingError(
|
|
774
|
+
"engine-binding-path-missing",
|
|
775
|
+
"forgeax engine use-local <engine-directory>",
|
|
776
|
+
"Pass the local Engine repository or SDK source directory."
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
let localPath;
|
|
780
|
+
try {
|
|
781
|
+
localPath = await realpath(resolve(root, options.path));
|
|
782
|
+
} catch (cause) {
|
|
783
|
+
return bindingError(
|
|
784
|
+
"engine-workspace-missing",
|
|
785
|
+
"the local Engine directory to exist",
|
|
786
|
+
"Pass an existing Engine repository or SDK source directory.",
|
|
787
|
+
{
|
|
788
|
+
path: resolve(root, options.path),
|
|
789
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
790
|
+
}
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
const workspace = await inspectEngineWorkspace(localPath);
|
|
794
|
+
if (!workspace.ok) return workspace;
|
|
795
|
+
const binding = {
|
|
796
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
797
|
+
path: localPath
|
|
798
|
+
};
|
|
799
|
+
if (options.dryRun !== true) await writeEngineBinding(root, binding);
|
|
800
|
+
const status = await engineStatusCommand({ root });
|
|
801
|
+
if (!status.ok) return status;
|
|
802
|
+
return {
|
|
803
|
+
ok: true,
|
|
804
|
+
value: {
|
|
805
|
+
...status.value,
|
|
806
|
+
next: workspace.value.missingBuilds.length === 0 ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm build:engine", "forgeax engine doctor"]
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
async function engineUnlinkCommand(options = {}) {
|
|
811
|
+
const root = resolve(options.root ?? process.cwd());
|
|
812
|
+
if (options.dryRun !== true) await rm(bindingFile(root), { force: true });
|
|
813
|
+
return engineStatusCommand({ root });
|
|
814
|
+
}
|
|
815
|
+
async function engineDoctorCommand(options = {}) {
|
|
816
|
+
const status = await engineStatusCommand(options);
|
|
817
|
+
if (!status.ok) return status;
|
|
818
|
+
if (status.value.projectDependencies === "workspace" || status.value.projectDependencies === "mixed") {
|
|
819
|
+
return {
|
|
820
|
+
ok: false,
|
|
821
|
+
error: {
|
|
822
|
+
code: "engine-project-workspace-dependency",
|
|
823
|
+
expected: "the game project to use registry or SDK-resolved dependencies for npm consumers",
|
|
824
|
+
hint: "This project is a pnpm workspace; use pnpm here, or create a clean SDK game project for npm install.",
|
|
825
|
+
detail: { root: status.value.root, projectDependencies: status.value.projectDependencies }
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
if (status.value.diagnostic !== void 0) return { ok: false, error: status.value.diagnostic };
|
|
830
|
+
if (!status.value.healthy) {
|
|
831
|
+
return {
|
|
832
|
+
ok: false,
|
|
833
|
+
error: {
|
|
834
|
+
code: status.value.mode === "local" ? "engine-local-build-missing" : "engine-sdk-unresolved",
|
|
835
|
+
expected: "the selected Engine binding to resolve to built packages",
|
|
836
|
+
hint: status.value.mode === "local" ? "Run pnpm build:engine in the selected Engine checkout, then rerun forgeax engine doctor." : "Run pnpm install in the game project or select a valid local Engine checkout.",
|
|
837
|
+
detail: { status: status.value }
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
return status;
|
|
842
|
+
}
|
|
843
|
+
var ENGINE_BINDING_SCHEMA_VERSION, bindingFile;
|
|
844
|
+
var init_engine_binding = __esm({
|
|
845
|
+
"src/engine-binding.ts"() {
|
|
846
|
+
init_project();
|
|
847
|
+
ENGINE_BINDING_SCHEMA_VERSION = "1.0.0";
|
|
848
|
+
bindingFile = (root) => resolve(root, ".forgeax", "engine-binding.json");
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// src/host.ts
|
|
853
|
+
var host_exports = {};
|
|
854
|
+
__export(host_exports, {
|
|
855
|
+
createEngineWorkspaceResolverForProject: () => createEngineWorkspaceResolverForProject,
|
|
856
|
+
createViteConfig: () => createViteConfig,
|
|
857
|
+
devKitDdcRoots: () => devKitDdcRoots,
|
|
858
|
+
ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
|
|
859
|
+
});
|
|
860
|
+
function ignoreDevKitCatalogPath(path) {
|
|
861
|
+
const normalized = path.replace(/\\/g, "/");
|
|
862
|
+
return normalized.split("/").includes("shaders");
|
|
863
|
+
}
|
|
864
|
+
function devKitDdcRoots(projectRoot2) {
|
|
865
|
+
return {
|
|
866
|
+
buildCacheRoot: resolve(projectRoot2, ".forgeax", "ddc", "build-cache"),
|
|
867
|
+
projectDdcRoot: resolve(projectRoot2, ".forgeax", "ddc", "v2")
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function isResourcePreviewIgnoredPath(path) {
|
|
871
|
+
return ignoreDevKitCatalogPath(path) || path.endsWith(".wgsl.meta.json") || path.endsWith("target-profile.json.meta.json");
|
|
872
|
+
}
|
|
873
|
+
function isProjectSourceIgnoredPath(path) {
|
|
874
|
+
return ignoreDevKitCatalogPath(path) || path.endsWith(".wgsl.meta.json");
|
|
875
|
+
}
|
|
876
|
+
async function prepareBuiltinPack(projectRoots, generated, ignorePath) {
|
|
877
|
+
const inventory = await scanInventory(projectRoots, { ignorePath });
|
|
878
|
+
if (!inventory.ok) return void 0;
|
|
879
|
+
const declared = /* @__PURE__ */ new Set();
|
|
880
|
+
for (const declaration of inventory.value.declarations.values()) {
|
|
881
|
+
if (declaration.format === "pack.json") {
|
|
882
|
+
for (const asset of declaration.value.assets) declared.add(asset.guid.toLowerCase());
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
for (const asset of declaration.value.subAssets) declared.add(asset.guid.toLowerCase());
|
|
886
|
+
}
|
|
887
|
+
const missing = BUILTIN_MESH_ASSETS.filter((asset) => !declared.has(asset.guid.toLowerCase()));
|
|
888
|
+
if (missing.length === 0) return void 0;
|
|
889
|
+
const packPath = resolve(generated, "engine-builtins.pack.json");
|
|
890
|
+
await writeFile(
|
|
891
|
+
packPath,
|
|
892
|
+
`${JSON.stringify(
|
|
893
|
+
{
|
|
894
|
+
schemaVersion: "2.0.0",
|
|
895
|
+
kind: "internal-text-package",
|
|
896
|
+
assets: missing.map((asset) => ({
|
|
897
|
+
guid: asset.guid,
|
|
898
|
+
kind: "mesh",
|
|
899
|
+
payload: { geometry: asset.geometry },
|
|
900
|
+
refs: [],
|
|
901
|
+
artifacts: {}
|
|
902
|
+
}))
|
|
903
|
+
},
|
|
904
|
+
null,
|
|
905
|
+
2
|
|
906
|
+
)}
|
|
907
|
+
`,
|
|
908
|
+
"utf8"
|
|
909
|
+
);
|
|
910
|
+
return packPath;
|
|
911
|
+
}
|
|
912
|
+
function findEngineWorkspaceRoot() {
|
|
913
|
+
let cursor = dirname(fileURLToPath(import.meta.url));
|
|
914
|
+
for (; ; ) {
|
|
915
|
+
if (existsSync(resolve(cursor, "pnpm-workspace.yaml"))) return cursor;
|
|
916
|
+
const parent = dirname(cursor);
|
|
917
|
+
if (parent === cursor) return void 0;
|
|
918
|
+
cursor = parent;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function engineWorkspacePackages(workspaceRoot = findEngineWorkspaceRoot()) {
|
|
922
|
+
if (workspaceRoot === void 0) return /* @__PURE__ */ new Map();
|
|
923
|
+
const packageRoot = resolve(workspaceRoot, "packages");
|
|
924
|
+
if (!existsSync(packageRoot)) return /* @__PURE__ */ new Map();
|
|
925
|
+
const packages = /* @__PURE__ */ new Map();
|
|
926
|
+
for (const entry of await readdir(packageRoot, { withFileTypes: true })) {
|
|
927
|
+
if (!entry.isDirectory()) continue;
|
|
928
|
+
const root = resolve(packageRoot, entry.name);
|
|
929
|
+
try {
|
|
930
|
+
const manifest = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
|
|
931
|
+
if (manifest === null || typeof manifest !== "object") continue;
|
|
932
|
+
const name = manifest.name;
|
|
933
|
+
if (typeof name === "string" && name.startsWith("@forgeax/engine")) {
|
|
934
|
+
packages.set(name, { root, manifest });
|
|
935
|
+
}
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return packages;
|
|
940
|
+
}
|
|
941
|
+
function conditionalExport2(value) {
|
|
942
|
+
if (typeof value === "string") return value;
|
|
943
|
+
if (value === null || Array.isArray(value)) return void 0;
|
|
944
|
+
for (const condition of ["browser", "import", "node", "default"]) {
|
|
945
|
+
const candidate = value[condition];
|
|
946
|
+
if (candidate === void 0) continue;
|
|
947
|
+
const selected = conditionalExport2(candidate);
|
|
948
|
+
if (selected !== void 0) return selected;
|
|
949
|
+
}
|
|
950
|
+
return void 0;
|
|
951
|
+
}
|
|
952
|
+
function packageExportTarget(manifest, subpath) {
|
|
953
|
+
const exportsValue = manifest.exports;
|
|
954
|
+
if (exportsValue === void 0) {
|
|
955
|
+
if (subpath.length > 0) return void 0;
|
|
956
|
+
const main = manifest.module ?? manifest.main;
|
|
957
|
+
return typeof main === "string" ? main : void 0;
|
|
958
|
+
}
|
|
959
|
+
if (typeof exportsValue === "string" || exportsValue === null) {
|
|
960
|
+
return subpath.length === 0 ? conditionalExport2(exportsValue) : void 0;
|
|
961
|
+
}
|
|
962
|
+
const keys = Object.keys(exportsValue);
|
|
963
|
+
const subpathMap = keys.some((key) => key === "." || key.startsWith("./"));
|
|
964
|
+
if (!subpathMap) return subpath.length === 0 ? conditionalExport2(exportsValue) : void 0;
|
|
965
|
+
const requested = subpath.length === 0 ? "." : `./${subpath}`;
|
|
966
|
+
const exact = exportsValue[requested];
|
|
967
|
+
if (exact !== void 0) return conditionalExport2(exact);
|
|
968
|
+
for (const key of keys) {
|
|
969
|
+
const marker = key.indexOf("*");
|
|
970
|
+
if (marker < 0) continue;
|
|
971
|
+
const prefix = key.slice(0, marker);
|
|
972
|
+
const suffix = key.slice(marker + 1);
|
|
432
973
|
if (!requested.startsWith(prefix) || !requested.endsWith(suffix)) continue;
|
|
433
974
|
const replacement = requested.slice(prefix.length, requested.length - suffix.length);
|
|
434
975
|
const exportTarget = exportsValue[key];
|
|
435
976
|
if (exportTarget === void 0) continue;
|
|
436
|
-
const selected =
|
|
977
|
+
const selected = conditionalExport2(exportTarget);
|
|
437
978
|
return selected?.replaceAll("*", replacement);
|
|
438
979
|
}
|
|
439
980
|
return void 0;
|
|
440
981
|
}
|
|
441
982
|
function engineWorkspaceImport(source, packages) {
|
|
442
|
-
if (!source.startsWith("@forgeax/engine
|
|
443
|
-
const separator = source.indexOf("/", "@forgeax/engine
|
|
983
|
+
if (!source.startsWith("@forgeax/engine")) return void 0;
|
|
984
|
+
const separator = source.indexOf("/", "@forgeax/engine".length);
|
|
444
985
|
const packageName = separator < 0 ? source : source.slice(0, separator);
|
|
445
986
|
const subpath = separator < 0 ? "" : source.slice(separator + 1);
|
|
446
987
|
const packageInfo = packages.get(packageName);
|
|
@@ -454,14 +995,25 @@ function engineWorkspaceImport(source, packages) {
|
|
|
454
995
|
}
|
|
455
996
|
return absolute;
|
|
456
997
|
}
|
|
457
|
-
async function createEngineWorkspaceResolver() {
|
|
458
|
-
const
|
|
998
|
+
async function createEngineWorkspaceResolver(projectRoot2) {
|
|
999
|
+
const binding = await readEngineBinding(projectRoot2);
|
|
1000
|
+
if (!binding.ok) {
|
|
1001
|
+
throw new Error(`${binding.error.code}: ${binding.error.hint}`);
|
|
1002
|
+
}
|
|
1003
|
+
const localRoot = binding.value?.path;
|
|
1004
|
+
if (localRoot !== void 0) {
|
|
1005
|
+
const inspected = await inspectEngineWorkspace(localRoot);
|
|
1006
|
+
if (!inspected.ok) throw new Error(`${inspected.error.code}: ${inspected.error.hint}`);
|
|
1007
|
+
}
|
|
1008
|
+
const packages = await engineWorkspacePackages(localRoot);
|
|
459
1009
|
if (packages.size === 0) return void 0;
|
|
460
1010
|
return {
|
|
461
1011
|
name: "forgeax:devkit-engine-workspace-resolver",
|
|
1012
|
+
enforce: "pre",
|
|
462
1013
|
async resolveId(source, importer) {
|
|
463
1014
|
const bareSource = source.split("?", 1)[0] ?? source;
|
|
464
|
-
if (!bareSource.startsWith("@forgeax/engine
|
|
1015
|
+
if (!bareSource.startsWith("@forgeax/engine")) return null;
|
|
1016
|
+
if (localRoot !== void 0) return engineWorkspaceImport(bareSource, packages);
|
|
465
1017
|
try {
|
|
466
1018
|
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
467
1019
|
if (resolved !== null) return resolved;
|
|
@@ -471,6 +1023,9 @@ async function createEngineWorkspaceResolver() {
|
|
|
471
1023
|
}
|
|
472
1024
|
};
|
|
473
1025
|
}
|
|
1026
|
+
async function createEngineWorkspaceResolverForProject(projectRoot2) {
|
|
1027
|
+
return createEngineWorkspaceResolver(projectRoot2);
|
|
1028
|
+
}
|
|
474
1029
|
async function consumerEngineAliases(projectRoot2) {
|
|
475
1030
|
const root = resolve(projectRoot2, "node_modules", ".pnpm", "node_modules", "@forgeax");
|
|
476
1031
|
try {
|
|
@@ -713,7 +1268,11 @@ function prepareAssetRegistry(assets) {
|
|
|
713
1268
|
const existing = assetPreparation.get(assets);
|
|
714
1269
|
if (existing !== undefined) return existing;
|
|
715
1270
|
const pending = (async () => {
|
|
716
|
-
|
|
1271
|
+
if (import.meta.env.DEV) {
|
|
1272
|
+
assets.configureRuntimeBinding(runtimeScopeBinding);
|
|
1273
|
+
} else {
|
|
1274
|
+
assets.configurePackIndex(new URL('pack-index.json', document.baseURI).href);
|
|
1275
|
+
}
|
|
717
1276
|
assets.setCatalogSource(assetCatalog);
|
|
718
1277
|
if (vfxRuntimeHost !== undefined) {
|
|
719
1278
|
assets.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
|
|
@@ -783,6 +1342,44 @@ async function previewOwnerFacts(assets, resource, payload) {
|
|
|
783
1342
|
return undefined;
|
|
784
1343
|
}
|
|
785
1344
|
|
|
1345
|
+
const gameProjectionDefinitions = new Map();
|
|
1346
|
+
function registerGameProjection(kind, definition) {
|
|
1347
|
+
if (gameProjectionDefinitions.has(definition.id)) {
|
|
1348
|
+
throw new Error('forgeax: duplicate game projection id ' + definition.id);
|
|
1349
|
+
}
|
|
1350
|
+
const entry = { kind, definition };
|
|
1351
|
+
gameProjectionDefinitions.set(definition.id, entry);
|
|
1352
|
+
return () => {
|
|
1353
|
+
if (gameProjectionDefinitions.get(definition.id) === entry) {
|
|
1354
|
+
gameProjectionDefinitions.delete(definition.id);
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
const gameProjection = {
|
|
1359
|
+
registerAction: (definition) => registerGameProjection('action', definition),
|
|
1360
|
+
registerRead: (definition) => registerGameProjection('read', definition),
|
|
1361
|
+
};
|
|
1362
|
+
function exposeGameInspection(app) {
|
|
1363
|
+
globalThis.__forgeaxGameInspection = {
|
|
1364
|
+
list() {
|
|
1365
|
+
return {
|
|
1366
|
+
reads: Array.from(gameProjectionDefinitions.entries())
|
|
1367
|
+
.filter(([, entry]) => entry.kind === 'read')
|
|
1368
|
+
.map(([id]) => id),
|
|
1369
|
+
};
|
|
1370
|
+
},
|
|
1371
|
+
async read(id) {
|
|
1372
|
+
const entry = gameProjectionDefinitions.get(id);
|
|
1373
|
+
if (entry?.kind !== 'read') throw new Error('forgeax: game read projection not found ' + id);
|
|
1374
|
+
return entry.definition.read();
|
|
1375
|
+
},
|
|
1376
|
+
renderer() {
|
|
1377
|
+
const inspection = app.renderer.inspect();
|
|
1378
|
+
return { state: inspection.state, frameId: inspection.frame.frameId };
|
|
1379
|
+
},
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
786
1383
|
async function prepareProject(app) {
|
|
787
1384
|
await prepareAssetRegistry(app.assets);
|
|
788
1385
|
previewWorld = app.world;
|
|
@@ -967,6 +1564,7 @@ await app.pluginContext.plugin(gameHostPlugin({
|
|
|
967
1564
|
...(defaultSceneRoot === undefined ? {} : { defaultSceneRoot }),
|
|
968
1565
|
uiRoot: uiRoot instanceof HTMLElement ? uiRoot : document.body,
|
|
969
1566
|
setPointerLockAllowed: (allowed) => app.input?.setPointerLockAllowed?.(allowed),
|
|
1567
|
+
...(import.meta.env.DEV ? { gameProjection } : {}),
|
|
970
1568
|
}));
|
|
971
1569
|
const { loader: pluginLoader } = await installCatalogLoader(app.pluginContext, pluginCatalog, 'engine');
|
|
972
1570
|
await pluginLoader.root.update(projectPluginEntries(pluginEntries, 'engine'));
|
|
@@ -1003,7 +1601,10 @@ if (query.has('forgeax-tool-replay')) {
|
|
|
1003
1601
|
if (current === undefined) return current;
|
|
1004
1602
|
if (resource?.kind !== 'vfx') {
|
|
1005
1603
|
if (resource?.kind === 'material' || resource?.kind === 'mesh' || resource?.kind === 'texture') {
|
|
1006
|
-
|
|
1604
|
+
const subjectDrawn = resource.kind === 'texture'
|
|
1605
|
+
? app.renderer.drawCalls > 0
|
|
1606
|
+
: app.renderer.drawCalls > 2;
|
|
1607
|
+
if (!subjectDrawn) return current;
|
|
1007
1608
|
if (resource.kind === 'mesh') {
|
|
1008
1609
|
const asset = current.asset;
|
|
1009
1610
|
const aabb = asset?.aabb;
|
|
@@ -1113,6 +1714,7 @@ if (query.has('forgeax-tool-replay')) {
|
|
|
1113
1714
|
);
|
|
1114
1715
|
if (!result.ok) throw result.error;
|
|
1115
1716
|
const app = result.value;
|
|
1717
|
+
if (import.meta.env.DEV) exposeGameInspection(app);
|
|
1116
1718
|
await prepareProject(app);
|
|
1117
1719
|
${bootstrapPlugin}
|
|
1118
1720
|
app.start().unwrap();
|
|
@@ -1166,32 +1768,62 @@ function htmlSource(title) {
|
|
|
1166
1768
|
<div id="app-shell"><canvas id="app"></canvas><div id="game-ui"></div></div><div id="forgeax-fatal" role="alert"></div>
|
|
1167
1769
|
<script>
|
|
1168
1770
|
(() => {
|
|
1771
|
+
const appendStructuredFailure = (value, prefix, depth, seen, lines) => {
|
|
1772
|
+
if (value === null || typeof value !== 'object' || depth > 3) return;
|
|
1773
|
+
if (seen.has(value)) {
|
|
1774
|
+
lines.push((prefix || 'cause') + ': [circular]');
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
seen.add(value);
|
|
1778
|
+
const record = value;
|
|
1779
|
+
const name = typeof record.name === 'string' && record.name.length > 0
|
|
1780
|
+
? record.name
|
|
1781
|
+
: undefined;
|
|
1782
|
+
const code = typeof record.code === 'string' && record.code.length > 0
|
|
1783
|
+
? record.code
|
|
1784
|
+
: undefined;
|
|
1785
|
+
const message = typeof record.message === 'string' && record.message.length > 0
|
|
1786
|
+
? record.message
|
|
1787
|
+
: undefined;
|
|
1788
|
+
if (name !== undefined || code !== undefined || message !== undefined) {
|
|
1789
|
+
const identity = [name || 'Error', code].filter(Boolean).join(' ');
|
|
1790
|
+
lines.push((prefix ? prefix + ': ' : '') + identity + (message ? ': ' + message : ''));
|
|
1791
|
+
}
|
|
1792
|
+
for (const key of ['expected', 'hint', 'reason']) {
|
|
1793
|
+
if (typeof record[key] === 'string' && record[key].length > 0) {
|
|
1794
|
+
lines.push((prefix ? prefix + '.' : '') + key + ': ' + record[key]);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
for (const key of ['cause', 'detail', 'webgpuError', 'wgpuError', 'error']) {
|
|
1798
|
+
const nested = record[key];
|
|
1799
|
+
const nestedPrefix = (prefix ? prefix + '.' : '') + key;
|
|
1800
|
+
if (nested !== null && typeof nested === 'object') {
|
|
1801
|
+
appendStructuredFailure(nested, nestedPrefix, depth + 1, seen, lines);
|
|
1802
|
+
} else if (typeof nested === 'string' && nested.length > 0) {
|
|
1803
|
+
lines.push(nestedPrefix + ': ' + nested);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1169
1807
|
const formatStartupFailure = (reason) => {
|
|
1170
|
-
if (reason instanceof Error) return reason.message;
|
|
1171
1808
|
if (reason !== null && typeof reason === 'object') {
|
|
1172
|
-
const record = reason;
|
|
1173
1809
|
const lines = [];
|
|
1174
|
-
|
|
1175
|
-
if (typeof record[key] === 'string' && record[key].length > 0) {
|
|
1176
|
-
lines.push(key + ': ' + record[key]);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
const detail = record.detail;
|
|
1180
|
-
if (detail !== null && typeof detail === 'object') {
|
|
1181
|
-
for (const key of ['reason', 'guid']) {
|
|
1182
|
-
if (typeof detail[key] === 'string' && detail[key].length > 0) {
|
|
1183
|
-
lines.push('detail.' + key + ': ' + detail[key]);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1810
|
+
appendStructuredFailure(reason, '', 0, new Set(), lines);
|
|
1187
1811
|
if (lines.length > 0) return lines.join('\\n');
|
|
1812
|
+
try {
|
|
1813
|
+
return JSON.stringify(reason) || 'Unknown structured startup failure';
|
|
1814
|
+
} catch {
|
|
1815
|
+
return 'Unserializable structured startup failure';
|
|
1816
|
+
}
|
|
1188
1817
|
}
|
|
1189
1818
|
return String(reason ?? 'Unknown startup failure');
|
|
1190
1819
|
};
|
|
1191
1820
|
const show = (reason) => {
|
|
1192
1821
|
const notice = document.querySelector('#forgeax-fatal');
|
|
1193
1822
|
if (!(notice instanceof HTMLElement)) return;
|
|
1194
|
-
|
|
1823
|
+
let message = formatStartupFailure(reason);
|
|
1824
|
+
if (/webgpu|adapter-unavailable|no usable (rendering )?backend/i.test(message)) {
|
|
1825
|
+
message += '\\n\\nRenderer diagnosis: ForgeaX supports browser WebGPU and a wgpu/WebGL2 fallback. This failure alone does not prove that WebGPU is unsupported; use the structured code, hint, and nested backend causes above.';
|
|
1826
|
+
}
|
|
1195
1827
|
notice.textContent = 'ForgeaX game failed to start.\\n' + message;
|
|
1196
1828
|
notice.style.display = 'grid';
|
|
1197
1829
|
};
|
|
@@ -1223,10 +1855,11 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
|
|
|
1223
1855
|
];
|
|
1224
1856
|
const importers = [...projectImporters(facts)];
|
|
1225
1857
|
const runtimeBinding = createStandaloneRuntimeAssetBinding(facts.id);
|
|
1226
|
-
const engineWorkspaceResolver = await createEngineWorkspaceResolver();
|
|
1858
|
+
const engineWorkspaceResolver = await createEngineWorkspaceResolver(facts.root);
|
|
1227
1859
|
const consumerAliases = await consumerEngineAliases(facts.root);
|
|
1228
1860
|
const plugins = [
|
|
1229
1861
|
...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
|
|
1862
|
+
...command2 === "serve" && process.env.FORGEAX_ENGINE_RHI_DEBUG === "1" ? [vitePluginRhiDebug({ rootDir: facts.root })] : [],
|
|
1230
1863
|
forgeaxShader(),
|
|
1231
1864
|
pluginPack({
|
|
1232
1865
|
roots,
|
|
@@ -1264,152 +1897,742 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
|
|
|
1264
1897
|
}
|
|
1265
1898
|
};
|
|
1266
1899
|
}
|
|
1267
|
-
var hostRequire
|
|
1900
|
+
var hostRequire;
|
|
1268
1901
|
var init_host = __esm({
|
|
1269
1902
|
"src/host.ts"() {
|
|
1903
|
+
init_engine_binding();
|
|
1270
1904
|
hostRequire = createRequire(import.meta.url);
|
|
1271
1905
|
}
|
|
1272
1906
|
});
|
|
1273
|
-
function
|
|
1274
|
-
return { ok: false, error:
|
|
1907
|
+
function errorResult(code, expected, hint, detail = {}) {
|
|
1908
|
+
return { ok: false, error: new SingleHtmlError(code, expected, hint, detail) };
|
|
1275
1909
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1910
|
+
function toErrorResult(cause, code, expected, hint) {
|
|
1911
|
+
if (cause instanceof SingleHtmlError) return { ok: false, error: cause };
|
|
1912
|
+
return errorResult(code, expected, hint, {
|
|
1913
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
1914
|
+
});
|
|
1278
1915
|
}
|
|
1279
|
-
function
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
entry.config,
|
|
1286
|
-
realm
|
|
1287
|
-
);
|
|
1288
|
-
if (unsupported !== void 0) return unsupported;
|
|
1289
|
-
}
|
|
1916
|
+
function normalizePath(value) {
|
|
1917
|
+
let decoded;
|
|
1918
|
+
try {
|
|
1919
|
+
decoded = decodeURIComponent(value.split(/[?#]/, 1)[0] ?? value);
|
|
1920
|
+
} catch {
|
|
1921
|
+
return void 0;
|
|
1290
1922
|
}
|
|
1291
|
-
|
|
1923
|
+
const normalized = decoded.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
1924
|
+
const segments = normalized.split("/");
|
|
1925
|
+
if (normalized.length === 0 || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
1926
|
+
return void 0;
|
|
1927
|
+
}
|
|
1928
|
+
return normalized;
|
|
1292
1929
|
}
|
|
1293
|
-
function
|
|
1294
|
-
return
|
|
1295
|
-
|
|
1930
|
+
function safeScriptText(value) {
|
|
1931
|
+
return value.replace(/<\/script/gi, "<\\/script");
|
|
1932
|
+
}
|
|
1933
|
+
function hashBytes(bytes) {
|
|
1934
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
1935
|
+
}
|
|
1936
|
+
function contentTypeForDataUri(value) {
|
|
1937
|
+
const semicolon = value.indexOf(";");
|
|
1938
|
+
return semicolon < 0 ? value : value.slice(0, semicolon);
|
|
1939
|
+
}
|
|
1940
|
+
function dataUri(bytes, type) {
|
|
1941
|
+
return `data:${contentTypeForDataUri(type)};base64,${Buffer.from(bytes).toString("base64")}`;
|
|
1942
|
+
}
|
|
1943
|
+
function attributeValueRange(source, location) {
|
|
1944
|
+
const raw = source.slice(location.startOffset, location.endOffset);
|
|
1945
|
+
const equals = raw.indexOf("=");
|
|
1946
|
+
if (equals < 0) return void 0;
|
|
1947
|
+
let cursor = equals + 1;
|
|
1948
|
+
while (/\s/.test(raw[cursor] ?? "")) cursor += 1;
|
|
1949
|
+
const quote = raw[cursor] === '"' || raw[cursor] === "'" ? raw[cursor] : void 0;
|
|
1950
|
+
if (quote !== void 0) {
|
|
1951
|
+
const start = cursor + 1;
|
|
1952
|
+
const end = raw.lastIndexOf(quote);
|
|
1953
|
+
return end <= start ? void 0 : { startOffset: location.startOffset + start, endOffset: location.startOffset + end };
|
|
1954
|
+
}
|
|
1955
|
+
return {
|
|
1956
|
+
startOffset: location.startOffset + cursor,
|
|
1957
|
+
endOffset: location.endOffset
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
function scanHtmlElements(source) {
|
|
1961
|
+
const document2 = parse(source, { sourceCodeLocationInfo: true });
|
|
1962
|
+
const elements = [];
|
|
1963
|
+
const visit = (node) => {
|
|
1964
|
+
if (node.nodeName === "#document" || node.nodeName === "#document-fragment") {
|
|
1965
|
+
for (const child of node.childNodes) visit(child);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
if (node.nodeName === "#text" || node.nodeName === "#comment" || node.nodeName === "#documentType") {
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
const element = node;
|
|
1972
|
+
const location = element.sourceCodeLocation;
|
|
1973
|
+
if (location?.startTag === void 0) {
|
|
1974
|
+
for (const child of element.childNodes) visit(child);
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
const name = element.tagName.toLowerCase();
|
|
1978
|
+
const attributes = element.attrs.map(({ name: attributeName, value }) => ({
|
|
1979
|
+
name: attributeName.toLowerCase(),
|
|
1980
|
+
value
|
|
1981
|
+
}));
|
|
1982
|
+
const attributeLocations = {};
|
|
1983
|
+
for (const attribute2 of attributes) {
|
|
1984
|
+
const attributeLocation = location.attrs?.[attribute2.name];
|
|
1985
|
+
if (attributeLocation !== void 0) attributeLocations[attribute2.name] = attributeLocation;
|
|
1986
|
+
}
|
|
1987
|
+
elements.push({
|
|
1988
|
+
name,
|
|
1989
|
+
start: location.startTag.startOffset,
|
|
1990
|
+
openEnd: location.startTag.endOffset,
|
|
1991
|
+
end: location.endTag?.endOffset ?? location.startTag.endOffset,
|
|
1992
|
+
...name === "script" || name === "style" ? {
|
|
1993
|
+
contentStart: location.startTag.endOffset,
|
|
1994
|
+
contentEnd: location.endTag?.startOffset ?? location.startTag.endOffset
|
|
1995
|
+
} : {},
|
|
1996
|
+
attributes,
|
|
1997
|
+
attributeLocations
|
|
1998
|
+
});
|
|
1999
|
+
for (const child of element.childNodes) visit(child);
|
|
2000
|
+
};
|
|
2001
|
+
visit(document2);
|
|
2002
|
+
return elements;
|
|
2003
|
+
}
|
|
2004
|
+
function attribute(element, name) {
|
|
2005
|
+
return element.attributes.find((candidate) => candidate.name === name);
|
|
2006
|
+
}
|
|
2007
|
+
function attributeValue(element, name) {
|
|
2008
|
+
return attribute(element, name)?.value;
|
|
2009
|
+
}
|
|
2010
|
+
function isModuleScript(element) {
|
|
2011
|
+
return element.name === "script" && attributeValue(element, "type")?.toLowerCase() === "module";
|
|
2012
|
+
}
|
|
2013
|
+
function isStylesheet(element) {
|
|
2014
|
+
return element.name === "link" && attributeValue(element, "rel")?.toLowerCase().split(/\s+/).includes("stylesheet") === true;
|
|
2015
|
+
}
|
|
2016
|
+
function isModulePreload(element) {
|
|
2017
|
+
return element.name === "link" && attributeValue(element, "rel")?.toLowerCase().split(/\s+/).includes("modulepreload") === true;
|
|
2018
|
+
}
|
|
2019
|
+
function resourceForPath(path, resources) {
|
|
2020
|
+
const normalized = normalizePath(path);
|
|
2021
|
+
if (normalized === void 0) return void 0;
|
|
2022
|
+
const direct = resources.get(normalized);
|
|
2023
|
+
if (direct !== void 0) return direct;
|
|
2024
|
+
const suffix = [...resources.entries()].filter(
|
|
2025
|
+
([candidate]) => candidate.endsWith(`/${normalized}`)
|
|
1296
2026
|
);
|
|
2027
|
+
return suffix.length === 1 ? suffix[0]?.[1] : void 0;
|
|
1297
2028
|
}
|
|
1298
|
-
|
|
1299
|
-
const root = resolve(rootInput);
|
|
1300
|
-
let forgeValue;
|
|
1301
|
-
let packageValue;
|
|
2029
|
+
function resourcePath(reference, basePath) {
|
|
1302
2030
|
try {
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
]);
|
|
1307
|
-
} catch (cause) {
|
|
1308
|
-
return projectError(
|
|
1309
|
-
"project-manifest-unreadable",
|
|
1310
|
-
"readable forge.json and package.json files",
|
|
1311
|
-
"Run the command from a ForgeaX game root or pass its directory.",
|
|
1312
|
-
{ root, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
1313
|
-
);
|
|
2031
|
+
return normalizePath(new URL(reference, `https://forgeax.invalid/${basePath}`).pathname);
|
|
2032
|
+
} catch {
|
|
2033
|
+
return void 0;
|
|
1314
2034
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
{ root }
|
|
1321
|
-
);
|
|
2035
|
+
}
|
|
2036
|
+
function applyReplacements(source, replacements) {
|
|
2037
|
+
const unique = /* @__PURE__ */ new Map();
|
|
2038
|
+
for (const replacement of replacements) {
|
|
2039
|
+
unique.set(`${replacement.start}:${replacement.end}`, replacement);
|
|
1322
2040
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
2041
|
+
const ordered = [...unique.values()].sort((left, right) => left.start - right.start);
|
|
2042
|
+
for (let index = 1; index < ordered.length; index += 1) {
|
|
2043
|
+
const previous = ordered[index - 1];
|
|
2044
|
+
const current = ordered[index];
|
|
2045
|
+
if (previous !== void 0 && current !== void 0 && previous.end > current.start) {
|
|
2046
|
+
throw new Error(
|
|
2047
|
+
`single-html AST replacements overlap (${previous.start}:${previous.end} and ${current.start}:${current.end})`
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
1330
2050
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
2051
|
+
return ordered.reverse().reduce(
|
|
2052
|
+
(value, replacement) => value.slice(0, replacement.start) + replacement.value + value.slice(replacement.end),
|
|
2053
|
+
source
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
function inlineCss(css, cssPath, resources) {
|
|
2057
|
+
const replacements = [];
|
|
2058
|
+
const pattern = /url\(\s*(["']?)(.*?)\1\s*\)/gi;
|
|
2059
|
+
for (; ; ) {
|
|
2060
|
+
const match = pattern.exec(css);
|
|
2061
|
+
if (match === null) break;
|
|
2062
|
+
const reference = match[2]?.trim();
|
|
2063
|
+
if (reference === void 0 || reference.length === 0 || reference.startsWith("data:") || reference.startsWith("#")) {
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
if (/^(?:https?:|blob:)/i.test(reference)) {
|
|
2067
|
+
return errorResult(
|
|
2068
|
+
"single-html-css-external-resource",
|
|
2069
|
+
"CSS URL references to be embedded or data/blob URLs",
|
|
2070
|
+
"Move the CSS resource into the verified dist closure before packaging.",
|
|
2071
|
+
{ cssPath, reference }
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
const path = resourcePath(reference, cssPath);
|
|
2075
|
+
const resource = path === void 0 ? void 0 : resourceForPath(path, resources);
|
|
2076
|
+
if (resource === void 0) {
|
|
2077
|
+
return errorResult(
|
|
2078
|
+
"single-html-css-asset-missing",
|
|
2079
|
+
"every local CSS URL to resolve to an embedded dist artifact",
|
|
2080
|
+
"Add the referenced asset to the project asset closure and rebuild.",
|
|
2081
|
+
{ cssPath, reference, path: path ?? null }
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
replacements.push({
|
|
2085
|
+
start: match.index,
|
|
2086
|
+
end: match.index + match[0].length,
|
|
2087
|
+
value: `url(${dataUri(resource.bytes, resource.mediaType)})`
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
return { ok: true, value: applyReplacements(css, replacements) };
|
|
2091
|
+
}
|
|
2092
|
+
function collectEntrySource(html) {
|
|
2093
|
+
const entries2 = scanHtmlElements(html).filter((element2) => isModuleScript(element2));
|
|
2094
|
+
const external = entries2.filter((element2) => attributeValue(element2, "src") !== void 0);
|
|
2095
|
+
const inline = entries2.filter((element2) => attributeValue(element2, "src") === void 0);
|
|
2096
|
+
if (external.length === 0 && inline.length === 0) {
|
|
2097
|
+
return errorResult(
|
|
2098
|
+
"single-html-entry-missing",
|
|
2099
|
+
"production index.html to contain one module entry",
|
|
2100
|
+
"Rebuild the game with a module entry in its generated host."
|
|
1338
2101
|
);
|
|
1339
2102
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
"
|
|
1344
|
-
"
|
|
1345
|
-
|
|
1346
|
-
{ root }
|
|
2103
|
+
if (external.length > 1 || inline.length > 1) {
|
|
2104
|
+
return errorResult(
|
|
2105
|
+
"single-html-entry-ambiguous",
|
|
2106
|
+
"production index.html to contain exactly one module entry",
|
|
2107
|
+
"Converge the generated host to one ForgeaX module entry before packaging.",
|
|
2108
|
+
{ external: external.length, inline: inline.length }
|
|
1347
2109
|
);
|
|
1348
2110
|
}
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
2111
|
+
const element = external[0] ?? inline[0];
|
|
2112
|
+
if (element === void 0) throw new Error("single-html entry selection was unexpectedly empty");
|
|
2113
|
+
const src = attributeValue(element, "src");
|
|
2114
|
+
if (src === void 0 && element.contentStart !== void 0 && element.contentEnd !== void 0) {
|
|
2115
|
+
return { ok: true, value: html.slice(element.contentStart, element.contentEnd) };
|
|
2116
|
+
}
|
|
2117
|
+
if (src === void 0) {
|
|
2118
|
+
return errorResult(
|
|
2119
|
+
"single-html-entry-missing",
|
|
2120
|
+
"a module entry source",
|
|
2121
|
+
"Add a module entry to index.html."
|
|
1357
2122
|
);
|
|
1358
2123
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
"forge.json#entry to resolve to a readable module",
|
|
1366
|
-
"Restore the game entry or update forge.json#entry.",
|
|
1367
|
-
{ root, entry: forge.entry }
|
|
2124
|
+
if (/^(?:https?:|file:|data:|blob:)/i.test(src)) {
|
|
2125
|
+
return errorResult(
|
|
2126
|
+
"single-html-entry-external",
|
|
2127
|
+
"the module entry to be a project-local dist artifact",
|
|
2128
|
+
"Rebuild the game so the generated host points at a local production module.",
|
|
2129
|
+
{ src }
|
|
1368
2130
|
);
|
|
1369
2131
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
const
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
2132
|
+
return { ok: true, value: src };
|
|
2133
|
+
}
|
|
2134
|
+
async function filesUnder2(root, directory = root) {
|
|
2135
|
+
const result = [];
|
|
2136
|
+
for (const name of (await readdir(directory)).sort()) {
|
|
2137
|
+
const path = resolve(directory, name);
|
|
2138
|
+
const info = await stat(path);
|
|
2139
|
+
if (info.isDirectory()) result.push(...await filesUnder2(root, path));
|
|
2140
|
+
else if (info.isFile()) result.push(path);
|
|
2141
|
+
}
|
|
2142
|
+
return result;
|
|
2143
|
+
}
|
|
2144
|
+
function isPreloadHelperSource(value) {
|
|
2145
|
+
if (typeof value !== "string") return false;
|
|
2146
|
+
const path = value.split(/[?#]/, 1)[0] ?? value;
|
|
2147
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
2148
|
+
return name.startsWith("preload-helper-") && name.endsWith(".js");
|
|
2149
|
+
}
|
|
2150
|
+
function isStaticImportSource(source) {
|
|
2151
|
+
return source.type === "Literal" || source.type === "StringLiteral" || source.type === "TemplateLiteral" && source.expressions?.length === 0;
|
|
2152
|
+
}
|
|
2153
|
+
function programReplacements(program) {
|
|
2154
|
+
const replacements = [];
|
|
2155
|
+
const preloadBindings = /* @__PURE__ */ new Set();
|
|
2156
|
+
new Visitor({
|
|
2157
|
+
ImportDeclaration(node) {
|
|
2158
|
+
if (!isPreloadHelperSource(node.source.value)) return;
|
|
2159
|
+
for (const specifier of node.specifiers) {
|
|
2160
|
+
if (specifier.type === "ImportSpecifier") preloadBindings.add(specifier.local.name);
|
|
2161
|
+
}
|
|
2162
|
+
},
|
|
2163
|
+
CallExpression(node) {
|
|
2164
|
+
if (node.callee.type !== "Identifier" || !preloadBindings.has(node.callee.name) || node.arguments.length < 2) {
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
const dependencies = node.arguments[1];
|
|
2168
|
+
if (dependencies !== void 0) {
|
|
2169
|
+
replacements.push({ start: dependencies.start, end: dependencies.end, value: "void 0" });
|
|
2170
|
+
}
|
|
2171
|
+
},
|
|
2172
|
+
ImportExpression(node) {
|
|
2173
|
+
if (isStaticImportSource(node.source)) return;
|
|
2174
|
+
replacements.push({
|
|
2175
|
+
start: node.start,
|
|
2176
|
+
end: node.start + "import".length,
|
|
2177
|
+
value: "globalThis.__forgeaxImport"
|
|
2178
|
+
});
|
|
1397
2179
|
}
|
|
2180
|
+
}).visit(program);
|
|
2181
|
+
return replacements;
|
|
2182
|
+
}
|
|
2183
|
+
function outputReplacements(program) {
|
|
2184
|
+
const replacements = [];
|
|
2185
|
+
new Visitor({
|
|
2186
|
+
ImportExpression(node) {
|
|
2187
|
+
replacements.push({
|
|
2188
|
+
start: node.start,
|
|
2189
|
+
end: node.start + "import".length,
|
|
2190
|
+
value: "globalThis.__forgeaxImport"
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
}).visit(program);
|
|
2194
|
+
return replacements;
|
|
2195
|
+
}
|
|
2196
|
+
function rewriteOutputJavaScript(code) {
|
|
2197
|
+
const program = parseAst(code);
|
|
2198
|
+
return applyReplacements(code, outputReplacements(program));
|
|
2199
|
+
}
|
|
2200
|
+
function residualImportStart(code) {
|
|
2201
|
+
let residualStart;
|
|
2202
|
+
new Visitor({
|
|
2203
|
+
ImportExpression(node) {
|
|
2204
|
+
residualStart ??= node.start;
|
|
2205
|
+
}
|
|
2206
|
+
}).visit(parseAst(code));
|
|
2207
|
+
return residualStart;
|
|
2208
|
+
}
|
|
2209
|
+
function isJavaScriptOutputAsset(fileName) {
|
|
2210
|
+
return /\.(?:c?js|mjs)$/i.test(fileName);
|
|
2211
|
+
}
|
|
2212
|
+
function singleHtmlBundlePlugin() {
|
|
2213
|
+
const plugin = {
|
|
2214
|
+
name: "forgeax-single-html-bundle-runtime",
|
|
2215
|
+
enforce: "post",
|
|
2216
|
+
transform: {
|
|
2217
|
+
order: "post",
|
|
2218
|
+
handler(code, _id, meta) {
|
|
2219
|
+
let program = meta?.ast;
|
|
2220
|
+
if (program === void 0) {
|
|
2221
|
+
try {
|
|
2222
|
+
program = parseAst(code);
|
|
2223
|
+
} catch {
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
const replacements = programReplacements(program);
|
|
2228
|
+
if (replacements.length === 0) return null;
|
|
2229
|
+
if (meta?.magicString !== void 0) {
|
|
2230
|
+
for (const replacement of replacements) {
|
|
2231
|
+
meta.magicString.overwrite(replacement.start, replacement.end, replacement.value);
|
|
2232
|
+
}
|
|
2233
|
+
return meta.magicString.hasChanged() ? meta.magicString : null;
|
|
2234
|
+
}
|
|
2235
|
+
return applyReplacements(code, replacements);
|
|
2236
|
+
}
|
|
2237
|
+
},
|
|
2238
|
+
generateBundle(_outputOptions, bundle) {
|
|
2239
|
+
for (const output of Object.values(bundle)) {
|
|
2240
|
+
if (output.type === "chunk") {
|
|
2241
|
+
output.code = rewriteOutputJavaScript(output.code);
|
|
2242
|
+
} else if (isJavaScriptOutputAsset(output.fileName) && output.source !== void 0) {
|
|
2243
|
+
const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString("utf8");
|
|
2244
|
+
output.source = rewriteOutputJavaScript(source);
|
|
2245
|
+
} else {
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
const code = output.type === "chunk" ? output.code : String(output.source);
|
|
2249
|
+
const residualStart = residualImportStart(code);
|
|
2250
|
+
if (residualStart !== void 0) {
|
|
2251
|
+
this.error(
|
|
2252
|
+
`single-html bundle left a native dynamic import in ${output.fileName} at ${residualStart}`
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
},
|
|
2257
|
+
resolveFileUrl: ({ fileName }) => JSON.stringify(`${GENERATED_PREFIX}${fileName}`)
|
|
1398
2258
|
};
|
|
2259
|
+
return plugin;
|
|
2260
|
+
}
|
|
2261
|
+
async function bundleSingleHtmlEntry(distRootInput, indexHtml, projectRoot2) {
|
|
2262
|
+
const distRoot = resolve(distRootInput);
|
|
2263
|
+
const selected = collectEntrySource(indexHtml);
|
|
2264
|
+
if (!selected.ok) return selected;
|
|
2265
|
+
const source = selected.value;
|
|
2266
|
+
const entryPath = normalizePath(source);
|
|
2267
|
+
const temporaryRoot = await mkdtemp(resolve(tmpdir(), "forgeax-single-html-bundle-"));
|
|
2268
|
+
try {
|
|
2269
|
+
let inputPath;
|
|
2270
|
+
if (entryPath !== void 0) {
|
|
2271
|
+
const candidate = resolve(distRoot, entryPath);
|
|
2272
|
+
try {
|
|
2273
|
+
const candidateInfo = await stat(candidate);
|
|
2274
|
+
if (candidateInfo.isFile()) inputPath = candidate;
|
|
2275
|
+
} catch {
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
if (inputPath === void 0) {
|
|
2279
|
+
inputPath = resolve(temporaryRoot, "inline-entry.mjs");
|
|
2280
|
+
await writeFile(inputPath, source, "utf8");
|
|
2281
|
+
}
|
|
2282
|
+
const outputRoot = resolve(temporaryRoot, "dist");
|
|
2283
|
+
const resolver = projectRoot2 === void 0 ? void 0 : await createEngineWorkspaceResolverForProject(projectRoot2);
|
|
2284
|
+
await build({
|
|
2285
|
+
configFile: false,
|
|
2286
|
+
root: distRoot,
|
|
2287
|
+
base: "./",
|
|
2288
|
+
logLevel: "error",
|
|
2289
|
+
plugins: [singleHtmlBundlePlugin(), ...resolver === void 0 ? [] : [resolver]],
|
|
2290
|
+
experimental: {
|
|
2291
|
+
renderBuiltUrl: (filename) => `forgeax-resource:///${GENERATED_PREFIX}${filename}`
|
|
2292
|
+
},
|
|
2293
|
+
build: {
|
|
2294
|
+
outDir: outputRoot,
|
|
2295
|
+
emptyOutDir: true,
|
|
2296
|
+
target: "esnext",
|
|
2297
|
+
minify: false,
|
|
2298
|
+
sourcemap: false,
|
|
2299
|
+
assetsInlineLimit: 0,
|
|
2300
|
+
modulePreload: false,
|
|
2301
|
+
rolldownOptions: {
|
|
2302
|
+
input: inputPath,
|
|
2303
|
+
output: {
|
|
2304
|
+
codeSplitting: false,
|
|
2305
|
+
entryFileNames: "entry.mjs",
|
|
2306
|
+
chunkFileNames: "assets/[name]-[hash].mjs",
|
|
2307
|
+
assetFileNames: "assets/[name]-[hash][extname]"
|
|
2308
|
+
},
|
|
2309
|
+
experimental: { nativeMagicString: true }
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
});
|
|
2313
|
+
const paths = (await filesUnder2(outputRoot)).map(
|
|
2314
|
+
(path) => relative(outputRoot, path).split(sep).join("/")
|
|
2315
|
+
);
|
|
2316
|
+
const entry = paths.find((path) => path === "entry.mjs");
|
|
2317
|
+
if (entry === void 0) {
|
|
2318
|
+
return errorResult(
|
|
2319
|
+
"single-html-bundle-entry-missing",
|
|
2320
|
+
"Vite to emit exactly one entry.mjs",
|
|
2321
|
+
"Inspect the production entry and the single-html bundler output.",
|
|
2322
|
+
{ outputRoot, paths }
|
|
2323
|
+
);
|
|
2324
|
+
}
|
|
2325
|
+
const generatedPaths = paths.filter((path) => path !== entry);
|
|
2326
|
+
const artifacts = [];
|
|
2327
|
+
for (const path of generatedPaths) {
|
|
2328
|
+
const bytes = await readFile(resolve(outputRoot, path));
|
|
2329
|
+
artifacts.push({
|
|
2330
|
+
path: `${GENERATED_PREFIX}${path}`,
|
|
2331
|
+
bytes,
|
|
2332
|
+
mediaType: mediaType(path)
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
const entryBytes = await readFile(resolve(outputRoot, entry));
|
|
2336
|
+
return {
|
|
2337
|
+
ok: true,
|
|
2338
|
+
value: {
|
|
2339
|
+
entrySource: entryBytes.toString("utf8"),
|
|
2340
|
+
artifacts
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
} catch (cause) {
|
|
2344
|
+
return toErrorResult(
|
|
2345
|
+
cause,
|
|
2346
|
+
"single-html-bundle-failed",
|
|
2347
|
+
"the production module entry to converge into one executable bundle",
|
|
2348
|
+
"Repair the production entry or inspect the Vite/Rolldown diagnostic before packaging."
|
|
2349
|
+
);
|
|
2350
|
+
} finally {
|
|
2351
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
2352
|
+
}
|
|
1399
2353
|
}
|
|
1400
|
-
function
|
|
1401
|
-
|
|
1402
|
-
|
|
2354
|
+
function workerPrelude(resources) {
|
|
2355
|
+
const payload = resources.map((resource) => ({
|
|
2356
|
+
path: resource.path,
|
|
2357
|
+
mime: resource.mediaType,
|
|
2358
|
+
data: Buffer.from(resource.bytes).toString("base64")
|
|
2359
|
+
}));
|
|
2360
|
+
const template = `globalThis.process??={env:{},versions:{node:'0.0.0'},platform:'browser',argv:[]};
|
|
2361
|
+
const __forgeaxWorkerPayload=${JSON.stringify(payload)};
|
|
2362
|
+
const __forgeaxSourcePath=__FORGEAX_SOURCE_PATH__;
|
|
2363
|
+
const __forgeaxNativeURL=globalThis.URL;
|
|
2364
|
+
const __forgeaxNativeFetch=globalThis.fetch?.bind(globalThis);
|
|
2365
|
+
const __forgeaxNativePostMessage=globalThis.postMessage?.bind(globalThis);
|
|
2366
|
+
const __forgeaxOwnedUrls=new Set();
|
|
2367
|
+
const __forgeaxBytes=(value)=>{const binary=atob(value);const bytes=new Uint8Array(binary.length);for(let index=0;index<binary.length;index+=1)bytes[index]=binary.charCodeAt(index);return bytes;};
|
|
2368
|
+
const __forgeaxDataURL=(source)=>{const bytes=new TextEncoder().encode(source);let binary='';for(const byte of bytes)binary+=String.fromCharCode(byte);return 'data:text/javascript;base64,'+btoa(binary);};
|
|
2369
|
+
const __forgeaxWorkerURL=(source)=>{if(/^(?:file:|data:|about:)/i.test(String(globalThis.location?.protocol??'')))return __forgeaxDataURL(source);const blob=__forgeaxNativeURL.createObjectURL(new Blob([source],{type:'text/javascript'}));__forgeaxOwnedUrls.add(blob);return blob;};
|
|
2370
|
+
const __forgeaxNormalize=(value)=>{try{return decodeURIComponent(String(value).split(/[?#]/,1)[0]).replaceAll('\\\\','/').replace(/^\\/+/, '');}catch{return undefined;}};
|
|
2371
|
+
const __forgeaxWithoutBundle=(value)=>String(value).replace(/^__forgeax-bundle\\//,'');
|
|
2372
|
+
const __forgeaxLookup=(value)=>{let url;try{url=value instanceof __forgeaxNativeURL?value:new __forgeaxNativeURL(String(value),'https://forgeax.invalid/'+__forgeaxSourcePath);}catch{return undefined;}const path=__forgeaxNormalize(url.pathname);if(path===undefined)return undefined;const normalized=__forgeaxWithoutBundle(path);const exact=__forgeaxWorkerPayload.find((entry)=>entry.path===path||__forgeaxWithoutBundle(entry.path)===normalized);if(exact!==undefined)return exact;const suffix=__forgeaxWorkerPayload.filter((entry)=>normalized.endsWith('/'+__forgeaxWithoutBundle(entry.path)));if(suffix.length===1)return suffix[0];const name=normalized.slice(normalized.lastIndexOf('/')+1);const matches=__forgeaxWorkerPayload.filter((entry)=>__forgeaxWithoutBundle(entry.path).slice(__forgeaxWithoutBundle(entry.path).lastIndexOf('/')+1)===name);return matches.length===1?matches[0]:undefined;};
|
|
2373
|
+
const __forgeaxResourceURL=(value)=>{const raw=String(value);try{const url=new __forgeaxNativeURL(raw,'https://forgeax.invalid/'+__forgeaxSourcePath);if(url.protocol!=='http:'&&url.protocol!=='https:')return url;const path=__forgeaxNormalize(url.pathname);return path===undefined?url:new __forgeaxNativeURL('forgeax-resource:///'+__forgeaxWithoutBundle(path));}catch{return new __forgeaxNativeURL(raw,'https://forgeax.invalid/'+__forgeaxSourcePath);}};
|
|
2374
|
+
globalThis.__forgeaxURL=__forgeaxResourceURL;
|
|
2375
|
+
class __ForgeaxURL extends __forgeaxNativeURL{constructor(input,base){if(base!==undefined&&/^blob:/i.test(String(base))&&!/^[a-z][a-z0-9+.-]*:/i.test(String(input))){super(__forgeaxResourceURL(input));return;}super(input,base);}}
|
|
2376
|
+
globalThis.URL=__ForgeaxURL;
|
|
2377
|
+
const __forgeaxNote=(kind,specifier)=>{const value={kind,specifier:String(specifier)};try{__forgeaxNativePostMessage?.({__forgeaxResourceMiss:value});}catch{}};
|
|
2378
|
+
const __forgeaxNoteHit=()=>{try{__forgeaxNativePostMessage?.({__forgeaxResourceHit:1});}catch{}};
|
|
2379
|
+
const __forgeaxNoteExternal=(kind,specifier)=>{const value={kind,specifier:String(specifier)};try{__forgeaxNativePostMessage?.({__forgeaxExternalRequest:value});}catch{}};
|
|
2380
|
+
const __forgeaxResponse=(entry)=>{__forgeaxNoteHit();return new Response(__forgeaxBytes(entry.data),{status:200,headers:{'Content-Type':entry.mime}});};
|
|
2381
|
+
const __forgeaxImportUrls=new Map();
|
|
2382
|
+
const __forgeaxImport=async(value)=>{const entry=__forgeaxLookup(value);if(entry===undefined){__forgeaxNote('import',value);throw new TypeError('forgeax single-html worker resource miss');}let url=__forgeaxImportUrls.get(entry.path);if(url===undefined){url=__forgeaxNativeURL.createObjectURL(new Blob([__forgeaxBytes(entry.data)],{type:'text/javascript'}));__forgeaxOwnedUrls.add(url);__forgeaxImportUrls.set(entry.path,url);}__forgeaxNoteHit();return import(url);};
|
|
2383
|
+
globalThis.__forgeaxImport=__forgeaxImport;
|
|
2384
|
+
if(typeof __forgeaxNativeFetch==='function')globalThis.fetch=(input,init)=>{const entry=__forgeaxLookup(input);if(entry!==undefined)return Promise.resolve(__forgeaxResponse(entry));const text=String(input);if(/^(?:data:|blob:)/i.test(text))return __forgeaxNativeFetch(input,init);if(/^https?:/i.test(text)){__forgeaxNoteExternal('fetch',input);}else{__forgeaxNote('fetch',input);}return Promise.reject(new TypeError('forgeax single-html worker resource miss'));};
|
|
2385
|
+
const __forgeaxWorkerPrelude=__FORGEAX_WORKER_FACTORY__;
|
|
2386
|
+
const __forgeaxNativeWorker=globalThis.Worker;
|
|
2387
|
+
if(typeof __forgeaxNativeWorker==='function')globalThis.Worker=class extends __forgeaxNativeWorker{constructor(input,options){const entry=__forgeaxLookup(input);if(entry===undefined){__forgeaxNote('worker',input);throw new TypeError('forgeax single-html worker resource miss');}const source=new TextDecoder().decode(__forgeaxBytes(entry.data));super(__forgeaxWorkerURL(__forgeaxWorkerPrelude(entry.path)+source),options);}};
|
|
2388
|
+
globalThis.addEventListener?.('message',(event)=>{if(event.data?.__forgeaxResourceMiss||event.data?.__forgeaxResourceHit||event.data?.__forgeaxExternalRequest){try{__forgeaxNativePostMessage?.(event.data);}catch{}event.stopImmediatePropagation?.();}});
|
|
2389
|
+
globalThis.addEventListener?.('unload',()=>{for(const url of __forgeaxOwnedUrls)__forgeaxNativeURL.revokeObjectURL(url);__forgeaxOwnedUrls.clear();});`;
|
|
2390
|
+
const factoryToken = "__FORGEAX_WORKER_FACTORY__";
|
|
2391
|
+
const recursiveTemplate = template;
|
|
2392
|
+
const factorySource = `(sourcePath)=>{const source=${JSON.stringify(recursiveTemplate)};return source.replace(${JSON.stringify(factoryToken)},()=> '('+__forgeaxWorkerPrelude.toString()+')').replace('__FORGEAX_SOURCE_PATH__',()=>JSON.stringify(sourcePath));}`;
|
|
2393
|
+
return `const __forgeaxWorkerPrelude=${factorySource};`;
|
|
2394
|
+
}
|
|
2395
|
+
function runtimeBootstrap(resources) {
|
|
2396
|
+
const workerFactory = workerPrelude(resources);
|
|
2397
|
+
return `(() => {
|
|
2398
|
+
globalThis.process ??= { env: {}, versions: { node: '0.0.0' }, platform: 'browser', argv: [] };
|
|
2399
|
+
const nativeURL = globalThis.URL;
|
|
2400
|
+
const nativeFetch = globalThis.fetch?.bind(globalThis);
|
|
2401
|
+
const nodes = [...document.querySelectorAll('script[data-forgeax-asset]')];
|
|
2402
|
+
const entries = nodes.map((node) => ({ path: node.getAttribute('data-path'), mime: node.getAttribute('data-mime') || 'application/octet-stream', data: (node.textContent || '').trim() })).filter((entry) => typeof entry.path === 'string');
|
|
2403
|
+
const decode = (value) => { const binary = atob(value); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); return bytes; };
|
|
2404
|
+
const normalize = (value) => { try { return decodeURIComponent(String(value).split(/[?#]/, 1)[0]).replaceAll('\\\\', '/').replace(/^\\/+/, ''); } catch { return undefined; } };
|
|
2405
|
+
const withoutBundle = (value) => String(value).replace(/^__forgeax-bundle\\//, '');
|
|
2406
|
+
const lookup = (input) => { let url; try { url = input instanceof nativeURL ? input : new nativeURL(String(input), document.baseURI); } catch { return undefined; } if (url.protocol === 'data:' || url.protocol === 'blob:') return undefined; const path = normalize(url.pathname); if (path === undefined) return undefined; const normalized = withoutBundle(path); const exact = entries.find((entry) => entry.path === path || withoutBundle(entry.path) === normalized); if (exact !== undefined) return exact; const suffix = entries.filter((entry) => normalized.endsWith('/' + withoutBundle(entry.path))); if (suffix.length === 1) return suffix[0]; const name = normalized.slice(normalized.lastIndexOf('/') + 1); const matches = entries.filter((entry) => withoutBundle(entry.path).slice(withoutBundle(entry.path).lastIndexOf('/') + 1) === name); return matches.length === 1 ? matches[0] : undefined; };
|
|
2407
|
+
const resourceMisses = [];
|
|
2408
|
+
const externalRequests = [];
|
|
2409
|
+
let resourceHits = 0;
|
|
2410
|
+
const noteMiss = (kind, specifier) => { resourceMisses.push({ kind, specifier: String(specifier), realm: 'main' }); };
|
|
2411
|
+
const noteExternal = (kind, specifier) => { externalRequests.push({ kind, specifier: String(specifier), realm: 'main' }); };
|
|
2412
|
+
const response = (entry) => { resourceHits += 1; return new Response(decode(entry.data), { status: 200, headers: { 'Content-Type': entry.mime } }); };
|
|
2413
|
+
const ownedBlobUrls = new Set();
|
|
2414
|
+
const makeBlobUrl = (entry) => { const url = nativeURL.createObjectURL(new Blob([decode(entry.data)], { type: entry.mime })); ownedBlobUrls.add(url); return url; };
|
|
2415
|
+
const dataUrl = (source) => { const bytes = new TextEncoder().encode(source); let binary = ''; for (const byte of bytes) binary += String.fromCharCode(byte); return 'data:text/javascript;base64,' + btoa(binary); };
|
|
2416
|
+
const workerUrl = (source) => { if (/^(?:file:|data:|about:)/i.test(String(globalThis.location?.protocol ?? ''))) return dataUrl(source); const url = nativeURL.createObjectURL(new Blob([source], { type: 'text/javascript' })); ownedBlobUrls.add(url); return url; };
|
|
2417
|
+
const importUrls = new Map();
|
|
2418
|
+
const forgeaxImport = async (specifier) => { const entry = lookup(specifier); if (entry === undefined) { const text = String(specifier); if (/^(?:https?:)/i.test(text)) noteExternal('import', text); else noteMiss('import', text); throw new TypeError('forgeax single-html resource miss'); } let url = importUrls.get(entry.path); if (url === undefined) { url = makeBlobUrl(entry); importUrls.set(entry.path, url); } resourceHits += 1; return import(url); };
|
|
2419
|
+
globalThis.__forgeaxImport = forgeaxImport;
|
|
2420
|
+
const external = (input) => { try { const url = input instanceof nativeURL ? input : new nativeURL(String(input), document.baseURI); return url.protocol === 'http:' || url.protocol === 'https:'; } catch { return false; } };
|
|
2421
|
+
if (typeof nativeFetch === 'function') globalThis.fetch = (input, init) => { const entry = lookup(input); if (entry !== undefined) return Promise.resolve(response(entry)); if (/^(?:data:|blob:)/i.test(String(input))) return nativeFetch(input, init); if (external(input)) { noteExternal('fetch', input); return Promise.reject(new TypeError('forgeax single-html blocked an external request')); } noteMiss('fetch', input); return Promise.reject(new TypeError('forgeax single-html resource miss')); };
|
|
2422
|
+
${workerFactory}
|
|
2423
|
+
const nativeWorker = globalThis.Worker;
|
|
2424
|
+
const attachWorker = (worker) => { worker.addEventListener('message', (event) => { const miss = event.data?.__forgeaxResourceMiss; if (miss !== undefined) { resourceMisses.push({ ...miss, realm: 'worker' }); event.stopImmediatePropagation?.(); } const externalRequest = event.data?.__forgeaxExternalRequest; if (externalRequest !== undefined) { externalRequests.push({ ...externalRequest, realm: 'worker' }); event.stopImmediatePropagation?.(); } const hit = event.data?.__forgeaxResourceHit; if (typeof hit === 'number' && Number.isFinite(hit)) resourceHits += hit; }); return worker; };
|
|
2425
|
+
if (typeof nativeWorker === 'function') globalThis.Worker = class extends nativeWorker { constructor(input, options) { const entry = lookup(input); if (entry === undefined) { const text = String(input); if (external(input)) noteExternal('worker', text); else noteMiss('worker', text); throw new TypeError('forgeax single-html worker resource miss'); } const source = new TextDecoder().decode(decode(entry.data)); super(workerUrl(__forgeaxWorkerPrelude(entry.path) + source), options); attachWorker(this); } };
|
|
2426
|
+
const witness = () => ({ ready: document.documentElement.dataset.forgeaxSingleHtmlReady === 'true', resourceHits, resourceMisses: resourceMisses.slice(), externalRequests: externalRequests.slice() });
|
|
2427
|
+
globalThis.__forgeaxSingleHtml = { lookup, entries: entries.map(({ path, mime }) => ({ path, mime })), witness };
|
|
2428
|
+
globalThis.addEventListener('pagehide', () => { for (const url of ownedBlobUrls) nativeURL.revokeObjectURL(url); ownedBlobUrls.clear(); }, { once: true });
|
|
2429
|
+
document.documentElement.dataset.forgeaxSingleHtmlReady = 'true';
|
|
2430
|
+
})();`;
|
|
2431
|
+
}
|
|
2432
|
+
function escapeAttribute(value) {
|
|
2433
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
2434
|
+
}
|
|
2435
|
+
function distResources(distRoot, manifest) {
|
|
2436
|
+
const rows = [
|
|
2437
|
+
...manifest.artifacts,
|
|
2438
|
+
{ path: "forgeax-dist.json" }
|
|
2439
|
+
];
|
|
2440
|
+
return Promise.all(
|
|
2441
|
+
rows.filter((artifact) => artifact.path !== "index.html").map(async (artifact) => ({
|
|
2442
|
+
path: artifact.path,
|
|
2443
|
+
bytes: await readFile(resolve(distRoot, artifact.path)),
|
|
2444
|
+
mediaType: "mediaType" in artifact ? artifact.mediaType : mediaType(artifact.path)
|
|
2445
|
+
}))
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
function buildHtml(indexHtml, distArtifacts, bundle) {
|
|
2449
|
+
const resources = /* @__PURE__ */ new Map();
|
|
2450
|
+
for (const resource of [...distArtifacts, ...bundle.artifacts]) {
|
|
2451
|
+
if (resources.has(resource.path)) {
|
|
2452
|
+
return errorResult(
|
|
2453
|
+
"single-html-asset-duplicate",
|
|
2454
|
+
"embedded resource paths to be unique",
|
|
2455
|
+
"Repair the generated bundle path collision before packaging.",
|
|
2456
|
+
{ path: resource.path }
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
resources.set(resource.path, resource);
|
|
2460
|
+
}
|
|
2461
|
+
const replacements = [];
|
|
2462
|
+
for (const element of scanHtmlElements(indexHtml)) {
|
|
2463
|
+
if (isModuleScript(element) || isModulePreload(element)) {
|
|
2464
|
+
replacements.push({ start: element.start, end: element.end, value: "" });
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
if (isStylesheet(element)) {
|
|
2468
|
+
const href = attributeValue(element, "href");
|
|
2469
|
+
if (href === void 0) {
|
|
2470
|
+
return errorResult(
|
|
2471
|
+
"single-html-stylesheet-missing",
|
|
2472
|
+
"stylesheet link to contain href",
|
|
2473
|
+
"Repair the generated HTML stylesheet link."
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
const path = resourcePath(href, "index.html");
|
|
2477
|
+
const stylesheet = path === void 0 ? void 0 : resourceForPath(path, resources);
|
|
2478
|
+
if (stylesheet === void 0) {
|
|
2479
|
+
return errorResult(
|
|
2480
|
+
"single-html-css-asset-missing",
|
|
2481
|
+
"stylesheet link to resolve to an embedded artifact",
|
|
2482
|
+
"Add the stylesheet to the dist closure and rebuild.",
|
|
2483
|
+
{ href, path: path ?? null }
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
const css = inlineCss(
|
|
2487
|
+
Buffer.from(stylesheet.bytes).toString("utf8"),
|
|
2488
|
+
stylesheet.path,
|
|
2489
|
+
resources
|
|
2490
|
+
);
|
|
2491
|
+
if (!css.ok) return css;
|
|
2492
|
+
replacements.push({
|
|
2493
|
+
start: element.start,
|
|
2494
|
+
end: element.end,
|
|
2495
|
+
value: `<style data-forgeax-inline-css>${css.value}</style>`
|
|
2496
|
+
});
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
if (element.name === "style" && element.contentStart !== void 0 && element.contentEnd !== void 0) {
|
|
2500
|
+
const css = inlineCss(
|
|
2501
|
+
indexHtml.slice(element.contentStart, element.contentEnd),
|
|
2502
|
+
"index.html",
|
|
2503
|
+
resources
|
|
2504
|
+
);
|
|
2505
|
+
if (!css.ok) return css;
|
|
2506
|
+
replacements.push({ start: element.contentStart, end: element.contentEnd, value: css.value });
|
|
2507
|
+
continue;
|
|
2508
|
+
}
|
|
2509
|
+
const resourceAttribute = element.name === "img" || element.name === "source" || element.name === "video" || element.name === "audio" || element.name === "link" ? element.attributes.find((candidate) => ["src", "poster", "href"].includes(candidate.name)) : void 0;
|
|
2510
|
+
const resourceAttributeLocation = resourceAttribute === void 0 ? void 0 : element.attributeLocations[resourceAttribute.name];
|
|
2511
|
+
const resourceAttributeRange = resourceAttribute === void 0 || resourceAttributeLocation === void 0 ? void 0 : attributeValueRange(indexHtml, resourceAttributeLocation);
|
|
2512
|
+
if (resourceAttribute?.value !== void 0 && resourceAttributeRange !== void 0 && !/^(?:data:|blob:|#)/i.test(resourceAttribute.value)) {
|
|
2513
|
+
const path = resourcePath(resourceAttribute.value, "index.html");
|
|
2514
|
+
const embedded = path === void 0 ? void 0 : resourceForPath(path, resources);
|
|
2515
|
+
if (embedded === void 0) {
|
|
2516
|
+
return errorResult(
|
|
2517
|
+
"single-html-asset-missing",
|
|
2518
|
+
"every local HTML resource to resolve to an embedded dist artifact",
|
|
2519
|
+
"Add the referenced asset to the project closure and rebuild.",
|
|
2520
|
+
{ value: resourceAttribute.value, path: path ?? null }
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
replacements.push({
|
|
2524
|
+
start: resourceAttributeRange.startOffset,
|
|
2525
|
+
end: resourceAttributeRange.endOffset,
|
|
2526
|
+
value: dataUri(embedded.bytes, embedded.mediaType)
|
|
2527
|
+
});
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
if (element.name === "script" && attributeValue(element, "src") !== void 0) {
|
|
2531
|
+
return errorResult(
|
|
2532
|
+
"single-html-script-external",
|
|
2533
|
+
"non-module script sources to be absent from the generated host",
|
|
2534
|
+
"Move the script into the production module entry before packaging.",
|
|
2535
|
+
{ src: attributeValue(element, "src") }
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
const assetNodes = [...resources.values()].sort((left, right) => left.path.localeCompare(right.path)).map(
|
|
2540
|
+
(resource, index) => `<script type="application/octet-stream" id="forgeax-asset-${index}" data-forgeax-asset data-path="${escapeAttribute(resource.path)}" data-mime="${escapeAttribute(resource.mediaType)}">${Buffer.from(resource.bytes).toString("base64")}</script>`
|
|
2541
|
+
).join("");
|
|
2542
|
+
const bootstrap = `<script type="application/javascript">${safeScriptText(runtimeBootstrap([...resources.values()]))}</script>`;
|
|
2543
|
+
const entry = `<script type="module">${safeScriptText(bundle.entrySource)}</script>`;
|
|
2544
|
+
const withResources = applyReplacements(indexHtml, replacements);
|
|
2545
|
+
const body = `${assetNodes}${bootstrap}${entry}`;
|
|
2546
|
+
const html = withResources.includes("</body>") ? withResources.replace(/<\/body>/i, () => `${body}</body>`) : `${withResources}${body}`;
|
|
2547
|
+
return { ok: true, value: { html, embeddedAssets: resources.size } };
|
|
2548
|
+
}
|
|
2549
|
+
async function writeSingleHtml(options) {
|
|
2550
|
+
const distRoot = resolve(options.distRoot);
|
|
2551
|
+
const output = resolve(options.output);
|
|
2552
|
+
const checksumPath = `${output}.sha256`;
|
|
2553
|
+
const temporary = `${output}.partial-${process.pid}`;
|
|
2554
|
+
const temporaryChecksum = `${checksumPath}.partial-${process.pid}`;
|
|
2555
|
+
try {
|
|
2556
|
+
const indexHtml = await readFile(resolve(distRoot, "index.html"), "utf8");
|
|
2557
|
+
const resources = await distResources(distRoot, options.manifest);
|
|
2558
|
+
const built = buildHtml(indexHtml, resources, options.bundle);
|
|
2559
|
+
if (!built.ok) return built;
|
|
2560
|
+
const bytes = Buffer.from(built.value.html, "utf8");
|
|
2561
|
+
const sha2562 = hashBytes(bytes);
|
|
2562
|
+
const manifestBytes = await readFile(resolve(distRoot, "forgeax-dist.json"));
|
|
2563
|
+
await mkdir(dirname(output), { recursive: true });
|
|
2564
|
+
await writeFile(temporary, bytes);
|
|
2565
|
+
await writeFile(temporaryChecksum, `${sha2562} ${basename(output)}
|
|
2566
|
+
`, "utf8");
|
|
2567
|
+
await rename(temporaryChecksum, checksumPath);
|
|
2568
|
+
await rename(temporary, output);
|
|
2569
|
+
return {
|
|
2570
|
+
ok: true,
|
|
2571
|
+
value: {
|
|
2572
|
+
schemaVersion: "1.0.0",
|
|
2573
|
+
format: SINGLE_HTML_FORMAT,
|
|
2574
|
+
target: "file",
|
|
2575
|
+
project: options.manifest.project,
|
|
2576
|
+
base: options.manifest.base,
|
|
2577
|
+
html: { path: output, bytes: bytes.byteLength, sha256: sha2562 },
|
|
2578
|
+
checksumPath,
|
|
2579
|
+
distManifestSha256: hashBytes(manifestBytes),
|
|
2580
|
+
embeddedAssets: built.value.embeddedAssets,
|
|
2581
|
+
run: {
|
|
2582
|
+
local: pathToFileURL(output).href,
|
|
2583
|
+
shared: "send the HTML file and open it in a desktop Chrome with WebGPU support"
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
} catch (cause) {
|
|
2588
|
+
return toErrorResult(
|
|
2589
|
+
cause,
|
|
2590
|
+
"single-html-write-failed",
|
|
2591
|
+
"the single HTML candidate and adjacent SHA-256 to be written atomically",
|
|
2592
|
+
"Repair the dist closure or output directory, then retry packaging."
|
|
2593
|
+
);
|
|
2594
|
+
} finally {
|
|
2595
|
+
await Promise.all([rm(temporary, { force: true }), rm(temporaryChecksum, { force: true })]);
|
|
1403
2596
|
}
|
|
1404
|
-
return {
|
|
1405
|
-
code: fallbackCode,
|
|
1406
|
-
expected: "the ForgeaX command to complete",
|
|
1407
|
-
hint: "Inspect the underlying diagnostic and repair the owning input.",
|
|
1408
|
-
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
1409
|
-
};
|
|
1410
2597
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
2598
|
+
function packageFormatError(format) {
|
|
2599
|
+
return errorResult(
|
|
2600
|
+
"package-format-unsupported",
|
|
2601
|
+
"package format to be web-zip or single-html",
|
|
2602
|
+
"Use web-zip for HTTPS hosting or single-html for a self-contained file:// delivery.",
|
|
2603
|
+
{ format }
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
function packageOutputError(output, format) {
|
|
2607
|
+
const expectedSuffix = format === "single-html" ? ".html" : ".zip";
|
|
2608
|
+
return errorResult(
|
|
2609
|
+
"package-output-suffix-mismatch",
|
|
2610
|
+
`package output to end with ${expectedSuffix}`,
|
|
2611
|
+
`Use a ${expectedSuffix} output path for ${format}.`,
|
|
2612
|
+
{ output, format }
|
|
2613
|
+
);
|
|
2614
|
+
}
|
|
2615
|
+
var SINGLE_HTML_FORMAT, GENERATED_PREFIX, SingleHtmlError;
|
|
2616
|
+
var init_single_html = __esm({
|
|
2617
|
+
"src/single-html.ts"() {
|
|
2618
|
+
init_dist();
|
|
2619
|
+
init_host();
|
|
2620
|
+
SINGLE_HTML_FORMAT = "forgeax-single-html-game";
|
|
2621
|
+
GENERATED_PREFIX = "__forgeax-bundle/";
|
|
2622
|
+
SingleHtmlError = class extends Error {
|
|
2623
|
+
constructor(code, expected, hint, detail) {
|
|
2624
|
+
super(`${code}: ${hint}`);
|
|
2625
|
+
this.code = code;
|
|
2626
|
+
this.expected = expected;
|
|
2627
|
+
this.hint = hint;
|
|
2628
|
+
this.detail = detail;
|
|
2629
|
+
this.name = "SingleHtmlError";
|
|
2630
|
+
}
|
|
2631
|
+
code;
|
|
2632
|
+
expected;
|
|
2633
|
+
hint;
|
|
2634
|
+
detail;
|
|
2635
|
+
};
|
|
1413
2636
|
}
|
|
1414
2637
|
});
|
|
1415
2638
|
|
|
@@ -1679,6 +2902,7 @@ var init_package = __esm({
|
|
|
1679
2902
|
"@forgeax/engine-project": "workspace:*",
|
|
1680
2903
|
"@forgeax/engine-rhi-debug": "workspace:*",
|
|
1681
2904
|
"@forgeax/engine-rhi-null": "workspace:*",
|
|
2905
|
+
"@forgeax/engine-rhi-webgpu": "workspace:*",
|
|
1682
2906
|
"@forgeax/engine-render": "workspace:*",
|
|
1683
2907
|
"@forgeax/engine-render-graph": "workspace:*",
|
|
1684
2908
|
"@forgeax/engine-runtime": "workspace:*",
|
|
@@ -1689,11 +2913,14 @@ var init_package = __esm({
|
|
|
1689
2913
|
"@forgeax/engine-types": "workspace:*",
|
|
1690
2914
|
"@forgeax/engine-vfx-compiler": "workspace:*",
|
|
1691
2915
|
"@forgeax/engine-vite-plugin-pack": "workspace:*",
|
|
2916
|
+
"@forgeax/engine-vite-plugin-rhi-debug": "workspace:*",
|
|
1692
2917
|
"@forgeax/engine-vite-plugin-shader": "workspace:*",
|
|
1693
2918
|
jiti: "1.21.7",
|
|
2919
|
+
parse5: "7.3.0",
|
|
1694
2920
|
playwright: "1.60.0",
|
|
1695
2921
|
vite: "8.0.10",
|
|
1696
|
-
vitest: "4.1.
|
|
2922
|
+
vitest: "4.1.11",
|
|
2923
|
+
webgpu: "^0.4.0"
|
|
1697
2924
|
},
|
|
1698
2925
|
devDependencies: {
|
|
1699
2926
|
"@types/node": "^20.14.0"
|
|
@@ -1867,7 +3094,7 @@ var init_init = __esm({
|
|
|
1867
3094
|
"@webgpu/types": "0.1.71",
|
|
1868
3095
|
tsx: "4.23.1",
|
|
1869
3096
|
typescript: "6.0.3",
|
|
1870
|
-
vitest: "4.1.
|
|
3097
|
+
vitest: "4.1.11"
|
|
1871
3098
|
};
|
|
1872
3099
|
}
|
|
1873
3100
|
});
|
|
@@ -1889,15 +3116,12 @@ async function findSdkContext() {
|
|
|
1889
3116
|
const templates = new Map(
|
|
1890
3117
|
manifest.templates.map((template) => [template.id, resolve(cursor, template.root)])
|
|
1891
3118
|
);
|
|
1892
|
-
const defaultTemplate = manifest.templates.find((template) => template.default);
|
|
1893
|
-
if (defaultTemplate === void 0) throw new Error("sdk-default-template-missing");
|
|
1894
3119
|
const store = resolve(cursor, "store", "pnpm");
|
|
1895
3120
|
return {
|
|
1896
3121
|
root: cursor,
|
|
1897
3122
|
manifest,
|
|
1898
3123
|
...await readable(store) ? { store } : {},
|
|
1899
|
-
templates
|
|
1900
|
-
defaultTemplate: defaultTemplate.id
|
|
3124
|
+
templates
|
|
1901
3125
|
};
|
|
1902
3126
|
}
|
|
1903
3127
|
const parent = dirname(cursor);
|
|
@@ -1917,10 +3141,14 @@ function agentOnboarding(sdk, projectRoot2) {
|
|
|
1917
3141
|
resolve(root, "skills", "forgeax-engine-sdk", "SKILL.md"),
|
|
1918
3142
|
resolve(root, "skills", "forgeax-engine-sdk", "references", "feature-catalog.md")
|
|
1919
3143
|
],
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
3144
|
+
...projectRoot2 === void 0 ? {
|
|
3145
|
+
templateSelection: {
|
|
3146
|
+
required: true,
|
|
3147
|
+
game3d: "game-3d",
|
|
3148
|
+
otherwise: "empty"
|
|
3149
|
+
}
|
|
3150
|
+
} : {},
|
|
3151
|
+
...projectRoot2 === void 0 ? {} : { next: { cwd: root, argv: ["pnpm", "exec", "forgeax", "list", "--json"] } }
|
|
1924
3152
|
};
|
|
1925
3153
|
}
|
|
1926
3154
|
function sdkInitPath(sdk) {
|
|
@@ -1934,6 +3162,7 @@ function sdkProjectInstallArgs(store) {
|
|
|
1934
3162
|
"install",
|
|
1935
3163
|
"--frozen-lockfile",
|
|
1936
3164
|
"--ignore-scripts",
|
|
3165
|
+
"--config.pm-on-fail=ignore",
|
|
1937
3166
|
"--side-effects-cache=true",
|
|
1938
3167
|
"--child-concurrency=1"
|
|
1939
3168
|
];
|
|
@@ -1943,6 +3172,7 @@ function sdkBootstrapInstallArgs(store) {
|
|
|
1943
3172
|
const common = [
|
|
1944
3173
|
"install",
|
|
1945
3174
|
"--frozen-lockfile",
|
|
3175
|
+
"--config.pm-on-fail=ignore",
|
|
1946
3176
|
"--child-concurrency=1",
|
|
1947
3177
|
"--side-effects-cache=true"
|
|
1948
3178
|
];
|
|
@@ -2000,8 +3230,8 @@ async function requireSdkInitialization(sdk) {
|
|
|
2000
3230
|
return { ok: true, value: state };
|
|
2001
3231
|
}
|
|
2002
3232
|
async function copyBootstrapInputs(sdk, root) {
|
|
2003
|
-
const template = sdk.templates.get(
|
|
2004
|
-
if (template === void 0) throw new Error("sdk-
|
|
3233
|
+
const template = sdk.templates.get("empty");
|
|
3234
|
+
if (template === void 0) throw new Error("sdk-bootstrap-template-missing");
|
|
2005
3235
|
for (const name of ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"]) {
|
|
2006
3236
|
await cp(resolve(template, name), resolve(root, name));
|
|
2007
3237
|
}
|
|
@@ -2193,13 +3423,13 @@ async function pathKind(path) {
|
|
|
2193
3423
|
throw cause;
|
|
2194
3424
|
}
|
|
2195
3425
|
}
|
|
2196
|
-
async function
|
|
3426
|
+
async function filesUnder3(root, directory = root) {
|
|
2197
3427
|
const files = [];
|
|
2198
3428
|
for (const name of (await readdir(directory)).sort()) {
|
|
2199
3429
|
const path = resolve(directory, name);
|
|
2200
3430
|
const info = await lstat(path);
|
|
2201
3431
|
if (info.isSymbolicLink()) throw new Error(`skill-source-symlink: ${path}`);
|
|
2202
|
-
if (info.isDirectory()) files.push(...await
|
|
3432
|
+
if (info.isDirectory()) files.push(...await filesUnder3(root, path));
|
|
2203
3433
|
else if (info.isFile()) files.push(path);
|
|
2204
3434
|
}
|
|
2205
3435
|
return files;
|
|
@@ -2211,7 +3441,7 @@ async function skillRow(sourceRoot, id) {
|
|
|
2211
3441
|
if (await pathKind(resolve(root, "SKILL.md")) !== "file") {
|
|
2212
3442
|
throw new Error(`skill-entry-missing: ${id}/SKILL.md`);
|
|
2213
3443
|
}
|
|
2214
|
-
const files = await
|
|
3444
|
+
const files = await filesUnder3(root);
|
|
2215
3445
|
return {
|
|
2216
3446
|
id,
|
|
2217
3447
|
root: `skills/${id}`,
|
|
@@ -2228,8 +3458,8 @@ async function discoverProjectSkills(root) {
|
|
|
2228
3458
|
return Promise.all(ids.map((id) => skillRow(sourceRoot, id)));
|
|
2229
3459
|
}
|
|
2230
3460
|
async function sameFiles(leftRoot, rightRoot) {
|
|
2231
|
-
const left = await
|
|
2232
|
-
const right = await
|
|
3461
|
+
const left = await filesUnder3(leftRoot);
|
|
3462
|
+
const right = await filesUnder3(rightRoot);
|
|
2233
3463
|
const leftNames = left.map((path) => slash(relative(leftRoot, path)));
|
|
2234
3464
|
const rightNames = right.map((path) => slash(relative(rightRoot, path)));
|
|
2235
3465
|
if (JSON.stringify(leftNames) !== JSON.stringify(rightNames)) return false;
|
|
@@ -2653,8 +3883,8 @@ async function initCommand(options = {}) {
|
|
|
2653
3883
|
const applied = await applyInitPlan(facts.value, plan.value, options);
|
|
2654
3884
|
if (!applied.ok || options.dryRun === true) return applied;
|
|
2655
3885
|
if (sdk !== void 0) {
|
|
2656
|
-
const template = sdk.templates.get(
|
|
2657
|
-
if (template === void 0) throw new Error("sdk-
|
|
3886
|
+
const template = sdk.templates.get("empty");
|
|
3887
|
+
if (template === void 0) throw new Error("sdk-bootstrap-template-missing");
|
|
2658
3888
|
await copyFile(
|
|
2659
3889
|
resolve(template, "pnpm-lock.yaml"),
|
|
2660
3890
|
resolve(facts.value.root, "pnpm-lock.yaml")
|
|
@@ -2722,7 +3952,18 @@ async function newCommand(options = {}) {
|
|
|
2722
3952
|
}
|
|
2723
3953
|
};
|
|
2724
3954
|
}
|
|
2725
|
-
|
|
3955
|
+
if (options.template === void 0) {
|
|
3956
|
+
return {
|
|
3957
|
+
ok: false,
|
|
3958
|
+
error: {
|
|
3959
|
+
code: "sdk-template-required",
|
|
3960
|
+
expected: "forgeax new to select exactly one template with --template",
|
|
3961
|
+
hint: "Use --template game-3d for a 3D game; otherwise use --template empty.",
|
|
3962
|
+
detail: { templates: [...sdk.templates.keys()].sort() }
|
|
3963
|
+
}
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
const templateId = options.template;
|
|
2726
3967
|
const template = sdk.templates.get(templateId);
|
|
2727
3968
|
if (template === void 0) {
|
|
2728
3969
|
return {
|
|
@@ -2854,7 +4095,7 @@ __export(plugin_authoring_exports, {
|
|
|
2854
4095
|
pluginInstallCommand: () => pluginInstallCommand,
|
|
2855
4096
|
pluginUninstallCommand: () => pluginUninstallCommand
|
|
2856
4097
|
});
|
|
2857
|
-
async function
|
|
4098
|
+
async function readManifest2(root) {
|
|
2858
4099
|
const path = resolve(root, "forge.json");
|
|
2859
4100
|
try {
|
|
2860
4101
|
const raw = await readFile(path, "utf8");
|
|
@@ -2907,7 +4148,7 @@ async function mutateDependency(root, action, dependency) {
|
|
|
2907
4148
|
}
|
|
2908
4149
|
async function pluginInstallCommand(options) {
|
|
2909
4150
|
const root = resolve(options.root ?? process.cwd());
|
|
2910
|
-
const manifest = await
|
|
4151
|
+
const manifest = await readManifest2(root);
|
|
2911
4152
|
if (!manifest.ok) return manifest;
|
|
2912
4153
|
const parsed = GameProjectSchema.safeParse(manifest.value.value);
|
|
2913
4154
|
if (!parsed.success) {
|
|
@@ -2960,7 +4201,7 @@ async function pluginInstallCommand(options) {
|
|
|
2960
4201
|
}
|
|
2961
4202
|
async function pluginUninstallCommand(options) {
|
|
2962
4203
|
const root = resolve(options.root ?? process.cwd());
|
|
2963
|
-
const manifest = await
|
|
4204
|
+
const manifest = await readManifest2(root);
|
|
2964
4205
|
if (!manifest.ok) return manifest;
|
|
2965
4206
|
const parsed = GameProjectSchema.safeParse(manifest.value.value);
|
|
2966
4207
|
if (!parsed.success) {
|
|
@@ -3048,26 +4289,62 @@ function createCliRhiDebugOperationContext() {
|
|
|
3048
4289
|
};
|
|
3049
4290
|
}
|
|
3050
4291
|
},
|
|
3051
|
-
async createReplayBackend() {
|
|
4292
|
+
async createReplayBackend(tape) {
|
|
4293
|
+
let createDawn;
|
|
4294
|
+
let gpuGlobals;
|
|
4295
|
+
try {
|
|
4296
|
+
const dawn = await import('webgpu');
|
|
4297
|
+
createDawn = dawn.create;
|
|
4298
|
+
gpuGlobals = dawn.globals;
|
|
4299
|
+
} catch (cause) {
|
|
4300
|
+
return {
|
|
4301
|
+
ok: false,
|
|
4302
|
+
error: {
|
|
4303
|
+
code: "replay-backend-unavailable",
|
|
4304
|
+
expected: "the Dawn WebGPU provider to load",
|
|
4305
|
+
hint: "Install the DevKit runtime closure and retry rhi.inspect.",
|
|
4306
|
+
detail: {
|
|
4307
|
+
stage: "provider",
|
|
4308
|
+
cause: cause instanceof Error ? cause.message : String(cause)
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
};
|
|
4312
|
+
}
|
|
4313
|
+
Object.assign(globalThis, gpuGlobals);
|
|
4314
|
+
const gpu = createDawn([]);
|
|
4315
|
+
if (!("navigator" in globalThis) || globalThis.navigator === void 0) {
|
|
4316
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
4317
|
+
value: {},
|
|
4318
|
+
configurable: true,
|
|
4319
|
+
writable: true
|
|
4320
|
+
});
|
|
4321
|
+
}
|
|
4322
|
+
Object.defineProperty(globalThis.navigator, "gpu", {
|
|
4323
|
+
value: gpu,
|
|
4324
|
+
configurable: true,
|
|
4325
|
+
writable: true
|
|
4326
|
+
});
|
|
3052
4327
|
const adapter = await rhi.requestAdapter();
|
|
3053
4328
|
if (!adapter.ok) {
|
|
3054
4329
|
return {
|
|
3055
4330
|
ok: false,
|
|
3056
4331
|
error: {
|
|
3057
4332
|
code: "replay-backend-unavailable",
|
|
3058
|
-
expected: "a fresh
|
|
4333
|
+
expected: "a fresh Dawn WebGPU adapter",
|
|
3059
4334
|
hint: adapter.error.hint,
|
|
3060
4335
|
detail: { stage: "adapter", cause: adapter.error.hint }
|
|
3061
4336
|
}
|
|
3062
4337
|
};
|
|
3063
4338
|
}
|
|
3064
|
-
const device = await adapter.value.requestDevice(
|
|
4339
|
+
const device = await adapter.value.requestDevice(
|
|
4340
|
+
replayDeviceRequest(tape, adapter.value.features, adapter.value.limits)
|
|
4341
|
+
);
|
|
3065
4342
|
if (!device.ok) {
|
|
3066
4343
|
return {
|
|
3067
4344
|
ok: false,
|
|
3068
4345
|
error: {
|
|
3069
4346
|
code: "replay-backend-unavailable",
|
|
3070
|
-
expected: "a fresh
|
|
4347
|
+
expected: "a fresh Dawn WebGPU device satisfying the recorded tape",
|
|
3071
4348
|
hint: device.error.hint,
|
|
3072
4349
|
detail: { stage: "device", cause: device.error.hint }
|
|
3073
4350
|
}
|
|
@@ -3105,6 +4382,9 @@ function renderRhiDebugHelp() {
|
|
|
3105
4382
|
return [
|
|
3106
4383
|
"forgeax run <operation>",
|
|
3107
4384
|
...discoverRhiDebugOperations().map((operation) => ` ${operation.name}: ${operation.summary}`),
|
|
4385
|
+
"Usage:",
|
|
4386
|
+
" forgeax run rhi.summary --artifact PATH --digest SHA256 --json",
|
|
4387
|
+
" forgeax run rhi.inspect --artifact PATH --digest SHA256 --work-index N --fields pipeline,bindings,pixels --json",
|
|
3108
4388
|
"ArtifactRef schema:",
|
|
3109
4389
|
JSON.stringify(RHI_DEBUG_OPERATION_MANIFEST.artifactRefSchema)
|
|
3110
4390
|
].join("\n");
|
|
@@ -3232,7 +4512,7 @@ async function runRhiDebugOperation(name, input, context) {
|
|
|
3232
4512
|
"Provide a fresh device and shader factory before running rhi.inspect."
|
|
3233
4513
|
);
|
|
3234
4514
|
}
|
|
3235
|
-
const backend = await context.createReplayBackend();
|
|
4515
|
+
const backend = await context.createReplayBackend(decoded.value);
|
|
3236
4516
|
if (!backend.ok) return backend;
|
|
3237
4517
|
const opened = await openReplay(decoded.value, backend.value);
|
|
3238
4518
|
if (!opened.ok) return { ok: false, error: coreError(opened.error) };
|
|
@@ -3324,7 +4604,10 @@ var init_operations = __esm({
|
|
|
3324
4604
|
properties: {
|
|
3325
4605
|
artifact: artifactRefSchema,
|
|
3326
4606
|
workIndex: { type: "integer", minimum: 0 },
|
|
3327
|
-
fields: {
|
|
4607
|
+
fields: {
|
|
4608
|
+
type: "array",
|
|
4609
|
+
items: { type: "string", enum: ["bindings", "pipeline", "pixels"] }
|
|
4610
|
+
}
|
|
3328
4611
|
},
|
|
3329
4612
|
required: ["artifact", "workIndex"],
|
|
3330
4613
|
additionalProperties: false
|
|
@@ -3364,113 +4647,840 @@ async function sdkInstallCommand(options) {
|
|
|
3364
4647
|
targetExists = false;
|
|
3365
4648
|
entries2 = [];
|
|
3366
4649
|
}
|
|
3367
|
-
if (entries2.length > 0) {
|
|
3368
|
-
return {
|
|
3369
|
-
ok: false,
|
|
3370
|
-
error: {
|
|
3371
|
-
code: "sdk-target-not-empty",
|
|
3372
|
-
expected: "the SDK install target to be absent or empty",
|
|
3373
|
-
hint: "Choose an empty versioned SDK directory so existing files cannot be overwritten.",
|
|
3374
|
-
detail: { root }
|
|
3375
|
-
}
|
|
3376
|
-
};
|
|
4650
|
+
if (entries2.length > 0) {
|
|
4651
|
+
return {
|
|
4652
|
+
ok: false,
|
|
4653
|
+
error: {
|
|
4654
|
+
code: "sdk-target-not-empty",
|
|
4655
|
+
expected: "the SDK install target to be absent or empty",
|
|
4656
|
+
hint: "Choose an empty versioned SDK directory so existing files cannot be overwritten.",
|
|
4657
|
+
detail: { root }
|
|
4658
|
+
}
|
|
4659
|
+
};
|
|
4660
|
+
}
|
|
4661
|
+
const version = options.version ?? await packageVersion();
|
|
4662
|
+
const parent = dirname(root);
|
|
4663
|
+
await mkdir(parent, { recursive: true });
|
|
4664
|
+
let staging;
|
|
4665
|
+
let download;
|
|
4666
|
+
try {
|
|
4667
|
+
staging = await mkdtemp(resolve(parent, `.${basename(root)}.forgeax-sdk-staging-`));
|
|
4668
|
+
download = await mkdtemp(resolve(parent, `.${basename(root)}.forgeax-sdk-download-`));
|
|
4669
|
+
await execFileAsync4(
|
|
4670
|
+
process.env.FORGEAX_NPM_CLIENT ?? "npm",
|
|
4671
|
+
[
|
|
4672
|
+
"install",
|
|
4673
|
+
"--prefix",
|
|
4674
|
+
download,
|
|
4675
|
+
"--ignore-scripts",
|
|
4676
|
+
"--no-package-lock",
|
|
4677
|
+
"--no-save",
|
|
4678
|
+
"--fund=false",
|
|
4679
|
+
"--audit=false",
|
|
4680
|
+
`@forgeax/engine-sdk@${version}`
|
|
4681
|
+
],
|
|
4682
|
+
{ env: { ...process.env, CI: "true" }, maxBuffer: 16 * 1024 * 1024 }
|
|
4683
|
+
);
|
|
4684
|
+
const carrier = resolve(download, "node_modules", "@forgeax", "engine-sdk", "sdk");
|
|
4685
|
+
const manifest = JSON.parse(await readFile(resolve(carrier, "sdk-manifest.json"), "utf8"));
|
|
4686
|
+
if (manifest.sdkVersion !== version) throw new Error("sdk-carrier-version-mismatch");
|
|
4687
|
+
for (const name of await readdir(carrier)) {
|
|
4688
|
+
await cp(resolve(carrier, name), resolve(staging, name), {
|
|
4689
|
+
recursive: true,
|
|
4690
|
+
errorOnExist: true,
|
|
4691
|
+
force: false
|
|
4692
|
+
});
|
|
4693
|
+
}
|
|
4694
|
+
if (!targetExists) {
|
|
4695
|
+
await rename(staging, root);
|
|
4696
|
+
staging = void 0;
|
|
4697
|
+
} else {
|
|
4698
|
+
for (const name of await readdir(staging)) {
|
|
4699
|
+
await rename(resolve(staging, name), resolve(root, name));
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
return {
|
|
4703
|
+
ok: true,
|
|
4704
|
+
value: {
|
|
4705
|
+
root,
|
|
4706
|
+
sdkVersion: version,
|
|
4707
|
+
source: "@forgeax/engine-sdk",
|
|
4708
|
+
next: { cwd: root, argv: ["node", "./bin/forgeax.mjs", "init"] }
|
|
4709
|
+
}
|
|
4710
|
+
};
|
|
4711
|
+
} catch (cause) {
|
|
4712
|
+
return { ok: false, error: commandError(cause, "sdk-install-failed") };
|
|
4713
|
+
} finally {
|
|
4714
|
+
if (staging !== void 0) await rm(staging, { recursive: true, force: true });
|
|
4715
|
+
if (download !== void 0) await rm(download, { recursive: true, force: true });
|
|
4716
|
+
}
|
|
4717
|
+
}
|
|
4718
|
+
var execFileAsync4;
|
|
4719
|
+
var init_sdk_install = __esm({
|
|
4720
|
+
"src/sdk-install.ts"() {
|
|
4721
|
+
init_project();
|
|
4722
|
+
execFileAsync4 = promisify(execFile);
|
|
4723
|
+
}
|
|
4724
|
+
});
|
|
4725
|
+
async function shaderCheckCommand(options = {}) {
|
|
4726
|
+
const facts = await readProjectFacts(options.root);
|
|
4727
|
+
if (!facts.ok) return facts;
|
|
4728
|
+
const target = options.path === void 0 ? void 0 : resolve(facts.value.root, options.path);
|
|
4729
|
+
if (target !== void 0) {
|
|
4730
|
+
try {
|
|
4731
|
+
await readFile(target);
|
|
4732
|
+
} catch (cause) {
|
|
4733
|
+
return { ok: false, error: commandError(cause, "shader-source-unreadable") };
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
const temporary = await mkdtemp(resolve(tmpdir(), "forgeax-shader-check-"));
|
|
4737
|
+
const previous = process.cwd();
|
|
4738
|
+
try {
|
|
4739
|
+
process.chdir(facts.value.root);
|
|
4740
|
+
const config = await createViteConfig(facts.value, "build");
|
|
4741
|
+
await build({
|
|
4742
|
+
...config,
|
|
4743
|
+
build: { ...config.build, outDir: temporary, emptyOutDir: true, write: true }
|
|
4744
|
+
});
|
|
4745
|
+
return { ok: true, value: { root: facts.value.root, path: target ?? null } };
|
|
4746
|
+
} catch (cause) {
|
|
4747
|
+
return { ok: false, error: commandError(cause, "shader-check-failed") };
|
|
4748
|
+
} finally {
|
|
4749
|
+
process.chdir(previous);
|
|
4750
|
+
await rm(temporary, { recursive: true, force: true });
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
var init_shader_check = __esm({
|
|
4754
|
+
"src/shader-check.ts"() {
|
|
4755
|
+
init_host();
|
|
4756
|
+
init_project();
|
|
4757
|
+
}
|
|
4758
|
+
});
|
|
4759
|
+
function fail(code, expected, hint, detail = {}) {
|
|
4760
|
+
throw new SoftwareCaptureError(code, expected, hint, detail);
|
|
4761
|
+
}
|
|
4762
|
+
async function pathExists2(path) {
|
|
4763
|
+
try {
|
|
4764
|
+
await access(path);
|
|
4765
|
+
return true;
|
|
4766
|
+
} catch {
|
|
4767
|
+
return false;
|
|
4768
|
+
}
|
|
4769
|
+
}
|
|
4770
|
+
async function reservePort() {
|
|
4771
|
+
const reservation = createServer();
|
|
4772
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
4773
|
+
reservation.once("error", rejectListen);
|
|
4774
|
+
reservation.listen(0, "127.0.0.1", resolveListen);
|
|
4775
|
+
});
|
|
4776
|
+
const address = reservation.address();
|
|
4777
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
4778
|
+
await new Promise((resolveClose, rejectClose) => {
|
|
4779
|
+
reservation.close((error) => error === void 0 ? resolveClose() : rejectClose(error));
|
|
4780
|
+
});
|
|
4781
|
+
if (port === 0) throw new Error("OS did not assign an ephemeral capture port");
|
|
4782
|
+
return port;
|
|
4783
|
+
}
|
|
4784
|
+
async function stopProcess(child) {
|
|
4785
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
4786
|
+
await new Promise((resolveStop) => {
|
|
4787
|
+
const timeout = setTimeout(() => {
|
|
4788
|
+
child.kill("SIGKILL");
|
|
4789
|
+
resolveStop();
|
|
4790
|
+
}, 2e3);
|
|
4791
|
+
child.once("exit", () => {
|
|
4792
|
+
clearTimeout(timeout);
|
|
4793
|
+
resolveStop();
|
|
4794
|
+
});
|
|
4795
|
+
child.kill("SIGTERM");
|
|
4796
|
+
});
|
|
4797
|
+
}
|
|
4798
|
+
async function startVirtualDisplay(width, height) {
|
|
4799
|
+
if (process.platform !== "linux" || process.env.DISPLAY !== void 0) {
|
|
4800
|
+
return {
|
|
4801
|
+
...process.env.DISPLAY === void 0 ? {} : { value: process.env.DISPLAY },
|
|
4802
|
+
async close() {
|
|
4803
|
+
}
|
|
4804
|
+
};
|
|
4805
|
+
}
|
|
4806
|
+
for (let offset = 0; offset < 100; offset += 1) {
|
|
4807
|
+
const number = 90 + (process.pid + offset) % 100;
|
|
4808
|
+
const socket = `/tmp/.X11-unix/X${number}`;
|
|
4809
|
+
const lock = `/tmp/.X${number}-lock`;
|
|
4810
|
+
if (await pathExists2(socket) || await pathExists2(lock)) continue;
|
|
4811
|
+
const display = `:${number}`;
|
|
4812
|
+
const child = spawn(
|
|
4813
|
+
"Xvfb",
|
|
4814
|
+
[display, "-screen", "0", `${width}x${height}x24`, "-nolisten", "tcp"],
|
|
4815
|
+
{ stdio: ["ignore", "ignore", "pipe"] }
|
|
4816
|
+
);
|
|
4817
|
+
let diagnostic = "";
|
|
4818
|
+
child.stderr?.setEncoding("utf8");
|
|
4819
|
+
child.stderr?.on("data", (chunk) => {
|
|
4820
|
+
diagnostic += chunk;
|
|
4821
|
+
});
|
|
4822
|
+
try {
|
|
4823
|
+
await new Promise((resolveReady, rejectReady) => {
|
|
4824
|
+
const timeout = setTimeout(
|
|
4825
|
+
() => rejectReady(new Error(`Xvfb did not create ${socket}: ${diagnostic.trim()}`)),
|
|
4826
|
+
5e3
|
|
4827
|
+
);
|
|
4828
|
+
const poll = setInterval(() => {
|
|
4829
|
+
void pathExists2(socket).then((exists) => {
|
|
4830
|
+
if (!exists) return;
|
|
4831
|
+
clearTimeout(timeout);
|
|
4832
|
+
clearInterval(poll);
|
|
4833
|
+
resolveReady();
|
|
4834
|
+
});
|
|
4835
|
+
}, 50);
|
|
4836
|
+
child.once("error", (error) => {
|
|
4837
|
+
clearTimeout(timeout);
|
|
4838
|
+
clearInterval(poll);
|
|
4839
|
+
rejectReady(error);
|
|
4840
|
+
});
|
|
4841
|
+
child.once("exit", (code, signal) => {
|
|
4842
|
+
clearTimeout(timeout);
|
|
4843
|
+
clearInterval(poll);
|
|
4844
|
+
rejectReady(
|
|
4845
|
+
new Error(`Xvfb exited before ready (${code ?? signal}): ${diagnostic.trim()}`)
|
|
4846
|
+
);
|
|
4847
|
+
});
|
|
4848
|
+
});
|
|
4849
|
+
return { value: display, close: () => stopProcess(child) };
|
|
4850
|
+
} catch (cause) {
|
|
4851
|
+
await stopProcess(child);
|
|
4852
|
+
throw cause;
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
throw new Error("no free X11 display number was available for browser capture");
|
|
4856
|
+
}
|
|
4857
|
+
async function lavapipeIcd() {
|
|
4858
|
+
for (const candidate of [
|
|
4859
|
+
"/usr/share/vulkan/icd.d/lvp_icd.x86_64.json",
|
|
4860
|
+
"/usr/share/vulkan/icd.d/lvp_icd.aarch64.json"
|
|
4861
|
+
]) {
|
|
4862
|
+
if (await pathExists2(candidate)) return candidate;
|
|
4863
|
+
}
|
|
4864
|
+
return void 0;
|
|
4865
|
+
}
|
|
4866
|
+
function evidencePath(output) {
|
|
4867
|
+
return output.toLowerCase().endsWith(".png") ? `${output.slice(0, -4)}.json` : `${output}.json`;
|
|
4868
|
+
}
|
|
4869
|
+
function newRunId() {
|
|
4870
|
+
return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
4871
|
+
}
|
|
4872
|
+
function checkpointSlug(checkpoint) {
|
|
4873
|
+
const value = (checkpoint ?? "capture").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
4874
|
+
return value.length === 0 ? "capture" : value;
|
|
4875
|
+
}
|
|
4876
|
+
function captureDigest(bytes) {
|
|
4877
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
4878
|
+
}
|
|
4879
|
+
function observedBackend(runtime) {
|
|
4880
|
+
if (runtime.adapter === null) return "unknown";
|
|
4881
|
+
return isSoftwareAdapter(runtime) ? "software" : "hardware";
|
|
4882
|
+
}
|
|
4883
|
+
async function resolveBrowserExecutable(requested) {
|
|
4884
|
+
if (requested !== void 0 && requested.length > 0) {
|
|
4885
|
+
return await pathExists2(requested) ? requested : void 0;
|
|
4886
|
+
}
|
|
4887
|
+
const candidates = [
|
|
4888
|
+
process.env.FORGEAX_BROWSER_EXECUTABLE,
|
|
4889
|
+
"/opt/google/chrome-beta/chrome",
|
|
4890
|
+
"/usr/bin/google-chrome",
|
|
4891
|
+
"/usr/bin/google-chrome-stable",
|
|
4892
|
+
"/usr/bin/chromium",
|
|
4893
|
+
"/usr/bin/chromium-browser",
|
|
4894
|
+
typeof chromium.executablePath === "function" ? chromium.executablePath() : void 0
|
|
4895
|
+
].filter((candidate) => candidate !== void 0 && candidate.length > 0);
|
|
4896
|
+
for (const candidate of candidates) {
|
|
4897
|
+
if (await pathExists2(candidate)) return candidate;
|
|
4898
|
+
}
|
|
4899
|
+
return void 0;
|
|
4900
|
+
}
|
|
4901
|
+
function browserLaunchArgs(backend, profile) {
|
|
4902
|
+
const common = ["--force-color-profile=srgb", "--force-device-scale-factor=1"];
|
|
4903
|
+
if (profile === "development") {
|
|
4904
|
+
common.unshift(
|
|
4905
|
+
"--enable-unsafe-webgpu",
|
|
4906
|
+
"--ignore-gpu-blocklist",
|
|
4907
|
+
"--disable-gpu-driver-bug-workarounds"
|
|
4908
|
+
);
|
|
4909
|
+
}
|
|
4910
|
+
if (backend !== "software") return common;
|
|
4911
|
+
return [
|
|
4912
|
+
...common,
|
|
4913
|
+
"--enable-features=Vulkan",
|
|
4914
|
+
"--use-vulkan=swiftshader",
|
|
4915
|
+
"--use-angle=swiftshader",
|
|
4916
|
+
"--disable-vulkan-surface"
|
|
4917
|
+
];
|
|
4918
|
+
}
|
|
4919
|
+
function summarizeCapturePixels(rgba, width, height) {
|
|
4920
|
+
const pixelCount = width * height;
|
|
4921
|
+
const stride = Math.max(1, Math.floor(pixelCount / 4096));
|
|
4922
|
+
const histogram = new Uint32Array(16);
|
|
4923
|
+
let lumaMin = 255;
|
|
4924
|
+
let lumaMax = 0;
|
|
4925
|
+
let sampledPixels = 0;
|
|
4926
|
+
for (let pixel = 0; pixel < pixelCount; pixel += stride) {
|
|
4927
|
+
const offset = pixel * 4;
|
|
4928
|
+
const red = rgba[offset];
|
|
4929
|
+
const green = rgba[offset + 1];
|
|
4930
|
+
const blue = rgba[offset + 2];
|
|
4931
|
+
if (red === void 0 || green === void 0 || blue === void 0) break;
|
|
4932
|
+
const luma = Math.round((54 * red + 183 * green + 19 * blue) / 256);
|
|
4933
|
+
lumaMin = Math.min(lumaMin, luma);
|
|
4934
|
+
lumaMax = Math.max(lumaMax, luma);
|
|
4935
|
+
const bucket = Math.min(15, Math.floor(luma / 16));
|
|
4936
|
+
histogram[bucket] = (histogram[bucket] ?? 0) + 1;
|
|
4937
|
+
sampledPixels += 1;
|
|
4938
|
+
}
|
|
4939
|
+
const dominantPixels = histogram.reduce((largest, count) => Math.max(largest, count), 0);
|
|
4940
|
+
const varyingPixels = sampledPixels - dominantPixels;
|
|
4941
|
+
const lumaRange = lumaMax - lumaMin;
|
|
4942
|
+
const requiredVariation = Math.min(sampledPixels, Math.max(8, Math.ceil(sampledPixels * 2e-3)));
|
|
4943
|
+
return {
|
|
4944
|
+
width,
|
|
4945
|
+
height,
|
|
4946
|
+
sampledPixels,
|
|
4947
|
+
lumaMin,
|
|
4948
|
+
lumaMax,
|
|
4949
|
+
lumaRange,
|
|
4950
|
+
varyingPixels,
|
|
4951
|
+
rendered: lumaRange >= 8 && varyingPixels >= requiredVariation
|
|
4952
|
+
};
|
|
4953
|
+
}
|
|
4954
|
+
function inspectCapturePng(png) {
|
|
4955
|
+
const decoded = parseImage(png, "image/png", { mipmap: false });
|
|
4956
|
+
if (!decoded.ok) throw decoded.error;
|
|
4957
|
+
return summarizeCapturePixels(decoded.value.bytes, decoded.value.width, decoded.value.height);
|
|
4958
|
+
}
|
|
4959
|
+
async function screenshotWithWitness(page) {
|
|
4960
|
+
const canvasPng = await page.locator("canvas").first().screenshot({
|
|
4961
|
+
type: "png",
|
|
4962
|
+
style: "* { visibility: hidden !important; } canvas { visibility: visible !important; }"
|
|
4963
|
+
});
|
|
4964
|
+
const png = await page.screenshot({ type: "png", caret: "hide" });
|
|
4965
|
+
return { png, pixels: inspectCapturePng(canvasPng) };
|
|
4966
|
+
}
|
|
4967
|
+
async function waitForCompositor(page) {
|
|
4968
|
+
await page.evaluate(async () => {
|
|
4969
|
+
await document.fonts.ready;
|
|
4970
|
+
await new Promise((resolveFrame) => requestAnimationFrame(() => resolveFrame()));
|
|
4971
|
+
await new Promise((resolveFrame) => requestAnimationFrame(() => resolveFrame()));
|
|
4972
|
+
});
|
|
4973
|
+
}
|
|
4974
|
+
async function runtimeWitness(page) {
|
|
4975
|
+
return page.evaluate(async (datasetKey) => {
|
|
4976
|
+
const canvas = document.querySelector("canvas");
|
|
4977
|
+
const uiRoot = document.querySelector("#game-ui");
|
|
4978
|
+
const shadowHosts = [...uiRoot?.querySelectorAll("*") ?? []].filter(
|
|
4979
|
+
(element) => element.shadowRoot?.childElementCount !== 0
|
|
4980
|
+
);
|
|
4981
|
+
let adapter = null;
|
|
4982
|
+
let adapterError = null;
|
|
4983
|
+
try {
|
|
4984
|
+
const gpuAdapter = await navigator.gpu?.requestAdapter();
|
|
4985
|
+
if (gpuAdapter === null || gpuAdapter === void 0) {
|
|
4986
|
+
adapterError = "navigator.gpu.requestAdapter() returned null";
|
|
4987
|
+
} else {
|
|
4988
|
+
const info = gpuAdapter.info;
|
|
4989
|
+
adapter = {
|
|
4990
|
+
vendor: info.vendor,
|
|
4991
|
+
architecture: info.architecture,
|
|
4992
|
+
device: info.device,
|
|
4993
|
+
description: info.description
|
|
4994
|
+
};
|
|
4995
|
+
}
|
|
4996
|
+
} catch (cause) {
|
|
4997
|
+
adapterError = String(cause);
|
|
4998
|
+
}
|
|
4999
|
+
const frameId = Number(document.documentElement.dataset[datasetKey]);
|
|
5000
|
+
const singleHtmlState = globalThis.__forgeaxSingleHtml;
|
|
5001
|
+
return {
|
|
5002
|
+
title: document.title,
|
|
5003
|
+
canvas: canvas instanceof HTMLCanvasElement ? { width: canvas.width, height: canvas.height } : null,
|
|
5004
|
+
domUi: {
|
|
5005
|
+
rootChildren: uiRoot?.childElementCount ?? 0,
|
|
5006
|
+
openShadowRoots: shadowHosts.length,
|
|
5007
|
+
textWitness: shadowHosts.map((host) => host.shadowRoot?.textContent?.replace(/\s+/g, " ").trim() ?? "").filter((text) => text.length > 0).join(" | ").slice(0, 500)
|
|
5008
|
+
},
|
|
5009
|
+
adapter,
|
|
5010
|
+
adapterError,
|
|
5011
|
+
engineFrameId: Number.isSafeInteger(frameId) && frameId > 0 ? frameId : null,
|
|
5012
|
+
captureReady: document.documentElement.dataset.forgeaxCaptureReady ?? null,
|
|
5013
|
+
singleHtml: singleHtmlState?.witness?.() ?? null,
|
|
5014
|
+
userAgent: navigator.userAgent
|
|
5015
|
+
};
|
|
5016
|
+
}, FORGEAX_FRAME_SUBMITTED_DATASET);
|
|
5017
|
+
}
|
|
5018
|
+
function isSoftwareAdapter(runtime) {
|
|
5019
|
+
const witness = Object.values(runtime.adapter ?? {}).map((value) => String(value).toLowerCase()).join(" ");
|
|
5020
|
+
return ["swiftshader", "llvmpipe", "lavapipe", "software"].some(
|
|
5021
|
+
(token) => witness.includes(token)
|
|
5022
|
+
);
|
|
5023
|
+
}
|
|
5024
|
+
async function writeRunReport(report) {
|
|
5025
|
+
await mkdir(dirname(report.report), { recursive: true });
|
|
5026
|
+
await writeFile(report.report, `${JSON.stringify(report, null, 2)}
|
|
5027
|
+
`, "utf8");
|
|
5028
|
+
}
|
|
5029
|
+
function requestProtocol(url) {
|
|
5030
|
+
try {
|
|
5031
|
+
return new URL(url).protocol;
|
|
5032
|
+
} catch {
|
|
5033
|
+
return void 0;
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
function requestRecord(request, target, candidateUrl) {
|
|
5037
|
+
const url = request.url();
|
|
5038
|
+
const protocol = requestProtocol(url);
|
|
5039
|
+
const requestDocument = url.split(/[?#]/, 1)[0];
|
|
5040
|
+
const candidateDocument = candidateUrl.split(/[?#]/, 1)[0];
|
|
5041
|
+
return {
|
|
5042
|
+
url,
|
|
5043
|
+
resourceType: request.resourceType(),
|
|
5044
|
+
status: null,
|
|
5045
|
+
failed: false,
|
|
5046
|
+
failure: null,
|
|
5047
|
+
resourceMiss: target.kind === "single-html" && protocol === "file:" && requestDocument !== candidateDocument
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
function summarizeRequests(records) {
|
|
5051
|
+
return {
|
|
5052
|
+
documents: records.filter((record) => record.resourceType === "document").length,
|
|
5053
|
+
http: records.filter((record) => requestProtocol(record.url) === "http:").length,
|
|
5054
|
+
https: records.filter((record) => requestProtocol(record.url) === "https:").length,
|
|
5055
|
+
failed: records.filter((record) => record.failed).length,
|
|
5056
|
+
resourceMisses: records.filter((record) => record.resourceMiss).length,
|
|
5057
|
+
entries: records.map((record) => ({ ...record }))
|
|
5058
|
+
};
|
|
5059
|
+
}
|
|
5060
|
+
function singleHtmlRuntimeClean(target, runtime, requests) {
|
|
5061
|
+
if (target.kind !== "single-html") return true;
|
|
5062
|
+
const singleHtml = runtime?.singleHtml;
|
|
5063
|
+
return singleHtml?.ready === true && singleHtml.resourceMisses.length === 0 && singleHtml.externalRequests.length === 0 && requests.http === 0 && requests.https === 0 && requests.failed === 0 && requests.resourceMisses === 0;
|
|
5064
|
+
}
|
|
5065
|
+
function markResponse(records, response) {
|
|
5066
|
+
const record = records.get(response.request());
|
|
5067
|
+
if (record === void 0) return;
|
|
5068
|
+
record.status = response.status();
|
|
5069
|
+
if (record.status >= 400) record.resourceMiss = true;
|
|
5070
|
+
}
|
|
5071
|
+
function markFailed(records, request) {
|
|
5072
|
+
const record = records.get(request);
|
|
5073
|
+
if (record === void 0) return;
|
|
5074
|
+
record.failed = true;
|
|
5075
|
+
record.failure = request.failure()?.errorText ?? "request failed";
|
|
5076
|
+
record.resourceMiss = true;
|
|
5077
|
+
}
|
|
5078
|
+
async function openBrowserCaptureSession(root, options) {
|
|
5079
|
+
const backend = options.backend ?? (options.software === true ? "software" : "auto");
|
|
5080
|
+
const launchProfile = options.launchProfile ?? "development";
|
|
5081
|
+
const requestedTarget = options.target ?? { kind: "project" };
|
|
5082
|
+
const target = requestedTarget.kind === "single-html" ? { kind: "single-html", path: resolve(requestedTarget.path) } : { kind: "project" };
|
|
5083
|
+
if (target.kind === "single-html") {
|
|
5084
|
+
if (!target.path.toLowerCase().endsWith(".html")) {
|
|
5085
|
+
fail(
|
|
5086
|
+
"browser-capture-target-invalid",
|
|
5087
|
+
"single-html target path to end with .html",
|
|
5088
|
+
"Pass the exact forgeax package --format single-html candidate.",
|
|
5089
|
+
{ path: target.path }
|
|
5090
|
+
);
|
|
5091
|
+
}
|
|
5092
|
+
try {
|
|
5093
|
+
const info = await stat(target.path);
|
|
5094
|
+
if (!info.isFile()) throw new Error("target is not a regular file");
|
|
5095
|
+
} catch (cause) {
|
|
5096
|
+
fail(
|
|
5097
|
+
"browser-capture-target-missing",
|
|
5098
|
+
"single-html target path to exist",
|
|
5099
|
+
"Package the game first, then pass its candidate HTML path.",
|
|
5100
|
+
{ path: target.path, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
5101
|
+
);
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
if (options.software === true && options.backend !== void 0 && options.backend !== "software") {
|
|
5105
|
+
fail(
|
|
5106
|
+
"browser-capture-option-conflict",
|
|
5107
|
+
"software and backend options to describe the same capture lane",
|
|
5108
|
+
"Use either software: true or backend: software; use backend: auto for a portable capture.",
|
|
5109
|
+
{ software: options.software, backend: options.backend }
|
|
5110
|
+
);
|
|
5111
|
+
}
|
|
5112
|
+
const facts = await readProjectFacts(root);
|
|
5113
|
+
if (!facts.ok) {
|
|
5114
|
+
throw new SoftwareCaptureError(
|
|
5115
|
+
facts.error.code,
|
|
5116
|
+
facts.error.expected,
|
|
5117
|
+
facts.error.hint,
|
|
5118
|
+
facts.error.detail
|
|
5119
|
+
);
|
|
5120
|
+
}
|
|
5121
|
+
const width = options.width ?? 1280;
|
|
5122
|
+
const height = options.height ?? 720;
|
|
5123
|
+
const browserPath = await resolveBrowserExecutable(options.browser);
|
|
5124
|
+
if (options.browser !== void 0 && browserPath === void 0) {
|
|
5125
|
+
fail(
|
|
5126
|
+
"browser-capture-browser-missing",
|
|
5127
|
+
`a runnable browser at ${options.browser}`,
|
|
5128
|
+
"Install Chrome/Chromium or pass --browser with an executable path.",
|
|
5129
|
+
{ browser: options.browser }
|
|
5130
|
+
);
|
|
3377
5131
|
}
|
|
3378
|
-
const
|
|
3379
|
-
const
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
5132
|
+
const id = options.runId ?? newRunId();
|
|
5133
|
+
const outputDirectory = resolve(
|
|
5134
|
+
facts.value.root,
|
|
5135
|
+
options.outputDir ?? `artifacts/playthrough/${id}`
|
|
5136
|
+
);
|
|
5137
|
+
const reportPath = resolve(
|
|
5138
|
+
facts.value.root,
|
|
5139
|
+
options.report ?? resolve(outputDirectory, "run.json")
|
|
5140
|
+
);
|
|
5141
|
+
const port = target.kind === "project" ? options.port === void 0 || options.port === 0 ? await reservePort() : options.port : void 0;
|
|
5142
|
+
let server;
|
|
5143
|
+
let browser;
|
|
5144
|
+
let page;
|
|
5145
|
+
let display;
|
|
3383
5146
|
try {
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
);
|
|
3401
|
-
const
|
|
3402
|
-
const
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
5147
|
+
if (target.kind === "project") {
|
|
5148
|
+
const { createServer: createServer3 } = await import('vite');
|
|
5149
|
+
if (port === void 0) throw new Error("project capture did not reserve a port");
|
|
5150
|
+
server = await createServer3(
|
|
5151
|
+
await createViteConfig(facts.value, "serve", "/", {
|
|
5152
|
+
server: { port, strictPort: true }
|
|
5153
|
+
})
|
|
5154
|
+
);
|
|
5155
|
+
await server.listen(port);
|
|
5156
|
+
}
|
|
5157
|
+
display = await startVirtualDisplay(width, height);
|
|
5158
|
+
const captureDisplay = display;
|
|
5159
|
+
const icd = await lavapipeIcd();
|
|
5160
|
+
const consoleErrors = [];
|
|
5161
|
+
const pageErrors = [];
|
|
5162
|
+
const requestRecords = [];
|
|
5163
|
+
const requestByObject = /* @__PURE__ */ new Map();
|
|
5164
|
+
const hasDisplay = display.value !== void 0;
|
|
5165
|
+
const headless = options.headless ?? (!hasDisplay && process.platform !== "linux");
|
|
5166
|
+
const baseUrl = target.kind === "project" ? new URL(server?.resolvedUrls?.local[0] ?? `http://127.0.0.1:${port ?? 0}/`) : void 0;
|
|
5167
|
+
const openPage = async (launchBackend) => {
|
|
5168
|
+
const browserEnvironment = {
|
|
5169
|
+
...Object.fromEntries(
|
|
5170
|
+
Object.entries(process.env).filter(
|
|
5171
|
+
(entry) => entry[1] !== void 0
|
|
5172
|
+
)
|
|
5173
|
+
),
|
|
5174
|
+
...launchBackend === "software" ? { LIBGL_ALWAYS_SOFTWARE: "1" } : {},
|
|
5175
|
+
...captureDisplay.value === void 0 ? {} : { DISPLAY: captureDisplay.value }
|
|
5176
|
+
};
|
|
5177
|
+
const launchOptions = {
|
|
5178
|
+
headless,
|
|
5179
|
+
env: browserEnvironment,
|
|
5180
|
+
args: browserLaunchArgs(launchBackend, launchProfile),
|
|
5181
|
+
...browserPath === void 0 ? {} : { executablePath: browserPath }
|
|
5182
|
+
};
|
|
5183
|
+
browser = await chromium.launch(launchOptions);
|
|
5184
|
+
page = await browser.newPage({
|
|
5185
|
+
viewport: { width, height },
|
|
5186
|
+
screen: { width, height },
|
|
5187
|
+
deviceScaleFactor: 1,
|
|
5188
|
+
colorScheme: "light",
|
|
5189
|
+
locale: "en-US",
|
|
5190
|
+
timezoneId: "UTC",
|
|
5191
|
+
serviceWorkers: "block"
|
|
3409
5192
|
});
|
|
5193
|
+
page.on("console", (message) => {
|
|
5194
|
+
if (message.type() === "error") consoleErrors.push(message.text());
|
|
5195
|
+
});
|
|
5196
|
+
page.on("pageerror", (error) => pageErrors.push(String(error)));
|
|
5197
|
+
const captureUrl2 = target.kind === "single-html" ? new URL(pathToFileURL(target.path).href) : new URL(baseUrl?.href ?? "http://127.0.0.1/");
|
|
5198
|
+
page.on("request", (request) => {
|
|
5199
|
+
const record = requestRecord(request, target, captureUrl2.href);
|
|
5200
|
+
requestRecords.push(record);
|
|
5201
|
+
requestByObject.set(request, record);
|
|
5202
|
+
});
|
|
5203
|
+
page.on("response", (response) => markResponse(requestByObject, response));
|
|
5204
|
+
page.on("requestfailed", (request) => markFailed(requestByObject, request));
|
|
5205
|
+
if (options.deterministic === true) captureUrl2.searchParams.set("forgeaxCapture", "1");
|
|
5206
|
+
await page.goto(captureUrl2.href, { waitUntil: "domcontentloaded", timeout: 12e4 });
|
|
5207
|
+
await page.waitForFunction(
|
|
5208
|
+
(requireUi) => {
|
|
5209
|
+
const canvas = document.querySelector("canvas");
|
|
5210
|
+
if (!(canvas instanceof HTMLCanvasElement) || canvas.width <= 0 || canvas.height <= 0)
|
|
5211
|
+
return false;
|
|
5212
|
+
if (!requireUi) return true;
|
|
5213
|
+
const uiRoot = document.querySelector("#game-ui");
|
|
5214
|
+
return uiRoot !== null && uiRoot.childElementCount > 0;
|
|
5215
|
+
},
|
|
5216
|
+
options.requireUi === true,
|
|
5217
|
+
{ timeout: 12e4 }
|
|
5218
|
+
);
|
|
5219
|
+
return runtimeWitness(page);
|
|
5220
|
+
};
|
|
5221
|
+
const initialLaunchBackend = backend === "software" ? "software" : "hardware";
|
|
5222
|
+
let runtime = await openPage(initialLaunchBackend);
|
|
5223
|
+
let observed = observedBackend(runtime);
|
|
5224
|
+
if (backend === "auto" && runtime.adapter === null) {
|
|
5225
|
+
await browser?.close();
|
|
5226
|
+
browser = void 0;
|
|
5227
|
+
page = void 0;
|
|
5228
|
+
consoleErrors.length = 0;
|
|
5229
|
+
pageErrors.length = 0;
|
|
5230
|
+
requestRecords.length = 0;
|
|
5231
|
+
requestByObject.clear();
|
|
5232
|
+
runtime = await openPage("software");
|
|
5233
|
+
observed = observedBackend(runtime);
|
|
3410
5234
|
}
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
5235
|
+
const captureUrl = target.kind === "single-html" ? new URL(pathToFileURL(target.path).href) : new URL(baseUrl?.href ?? "http://127.0.0.1/");
|
|
5236
|
+
if (options.deterministic === true) captureUrl.searchParams.set("forgeaxCapture", "1");
|
|
5237
|
+
if (page === void 0 || browser === void 0) {
|
|
5238
|
+
fail(
|
|
5239
|
+
"browser-capture-browser-unavailable",
|
|
5240
|
+
"the browser capture session to open",
|
|
5241
|
+
"Inspect the browser launch diagnostic and retry with --browser or --headless."
|
|
5242
|
+
);
|
|
3418
5243
|
}
|
|
5244
|
+
const captures = [];
|
|
5245
|
+
let latestRuntime = runtime;
|
|
5246
|
+
let closed = false;
|
|
5247
|
+
const report = {
|
|
5248
|
+
schemaVersion: "2.0.0",
|
|
5249
|
+
runId: id,
|
|
5250
|
+
ok: false,
|
|
5251
|
+
mode: "browser-compositor",
|
|
5252
|
+
root: facts.value.root,
|
|
5253
|
+
url: captureUrl.href,
|
|
5254
|
+
target,
|
|
5255
|
+
launchProfile,
|
|
5256
|
+
report: reportPath,
|
|
5257
|
+
backendRequested: backend,
|
|
5258
|
+
backend: observed,
|
|
5259
|
+
softwareRequested: backend === "software",
|
|
5260
|
+
deterministicRequested: options.deterministic === true,
|
|
5261
|
+
viewport: {
|
|
5262
|
+
width,
|
|
5263
|
+
height,
|
|
5264
|
+
deviceScaleFactor: 1,
|
|
5265
|
+
colorProfile: "srgb",
|
|
5266
|
+
colorScheme: "light",
|
|
5267
|
+
locale: "en-US",
|
|
5268
|
+
timezone: "UTC"
|
|
5269
|
+
},
|
|
5270
|
+
browser: { version: browser.version(), executable: browserPath ?? "playwright-managed" },
|
|
5271
|
+
display: display.value ?? null,
|
|
5272
|
+
lavapipeIcd: icd ?? null,
|
|
5273
|
+
captures,
|
|
5274
|
+
requests: summarizeRequests(requestRecords),
|
|
5275
|
+
singleHtml: runtime.singleHtml,
|
|
5276
|
+
consoleErrors,
|
|
5277
|
+
pageErrors,
|
|
5278
|
+
boundary: "Browser-compositor capture is visual iteration evidence, not physical-GPU performance, HDR-display output, or release acceptance."
|
|
5279
|
+
};
|
|
5280
|
+
await writeRunReport(report);
|
|
5281
|
+
const updateReport = async () => {
|
|
5282
|
+
const requests = summarizeRequests(requestRecords);
|
|
5283
|
+
Object.assign(report, {
|
|
5284
|
+
requests,
|
|
5285
|
+
singleHtml: latestRuntime?.singleHtml ?? null,
|
|
5286
|
+
ok: captures.length > 0 && captures.every((capture) => capture.ok) && consoleErrors.length === 0 && pageErrors.length === 0 && singleHtmlRuntimeClean(target, latestRuntime, requests)
|
|
5287
|
+
});
|
|
5288
|
+
await writeRunReport(report);
|
|
5289
|
+
};
|
|
5290
|
+
const close = async () => {
|
|
5291
|
+
if (closed) return;
|
|
5292
|
+
closed = true;
|
|
5293
|
+
await Promise.allSettled([browser?.close(), display?.close(), server?.close()]);
|
|
5294
|
+
Object.assign(report, { closedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5295
|
+
await updateReport();
|
|
5296
|
+
};
|
|
3419
5297
|
return {
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
5298
|
+
page,
|
|
5299
|
+
url: captureUrl.href,
|
|
5300
|
+
reportPath,
|
|
5301
|
+
async capture(checkpoint, captureOptions = {}) {
|
|
5302
|
+
if (closed) {
|
|
5303
|
+
fail(
|
|
5304
|
+
"browser-capture-session-closed",
|
|
5305
|
+
"capture to run inside an open browser session",
|
|
5306
|
+
"Open one session, complete all checkpoints, then close it.",
|
|
5307
|
+
{ report: reportPath }
|
|
5308
|
+
);
|
|
5309
|
+
}
|
|
5310
|
+
const activePage = page;
|
|
5311
|
+
if (activePage === void 0) {
|
|
5312
|
+
fail(
|
|
5313
|
+
"browser-capture-browser-unavailable",
|
|
5314
|
+
"an open Playwright page",
|
|
5315
|
+
"Inspect the browser launch diagnostic and retry the capture."
|
|
5316
|
+
);
|
|
5317
|
+
}
|
|
5318
|
+
const expectedReady = checkpoint ?? (options.deterministic === true ? "true" : void 0);
|
|
5319
|
+
await activePage.waitForFunction(
|
|
5320
|
+
({ datasetKey }) => Number(document.documentElement.dataset[datasetKey]) > 0,
|
|
5321
|
+
{ datasetKey: FORGEAX_FRAME_SUBMITTED_DATASET },
|
|
5322
|
+
{ timeout: 12e4 }
|
|
5323
|
+
);
|
|
5324
|
+
if (expectedReady !== void 0) {
|
|
5325
|
+
await activePage.waitForFunction(
|
|
5326
|
+
(expected) => document.documentElement.dataset.forgeaxCaptureReady === expected,
|
|
5327
|
+
expectedReady,
|
|
5328
|
+
{ timeout: 12e4 }
|
|
5329
|
+
);
|
|
5330
|
+
}
|
|
5331
|
+
await waitForCompositor(activePage);
|
|
5332
|
+
let captured = await screenshotWithWitness(activePage);
|
|
5333
|
+
const deadline = Date.now() + 12e4;
|
|
5334
|
+
while (!captured.pixels.rendered && Date.now() < deadline) {
|
|
5335
|
+
await activePage.waitForTimeout(500);
|
|
5336
|
+
await waitForCompositor(activePage);
|
|
5337
|
+
captured = await screenshotWithWitness(activePage);
|
|
5338
|
+
}
|
|
5339
|
+
const waitMs = captureOptions.waitMs ?? 0;
|
|
5340
|
+
if (waitMs > 0) {
|
|
5341
|
+
await activePage.waitForTimeout(waitMs);
|
|
5342
|
+
await waitForCompositor(activePage);
|
|
5343
|
+
captured = await screenshotWithWitness(activePage);
|
|
5344
|
+
while (!captured.pixels.rendered && Date.now() < deadline) {
|
|
5345
|
+
await activePage.waitForTimeout(500);
|
|
5346
|
+
await waitForCompositor(activePage);
|
|
5347
|
+
captured = await screenshotWithWitness(activePage);
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
const runtime2 = await runtimeWitness(activePage);
|
|
5351
|
+
latestRuntime = runtime2;
|
|
5352
|
+
const requestSummary = summarizeRequests(requestRecords);
|
|
5353
|
+
const uiPresent = runtime2.domUi.rootChildren > 0;
|
|
5354
|
+
const requireUi = captureOptions.requireUi ?? options.requireUi ?? false;
|
|
5355
|
+
const captureBackend = observedBackend(runtime2);
|
|
5356
|
+
if (captureBackend !== "unknown") Object.assign(report, { backend: captureBackend });
|
|
5357
|
+
const backendMatches = backend === "auto" || captureBackend === backend;
|
|
5358
|
+
const ok2 = runtime2.canvas !== null && captured.pixels.rendered && backendMatches && runtime2.engineFrameId !== null && consoleErrors.length === 0 && pageErrors.length === 0 && singleHtmlRuntimeClean(target, runtime2, requestSummary) && (expectedReady === void 0 || runtime2.captureReady === expectedReady) && (!requireUi || uiPresent);
|
|
5359
|
+
const index = captures.length + 1;
|
|
5360
|
+
const output = resolve(
|
|
5361
|
+
facts.value.root,
|
|
5362
|
+
captureOptions.output ?? resolve(
|
|
5363
|
+
outputDirectory,
|
|
5364
|
+
`${String(index).padStart(3, "0")}-${checkpointSlug(checkpoint)}.png`
|
|
5365
|
+
)
|
|
5366
|
+
);
|
|
5367
|
+
await mkdir(dirname(output), { recursive: true });
|
|
5368
|
+
await writeFile(output, captured.png);
|
|
5369
|
+
const record = {
|
|
5370
|
+
index,
|
|
5371
|
+
checkpoint: checkpoint ?? null,
|
|
5372
|
+
ok: ok2,
|
|
5373
|
+
output,
|
|
5374
|
+
digest: captureDigest(captured.png),
|
|
5375
|
+
pixels: captured.pixels,
|
|
5376
|
+
runtime: runtime2
|
|
5377
|
+
};
|
|
5378
|
+
captures.push(record);
|
|
5379
|
+
await updateReport();
|
|
5380
|
+
if (!ok2) {
|
|
5381
|
+
fail(
|
|
5382
|
+
`${backend === "software" ? "software" : "browser"}-capture-runtime-failed`,
|
|
5383
|
+
"non-flat canvas pixels, the requested browser backend/checkpoint, and no browser errors",
|
|
5384
|
+
"Inspect the run report and repair the first browser runtime failure.",
|
|
5385
|
+
{ checkpoint: checkpoint ?? null, output, report: reportPath }
|
|
5386
|
+
);
|
|
5387
|
+
}
|
|
5388
|
+
return record;
|
|
5389
|
+
},
|
|
5390
|
+
report: () => report,
|
|
5391
|
+
close
|
|
3427
5392
|
};
|
|
3428
5393
|
} catch (cause) {
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
if (staging !== void 0) await rm(staging, { recursive: true, force: true });
|
|
3432
|
-
if (download !== void 0) await rm(download, { recursive: true, force: true });
|
|
5394
|
+
await Promise.allSettled([browser?.close(), display?.close(), server?.close()]);
|
|
5395
|
+
throw cause;
|
|
3433
5396
|
}
|
|
3434
5397
|
}
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
if (target !== void 0) {
|
|
3447
|
-
try {
|
|
3448
|
-
await readFile(target);
|
|
3449
|
-
} catch (cause) {
|
|
3450
|
-
return { ok: false, error: commandError(cause, "shader-source-unreadable") };
|
|
5398
|
+
function createBrowserCapture(root) {
|
|
5399
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
5400
|
+
return {
|
|
5401
|
+
async open(options) {
|
|
5402
|
+
const session = await openBrowserCaptureSession(root, options);
|
|
5403
|
+
sessions.add(session);
|
|
5404
|
+
return session;
|
|
5405
|
+
},
|
|
5406
|
+
async close() {
|
|
5407
|
+
await Promise.allSettled([...sessions].map((session) => session.close()));
|
|
5408
|
+
sessions.clear();
|
|
3451
5409
|
}
|
|
5410
|
+
};
|
|
5411
|
+
}
|
|
5412
|
+
function captureCommandError(cause, legacySoftware = false) {
|
|
5413
|
+
if (cause instanceof SoftwareCaptureError) {
|
|
5414
|
+
const code = legacySoftware && cause.code.startsWith("browser-capture-") ? cause.code.replace(/^browser-capture-/, "software-capture-") : cause.code;
|
|
5415
|
+
return {
|
|
5416
|
+
code,
|
|
5417
|
+
expected: cause.expected,
|
|
5418
|
+
hint: cause.hint,
|
|
5419
|
+
detail: cause.detail
|
|
5420
|
+
};
|
|
3452
5421
|
}
|
|
3453
|
-
|
|
3454
|
-
|
|
5422
|
+
return commandError(cause, legacySoftware ? "software-capture-failed" : "browser-capture-failed");
|
|
5423
|
+
}
|
|
5424
|
+
async function browserCaptureCommand(options) {
|
|
5425
|
+
const root = options.root ?? process.cwd();
|
|
5426
|
+
const output = resolve(root, options.output ?? "artifacts/capture/game-ui.png");
|
|
5427
|
+
const browser = createBrowserCapture(root);
|
|
3455
5428
|
try {
|
|
3456
|
-
|
|
3457
|
-
const
|
|
3458
|
-
|
|
3459
|
-
...
|
|
3460
|
-
|
|
5429
|
+
const backend = options.backend ?? (options.software === true ? "software" : "auto");
|
|
5430
|
+
const session = await browser.open({
|
|
5431
|
+
backend,
|
|
5432
|
+
...backend === "software" ? { software: true } : {},
|
|
5433
|
+
...options.browser === void 0 ? {} : { browser: options.browser },
|
|
5434
|
+
...options.width === void 0 ? {} : { width: options.width },
|
|
5435
|
+
...options.height === void 0 ? {} : { height: options.height },
|
|
5436
|
+
...options.port === void 0 ? {} : { port: options.port },
|
|
5437
|
+
...options.headless === void 0 ? {} : { headless: options.headless },
|
|
5438
|
+
requireUi: options.requireUi === true,
|
|
5439
|
+
deterministic: options.deterministic === true,
|
|
5440
|
+
outputDir: dirname(output),
|
|
5441
|
+
report: evidencePath(output)
|
|
3461
5442
|
});
|
|
3462
|
-
|
|
5443
|
+
await session.capture(options.deterministic === true ? "true" : void 0, {
|
|
5444
|
+
output,
|
|
5445
|
+
waitMs: options.waitMs ?? 4e3,
|
|
5446
|
+
requireUi: options.requireUi === true
|
|
5447
|
+
});
|
|
5448
|
+
await session.close();
|
|
5449
|
+
return { ok: true, value: session.report() };
|
|
3463
5450
|
} catch (cause) {
|
|
3464
|
-
return { ok: false, error:
|
|
5451
|
+
return { ok: false, error: captureCommandError(cause) };
|
|
3465
5452
|
} finally {
|
|
3466
|
-
|
|
3467
|
-
await rm(temporary, { recursive: true, force: true });
|
|
5453
|
+
await browser.close();
|
|
3468
5454
|
}
|
|
3469
5455
|
}
|
|
3470
|
-
|
|
3471
|
-
"
|
|
5456
|
+
async function softwareCaptureCommand(options) {
|
|
5457
|
+
const result = await browserCaptureCommand({ ...options, backend: "software", software: true });
|
|
5458
|
+
if (result.ok) return result;
|
|
5459
|
+
return { ok: false, error: captureCommandErrorFromResult(result.error) };
|
|
5460
|
+
}
|
|
5461
|
+
function captureCommandErrorFromResult(error, legacySoftware) {
|
|
5462
|
+
if (!error.code.startsWith("browser-capture-")) return error;
|
|
5463
|
+
return { ...error, code: error.code.replace(/^browser-capture-/, "software-capture-") };
|
|
5464
|
+
}
|
|
5465
|
+
var SoftwareCaptureError;
|
|
5466
|
+
var init_software_capture = __esm({
|
|
5467
|
+
"src/software-capture.ts"() {
|
|
3472
5468
|
init_host();
|
|
3473
5469
|
init_project();
|
|
5470
|
+
SoftwareCaptureError = class extends Error {
|
|
5471
|
+
constructor(code, expected, hint, detail) {
|
|
5472
|
+
super(`${code}: ${hint}`);
|
|
5473
|
+
this.code = code;
|
|
5474
|
+
this.expected = expected;
|
|
5475
|
+
this.hint = hint;
|
|
5476
|
+
this.detail = detail;
|
|
5477
|
+
this.name = "SoftwareCaptureError";
|
|
5478
|
+
}
|
|
5479
|
+
code;
|
|
5480
|
+
expected;
|
|
5481
|
+
hint;
|
|
5482
|
+
detail;
|
|
5483
|
+
};
|
|
3474
5484
|
}
|
|
3475
5485
|
});
|
|
3476
5486
|
|
|
@@ -3482,12 +5492,17 @@ __export(commands_exports, {
|
|
|
3482
5492
|
assetInspectCommand: () => assetInspectCommand,
|
|
3483
5493
|
assetListCommand: () => assetListCommand,
|
|
3484
5494
|
assetVerifyCommand: () => assetVerifyCommand,
|
|
5495
|
+
browserCaptureCommand: () => browserCaptureCommand,
|
|
3485
5496
|
buildCommand: () => buildCommand,
|
|
3486
5497
|
createCliRhiDebugOperationContext: () => createCliRhiDebugOperationContext,
|
|
3487
5498
|
createRhiDebugOperationContext: () => createRhiDebugOperationContext,
|
|
3488
5499
|
devCommand: () => devCommand,
|
|
3489
5500
|
discoverRhiDebugOperations: () => discoverRhiDebugOperations,
|
|
3490
5501
|
doctorCommand: () => doctorCommand,
|
|
5502
|
+
engineDoctorCommand: () => engineDoctorCommand,
|
|
5503
|
+
engineStatusCommand: () => engineStatusCommand,
|
|
5504
|
+
engineUnlinkCommand: () => engineUnlinkCommand,
|
|
5505
|
+
engineUseLocalCommand: () => engineUseLocalCommand,
|
|
3491
5506
|
initCommand: () => initCommand,
|
|
3492
5507
|
newCommand: () => newCommand,
|
|
3493
5508
|
packageCommand: () => packageCommand,
|
|
@@ -3502,6 +5517,7 @@ __export(commands_exports, {
|
|
|
3502
5517
|
shaderCheckCommand: () => shaderCheckCommand,
|
|
3503
5518
|
skillInstallCommand: () => skillInstallCommand,
|
|
3504
5519
|
skillVerifyCommand: () => skillVerifyCommand,
|
|
5520
|
+
softwareCaptureCommand: () => softwareCaptureCommand,
|
|
3505
5521
|
testCommand: () => testCommand
|
|
3506
5522
|
});
|
|
3507
5523
|
function runRhiDebugCommand(name, input, context) {
|
|
@@ -3545,31 +5561,47 @@ function releaseSlug(name) {
|
|
|
3545
5561
|
async function packageCommand(options = {}) {
|
|
3546
5562
|
const facts = await readProjectFacts(options.root);
|
|
3547
5563
|
if (!facts.ok) return facts;
|
|
5564
|
+
const format = options.format ?? "web-zip";
|
|
5565
|
+
if (format !== "web-zip" && format !== "single-html") return packageFormatError(String(format));
|
|
5566
|
+
const defaultOutput = format === "single-html" ? `release/${releaseSlug(facts.value.name)}-offline.html` : `release/${releaseSlug(facts.value.name)}-web.zip`;
|
|
5567
|
+
const output = resolve(facts.value.root, options.output ?? defaultOutput);
|
|
5568
|
+
const expectedSuffix = format === "single-html" ? ".html" : ".zip";
|
|
5569
|
+
if (!output.toLowerCase().endsWith(expectedSuffix)) {
|
|
5570
|
+
return packageOutputError(output, format);
|
|
5571
|
+
}
|
|
5572
|
+
const distRoot = resolve(facts.value.root, "dist");
|
|
5573
|
+
const outputRelativeToDist = relative(distRoot, output);
|
|
5574
|
+
if (outputRelativeToDist === "" || !outputRelativeToDist.startsWith("..") && outputRelativeToDist !== "..") {
|
|
5575
|
+
return {
|
|
5576
|
+
ok: false,
|
|
5577
|
+
error: {
|
|
5578
|
+
code: "release-output-inside-dist",
|
|
5579
|
+
expected: "the release artifact to live outside the derived dist directory",
|
|
5580
|
+
hint: "Use --output release/<game>-web.zip or release/<game>-offline.html.",
|
|
5581
|
+
detail: { output, distRoot, format }
|
|
5582
|
+
}
|
|
5583
|
+
};
|
|
5584
|
+
}
|
|
3548
5585
|
const built = await buildCommand({
|
|
3549
5586
|
root: facts.value.root,
|
|
3550
5587
|
base: "./",
|
|
3551
5588
|
...options.json === void 0 ? {} : { json: options.json }
|
|
3552
5589
|
});
|
|
3553
5590
|
if (!built.ok) return built;
|
|
3554
|
-
const distRoot = resolve(facts.value.root, "dist");
|
|
3555
5591
|
const verified = await verifyDist(distRoot);
|
|
3556
5592
|
if (!verified.ok) return verified;
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
expected: "the release archive to live outside the derived dist directory",
|
|
3568
|
-
hint: "Use --output release/<game>-web.zip or another path outside dist.",
|
|
3569
|
-
detail: { archive, distRoot }
|
|
3570
|
-
}
|
|
3571
|
-
};
|
|
5593
|
+
if (format === "single-html") {
|
|
5594
|
+
const indexHtml = await readFile(resolve(distRoot, "index.html"), "utf8");
|
|
5595
|
+
const bundle = await bundleSingleHtmlEntry(distRoot, indexHtml, facts.value.root);
|
|
5596
|
+
if (!bundle.ok) return bundle;
|
|
5597
|
+
return writeSingleHtml({
|
|
5598
|
+
distRoot,
|
|
5599
|
+
output,
|
|
5600
|
+
manifest: verified.value,
|
|
5601
|
+
bundle: bundle.value
|
|
5602
|
+
});
|
|
3572
5603
|
}
|
|
5604
|
+
const archive = output;
|
|
3573
5605
|
const temporaryArchive = `${archive}.partial-${process.pid}`;
|
|
3574
5606
|
const checksumPath = `${archive}.sha256`;
|
|
3575
5607
|
const temporaryChecksum = `${checksumPath}.partial-${process.pid}`;
|
|
@@ -3582,11 +5614,11 @@ async function packageCommand(options = {}) {
|
|
|
3582
5614
|
paths.map(async (path) => ({ path, bytes: await readFile(resolve(distRoot, path)) }))
|
|
3583
5615
|
);
|
|
3584
5616
|
const bytes = createZip(entries2);
|
|
3585
|
-
const
|
|
5617
|
+
const sha2562 = createHash("sha256").update(bytes).digest("hex");
|
|
3586
5618
|
const manifestBytes = await readFile(resolve(distRoot, "forgeax-dist.json"));
|
|
3587
5619
|
await mkdir(dirname(archive), { recursive: true });
|
|
3588
5620
|
await writeFile(temporaryArchive, bytes);
|
|
3589
|
-
await writeFile(temporaryChecksum, `${
|
|
5621
|
+
await writeFile(temporaryChecksum, `${sha2562} ${basename(archive)}
|
|
3590
5622
|
`);
|
|
3591
5623
|
await rename(temporaryChecksum, checksumPath);
|
|
3592
5624
|
await rename(temporaryArchive, archive);
|
|
@@ -3602,7 +5634,7 @@ async function packageCommand(options = {}) {
|
|
|
3602
5634
|
delivery: "bundled-runtime",
|
|
3603
5635
|
wasm: verified.value.artifacts.some((artifact) => artifact.path.endsWith(".wasm"))
|
|
3604
5636
|
},
|
|
3605
|
-
archive: { path: archive, bytes: bytes.byteLength, sha256 },
|
|
5637
|
+
archive: { path: archive, bytes: bytes.byteLength, sha256: sha2562 },
|
|
3606
5638
|
checksumPath,
|
|
3607
5639
|
distManifestSha256: createHash("sha256").update(manifestBytes).digest("hex"),
|
|
3608
5640
|
run: {
|
|
@@ -3626,12 +5658,12 @@ async function devCommand(options = {}) {
|
|
|
3626
5658
|
const previous = process.cwd();
|
|
3627
5659
|
let server;
|
|
3628
5660
|
try {
|
|
3629
|
-
const { createServer:
|
|
5661
|
+
const { createServer: createServer3 } = await import('vite');
|
|
3630
5662
|
const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_host(), host_exports));
|
|
3631
5663
|
const port = resolveProjectPort(options.port);
|
|
3632
5664
|
const vitePort = { ...port, port: await materializeViteDevPort(port.port) };
|
|
3633
5665
|
process.chdir(facts.value.root);
|
|
3634
|
-
server = await
|
|
5666
|
+
server = await createServer3(
|
|
3635
5667
|
await createViteConfig2(facts.value, "serve", "/", {
|
|
3636
5668
|
server: vitePort
|
|
3637
5669
|
})
|
|
@@ -3739,18 +5771,34 @@ var init_commands = __esm({
|
|
|
3739
5771
|
init_dist();
|
|
3740
5772
|
init_host();
|
|
3741
5773
|
init_project();
|
|
5774
|
+
init_single_html();
|
|
3742
5775
|
init_types();
|
|
3743
5776
|
init_assets();
|
|
3744
5777
|
init_bootstrap_commands();
|
|
5778
|
+
init_engine_binding();
|
|
3745
5779
|
init_plugin_authoring();
|
|
3746
5780
|
init_cli_context();
|
|
3747
5781
|
init_operations();
|
|
3748
5782
|
init_sdk_install();
|
|
3749
5783
|
init_shader_check();
|
|
3750
5784
|
init_skill_install();
|
|
5785
|
+
init_software_capture();
|
|
3751
5786
|
init_operations();
|
|
3752
5787
|
}
|
|
3753
5788
|
});
|
|
5789
|
+
function createNativePreviewCatalog(host, selectedName) {
|
|
5790
|
+
return new Map(
|
|
5791
|
+
nativePreviewPlugins.filter(([name]) => selectedName === void 0 || name === selectedName).map(([name, plugin]) => [
|
|
5792
|
+
name,
|
|
5793
|
+
{
|
|
5794
|
+
realm: "host",
|
|
5795
|
+
load: async () => ({
|
|
5796
|
+
default: bindPreviewHost(plugin, host)
|
|
5797
|
+
})
|
|
5798
|
+
}
|
|
5799
|
+
])
|
|
5800
|
+
);
|
|
5801
|
+
}
|
|
3754
5802
|
var nativePreviewPlugins, nativePreviewTools, nativePreviewDescriptors;
|
|
3755
5803
|
var init_preview_catalog = __esm({
|
|
3756
5804
|
"src/tools/preview-catalog.ts"() {
|
|
@@ -4225,11 +6273,11 @@ async function runProjectTool(binding, args2, options) {
|
|
|
4225
6273
|
);
|
|
4226
6274
|
await loader.root.update(entries2);
|
|
4227
6275
|
await loader.await();
|
|
4228
|
-
const { createToolRuntime:
|
|
4229
|
-
const { createContextCapabilityResolver } = await import('@forgeax/engine-plugin');
|
|
4230
|
-
return await
|
|
6276
|
+
const { createToolRuntime: createToolRuntime5 } = await import('@forgeax/engine-tool-runtime');
|
|
6277
|
+
const { createContextCapabilityResolver: createContextCapabilityResolver2 } = await import('@forgeax/engine-plugin');
|
|
6278
|
+
return await createToolRuntime5([contribution]).run(contribution, args2, {
|
|
4231
6279
|
...options,
|
|
4232
|
-
capabilityResolver:
|
|
6280
|
+
capabilityResolver: createContextCapabilityResolver2(ctx)
|
|
4233
6281
|
}).terminal;
|
|
4234
6282
|
} catch (cause) {
|
|
4235
6283
|
return activationFailure(cause);
|
|
@@ -4241,6 +6289,50 @@ var init_project_tools = __esm({
|
|
|
4241
6289
|
"src/tools/project-tools.ts"() {
|
|
4242
6290
|
}
|
|
4243
6291
|
});
|
|
6292
|
+
|
|
6293
|
+
// src/tools/contributions.ts
|
|
6294
|
+
var contributions_exports = {};
|
|
6295
|
+
__export(contributions_exports, {
|
|
6296
|
+
createAuthorContribution: () => createAuthorContribution,
|
|
6297
|
+
createBuildContribution: () => createBuildContribution,
|
|
6298
|
+
createDefaultContributions: () => createDefaultContributions
|
|
6299
|
+
});
|
|
6300
|
+
function commandFailure2(error) {
|
|
6301
|
+
return { ok: false, error: { ...error, detail: error.detail } };
|
|
6302
|
+
}
|
|
6303
|
+
function createBuildContribution(projectRoot2 = process.cwd()) {
|
|
6304
|
+
return defineTool(
|
|
6305
|
+
projectBuildDescriptor,
|
|
6306
|
+
async (options) => {
|
|
6307
|
+
const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
6308
|
+
const result = await buildCommand2({ ...options, root: options.root ?? projectRoot2 });
|
|
6309
|
+
return result.ok ? result.value : commandFailure2(result.error);
|
|
6310
|
+
}
|
|
6311
|
+
);
|
|
6312
|
+
}
|
|
6313
|
+
function createAuthorContribution(projectRoot2 = process.cwd()) {
|
|
6314
|
+
return defineTool(
|
|
6315
|
+
authorPluginInstallDescriptor,
|
|
6316
|
+
async (options) => {
|
|
6317
|
+
const { pluginInstallCommand: pluginInstallCommand2 } = await Promise.resolve().then(() => (init_plugin_authoring(), plugin_authoring_exports));
|
|
6318
|
+
const result = await pluginInstallCommand2({ ...options, root: options.root ?? projectRoot2 });
|
|
6319
|
+
return result.ok ? result.value : commandFailure2(result.error);
|
|
6320
|
+
}
|
|
6321
|
+
);
|
|
6322
|
+
}
|
|
6323
|
+
function createDefaultContributions(projectRoot2 = process.cwd()) {
|
|
6324
|
+
return [
|
|
6325
|
+
createBuildContribution(projectRoot2),
|
|
6326
|
+
createAuthorContribution(projectRoot2),
|
|
6327
|
+
...nativePreviewTools
|
|
6328
|
+
];
|
|
6329
|
+
}
|
|
6330
|
+
var init_contributions = __esm({
|
|
6331
|
+
"src/tools/contributions.ts"() {
|
|
6332
|
+
init_catalog();
|
|
6333
|
+
init_preview_catalog();
|
|
6334
|
+
}
|
|
6335
|
+
});
|
|
4244
6336
|
function analyzePreviewArtifacts(request) {
|
|
4245
6337
|
const required = request.required ?? [];
|
|
4246
6338
|
const validated = request.manifest.schemaVersion === "1.0.0" ? required.some((kind) => kind !== "rhi-tape" && kind !== "png" && kind !== "profile-capture") ? {
|
|
@@ -4251,7 +6343,7 @@ function analyzePreviewArtifacts(request) {
|
|
|
4251
6343
|
hint: "Request one of rhi-tape, png, or profile-capture for a v1 manifest.",
|
|
4252
6344
|
detail: { reason: "unsupported required evidence kind" }
|
|
4253
6345
|
}
|
|
4254
|
-
} : validateArtifactManifest(request.manifest, required) : validatePreviewArtifactManifest(
|
|
6346
|
+
} : validateArtifactManifest(request.manifest, required) : validatePreviewArtifactManifest$1(
|
|
4255
6347
|
request.manifest,
|
|
4256
6348
|
(request.required ?? []).filter(
|
|
4257
6349
|
(role) => role === "report" || role === "rhi-tape" || role === "capture" || role === "fresh-replay" || role === "profile-capture" || role === "contact-sheet"
|
|
@@ -4277,77 +6369,25 @@ function createPreviewContribution() {
|
|
|
4277
6369
|
error: retiredPreviewOperationFailure
|
|
4278
6370
|
}));
|
|
4279
6371
|
}
|
|
4280
|
-
function createOfflineAnalysisContribution() {
|
|
4281
|
-
return defineTool(
|
|
4282
|
-
{
|
|
4283
|
-
...previewOfflineAnalysisDescriptor
|
|
4284
|
-
},
|
|
4285
|
-
(request) => analyzePreviewArtifacts(request)
|
|
4286
|
-
);
|
|
4287
|
-
}
|
|
4288
|
-
function createPreviewContributions() {
|
|
4289
|
-
return [
|
|
4290
|
-
createPreviewContribution(),
|
|
4291
|
-
createOfflineAnalysisContribution()
|
|
4292
|
-
];
|
|
4293
|
-
}
|
|
4294
|
-
function createDomainPreviewContributions() {
|
|
4295
|
-
return [
|
|
4296
|
-
createMaterialPreviewContribution(),
|
|
4297
|
-
createMeshPreviewContribution(),
|
|
4298
|
-
createVfxPreviewContribution(),
|
|
4299
|
-
createTexturePreviewContribution()
|
|
4300
|
-
];
|
|
4301
|
-
}
|
|
4302
|
-
var init_preview_contributions = __esm({
|
|
4303
|
-
"src/tools/preview-contributions.ts"() {
|
|
4304
|
-
init_catalog();
|
|
4305
|
-
init_offline_analysis();
|
|
4306
|
-
init_preview_migration();
|
|
4307
|
-
}
|
|
4308
|
-
});
|
|
4309
|
-
|
|
4310
|
-
// src/tools/contributions.ts
|
|
4311
|
-
var contributions_exports = {};
|
|
4312
|
-
__export(contributions_exports, {
|
|
4313
|
-
createAuthorContribution: () => createAuthorContribution,
|
|
4314
|
-
createBuildContribution: () => createBuildContribution,
|
|
4315
|
-
createDefaultContributions: () => createDefaultContributions
|
|
4316
|
-
});
|
|
4317
|
-
function commandFailure2(error) {
|
|
4318
|
-
return { ok: false, error: { ...error, detail: error.detail } };
|
|
4319
|
-
}
|
|
4320
|
-
function createBuildContribution(projectRoot2 = process.cwd()) {
|
|
4321
|
-
return defineTool(
|
|
4322
|
-
projectBuildDescriptor,
|
|
4323
|
-
async (options) => {
|
|
4324
|
-
const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
4325
|
-
const result = await buildCommand2({ ...options, root: options.root ?? projectRoot2 });
|
|
4326
|
-
return result.ok ? result.value : commandFailure2(result.error);
|
|
4327
|
-
}
|
|
4328
|
-
);
|
|
4329
|
-
}
|
|
4330
|
-
function createAuthorContribution(projectRoot2 = process.cwd()) {
|
|
6372
|
+
function createOfflineAnalysisContribution() {
|
|
4331
6373
|
return defineTool(
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
return result.ok ? result.value : commandFailure2(result.error);
|
|
4337
|
-
}
|
|
6374
|
+
{
|
|
6375
|
+
...previewOfflineAnalysisDescriptor
|
|
6376
|
+
},
|
|
6377
|
+
(request) => analyzePreviewArtifacts(request)
|
|
4338
6378
|
);
|
|
4339
6379
|
}
|
|
4340
|
-
function
|
|
6380
|
+
function createPreviewContributions() {
|
|
4341
6381
|
return [
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
...createDomainPreviewContributions()
|
|
6382
|
+
createPreviewContribution(),
|
|
6383
|
+
createOfflineAnalysisContribution()
|
|
4345
6384
|
];
|
|
4346
6385
|
}
|
|
4347
|
-
var
|
|
4348
|
-
"src/tools/contributions.ts"() {
|
|
6386
|
+
var init_preview_contributions = __esm({
|
|
6387
|
+
"src/tools/preview-contributions.ts"() {
|
|
4349
6388
|
init_catalog();
|
|
4350
|
-
|
|
6389
|
+
init_offline_analysis();
|
|
6390
|
+
init_preview_migration();
|
|
4351
6391
|
}
|
|
4352
6392
|
});
|
|
4353
6393
|
|
|
@@ -4368,6 +6408,679 @@ var init_runtime = __esm({
|
|
|
4368
6408
|
init_preview_contributions();
|
|
4369
6409
|
}
|
|
4370
6410
|
});
|
|
6411
|
+
function transportCause(cause) {
|
|
6412
|
+
if (cause instanceof Error) {
|
|
6413
|
+
return {
|
|
6414
|
+
name: cause.name,
|
|
6415
|
+
message: cause.message,
|
|
6416
|
+
...cause.stack === void 0 ? {} : { stack: cause.stack }
|
|
6417
|
+
};
|
|
6418
|
+
}
|
|
6419
|
+
if (cause === void 0) return "undefined";
|
|
6420
|
+
try {
|
|
6421
|
+
return JSON.parse(JSON.stringify(cause));
|
|
6422
|
+
} catch {
|
|
6423
|
+
return String(cause);
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
function dataUriBytes(uri) {
|
|
6427
|
+
const separator = uri.indexOf(",");
|
|
6428
|
+
if (separator < 0) throw new TypeError("preview artifact must be a data URI before publication");
|
|
6429
|
+
return Buffer.from(uri.slice(separator + 1), "base64");
|
|
6430
|
+
}
|
|
6431
|
+
function projectUri(projectRoot2, path) {
|
|
6432
|
+
return relative(projectRoot2, path).split(sep).join("/");
|
|
6433
|
+
}
|
|
6434
|
+
async function allocateLoopbackPort() {
|
|
6435
|
+
const probe = createServer();
|
|
6436
|
+
try {
|
|
6437
|
+
await new Promise((resolve20, reject) => {
|
|
6438
|
+
const onError = (error) => {
|
|
6439
|
+
probe.off("listening", onListening);
|
|
6440
|
+
reject(error);
|
|
6441
|
+
};
|
|
6442
|
+
const onListening = () => {
|
|
6443
|
+
probe.off("error", onError);
|
|
6444
|
+
resolve20();
|
|
6445
|
+
};
|
|
6446
|
+
probe.once("error", onError);
|
|
6447
|
+
probe.once("listening", onListening);
|
|
6448
|
+
probe.listen(0, "127.0.0.1");
|
|
6449
|
+
});
|
|
6450
|
+
const address = probe.address();
|
|
6451
|
+
if (address === null || typeof address === "string") {
|
|
6452
|
+
throw new Error("loopback port probe did not expose a TCP address");
|
|
6453
|
+
}
|
|
6454
|
+
return address.port;
|
|
6455
|
+
} finally {
|
|
6456
|
+
if (probe.listening) {
|
|
6457
|
+
await new Promise((resolve20) => probe.close(() => resolve20()));
|
|
6458
|
+
}
|
|
6459
|
+
}
|
|
6460
|
+
}
|
|
6461
|
+
function sha256(bytes) {
|
|
6462
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
6463
|
+
}
|
|
6464
|
+
function artifactRef(artifact) {
|
|
6465
|
+
const kind = artifact.kind === "report" || artifact.kind === "contact-sheet" ? "tool-result" : artifact.kind;
|
|
6466
|
+
return {
|
|
6467
|
+
kind,
|
|
6468
|
+
digest: artifact.digest,
|
|
6469
|
+
uri: artifact.uri,
|
|
6470
|
+
mediaType: artifact.mediaType,
|
|
6471
|
+
sizeBytes: artifact.byteLength
|
|
6472
|
+
};
|
|
6473
|
+
}
|
|
6474
|
+
async function publishPreviewArtifacts(projectRoot2, runId, result, reportInput) {
|
|
6475
|
+
if (result.manifest.identity.runId !== runId) {
|
|
6476
|
+
throw new Error(
|
|
6477
|
+
`preview artifact run identity mismatch: manifest=${result.manifest.identity.runId} requested=${runId}`
|
|
6478
|
+
);
|
|
6479
|
+
}
|
|
6480
|
+
const runsRoot = join(projectRoot2, ".forgeax", "tool-runs");
|
|
6481
|
+
await mkdir(runsRoot, { recursive: true });
|
|
6482
|
+
const directoryName = runId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
6483
|
+
const published = join(runsRoot, directoryName);
|
|
6484
|
+
const finalPaths = {
|
|
6485
|
+
tapeJson: join(published, "rhi-tape.json"),
|
|
6486
|
+
tapeBlob: join(published, "rhi-tape.bin"),
|
|
6487
|
+
capturePng: join(published, "capture.png"),
|
|
6488
|
+
freshReplayPng: join(published, "fresh-replay.png"),
|
|
6489
|
+
profile: join(published, "profile.json"),
|
|
6490
|
+
manifest: join(published, "manifest.json"),
|
|
6491
|
+
report: join(published, "report.json")
|
|
6492
|
+
};
|
|
6493
|
+
const tapeJson = dataUriBytes(result.tape.jsonUri);
|
|
6494
|
+
const tapeBlob = dataUriBytes(result.tape.blobUri);
|
|
6495
|
+
const capturePng = dataUriBytes(result.capturePng.uri);
|
|
6496
|
+
const freshReplayPng = dataUriBytes(result.png.uri);
|
|
6497
|
+
const profile = dataUriBytes(result.profile.uri);
|
|
6498
|
+
const bytesByRole = {
|
|
6499
|
+
"rhi-tape": tapeJson,
|
|
6500
|
+
capture: capturePng,
|
|
6501
|
+
"fresh-replay": freshReplayPng,
|
|
6502
|
+
"profile-capture": profile
|
|
6503
|
+
};
|
|
6504
|
+
const uriByRole = {
|
|
6505
|
+
"rhi-tape": projectUri(projectRoot2, finalPaths.tapeJson),
|
|
6506
|
+
capture: projectUri(projectRoot2, finalPaths.capturePng),
|
|
6507
|
+
"fresh-replay": projectUri(projectRoot2, finalPaths.freshReplayPng),
|
|
6508
|
+
"profile-capture": projectUri(projectRoot2, finalPaths.profile)
|
|
6509
|
+
};
|
|
6510
|
+
const mediaTypeByRole = {
|
|
6511
|
+
"rhi-tape": "application/vnd.forgeax.rhi-tape+json",
|
|
6512
|
+
capture: "image/png",
|
|
6513
|
+
"fresh-replay": "image/png",
|
|
6514
|
+
"profile-capture": "application/vnd.forgeax.profile+json"
|
|
6515
|
+
};
|
|
6516
|
+
const requiredRoles = ["rhi-tape", "capture", "fresh-replay", "profile-capture"];
|
|
6517
|
+
const nonReportArtifacts = result.manifest.artifacts.filter((artifact) => artifact.role !== "report").map((artifact) => {
|
|
6518
|
+
if (!(artifact.role in bytesByRole)) {
|
|
6519
|
+
return artifact;
|
|
6520
|
+
}
|
|
6521
|
+
const role = artifact.role;
|
|
6522
|
+
const bytes = bytesByRole[role];
|
|
6523
|
+
const digest = sha256(bytes);
|
|
6524
|
+
if (artifact.digest !== digest) {
|
|
6525
|
+
throw new Error(`preview artifact digest mismatch for ${artifact.role}`);
|
|
6526
|
+
}
|
|
6527
|
+
return {
|
|
6528
|
+
...artifact,
|
|
6529
|
+
uri: uriByRole[role],
|
|
6530
|
+
digest,
|
|
6531
|
+
byteLength: bytes.byteLength,
|
|
6532
|
+
mediaType: mediaTypeByRole[role]
|
|
6533
|
+
};
|
|
6534
|
+
});
|
|
6535
|
+
if (requiredRoles.some((role) => !nonReportArtifacts.some((artifact) => artifact.role === role))) {
|
|
6536
|
+
throw new Error(
|
|
6537
|
+
"preview publication is missing capture, fresh-replay, tape, or profile artifact"
|
|
6538
|
+
);
|
|
6539
|
+
}
|
|
6540
|
+
let report;
|
|
6541
|
+
let reportBytes;
|
|
6542
|
+
if (reportInput !== void 0) {
|
|
6543
|
+
if (reportInput.snapshot.digest !== result.manifest.identity.snapshotDigest) {
|
|
6544
|
+
throw new Error("preview report snapshot identity does not match the artifact manifest");
|
|
6545
|
+
}
|
|
6546
|
+
report = createResourcePreviewReport({
|
|
6547
|
+
runId,
|
|
6548
|
+
snapshot: reportInput.snapshot,
|
|
6549
|
+
subject: reportInput.subject,
|
|
6550
|
+
presentation: reportInput.presentation,
|
|
6551
|
+
oracle: reportInput.oracle,
|
|
6552
|
+
artifacts: nonReportArtifacts
|
|
6553
|
+
});
|
|
6554
|
+
reportBytes = new TextEncoder().encode(JSON.stringify(report));
|
|
6555
|
+
}
|
|
6556
|
+
const reportArtifact = reportBytes === void 0 ? void 0 : {
|
|
6557
|
+
owner: "resource-preview",
|
|
6558
|
+
kind: "report",
|
|
6559
|
+
role: "report",
|
|
6560
|
+
uri: projectUri(projectRoot2, finalPaths.report),
|
|
6561
|
+
digest: sha256(reportBytes),
|
|
6562
|
+
byteLength: reportBytes.byteLength,
|
|
6563
|
+
mediaType: "application/vnd.forgeax.resource-preview+json",
|
|
6564
|
+
derivedFrom: nonReportArtifacts.map((artifact) => artifact.digest)
|
|
6565
|
+
};
|
|
6566
|
+
const manifest = createPreviewArtifactManifest({
|
|
6567
|
+
...result.manifest,
|
|
6568
|
+
artifacts: reportArtifact === void 0 ? nonReportArtifacts : [reportArtifact, ...nonReportArtifacts]
|
|
6569
|
+
});
|
|
6570
|
+
const validated = validatePreviewArtifactManifest(
|
|
6571
|
+
manifest,
|
|
6572
|
+
reportArtifact === void 0 ? requiredRoles : ["report", ...requiredRoles]
|
|
6573
|
+
);
|
|
6574
|
+
if (!validated.ok) throw new Error(JSON.stringify(validated.error.detail));
|
|
6575
|
+
const staging = await mkdtemp(join(runsRoot, ".staging-"));
|
|
6576
|
+
const paths = {
|
|
6577
|
+
tapeJson: join(staging, "rhi-tape.json"),
|
|
6578
|
+
tapeBlob: join(staging, "rhi-tape.bin"),
|
|
6579
|
+
capturePng: join(staging, "capture.png"),
|
|
6580
|
+
freshReplayPng: join(staging, "fresh-replay.png"),
|
|
6581
|
+
profile: join(staging, "profile.json"),
|
|
6582
|
+
manifest: join(staging, "manifest.json"),
|
|
6583
|
+
report: join(staging, "report.json")
|
|
6584
|
+
};
|
|
6585
|
+
try {
|
|
6586
|
+
await Promise.all([
|
|
6587
|
+
writeFile(paths.tapeJson, tapeJson),
|
|
6588
|
+
writeFile(paths.tapeBlob, tapeBlob),
|
|
6589
|
+
writeFile(paths.capturePng, capturePng),
|
|
6590
|
+
writeFile(paths.freshReplayPng, freshReplayPng),
|
|
6591
|
+
writeFile(paths.profile, profile),
|
|
6592
|
+
...reportBytes === void 0 ? [] : [writeFile(paths.report, reportBytes)]
|
|
6593
|
+
]);
|
|
6594
|
+
await writeFile(paths.manifest, `${JSON.stringify(manifest, null, 2)}
|
|
6595
|
+
`);
|
|
6596
|
+
await rename(staging, published);
|
|
6597
|
+
} catch (cause) {
|
|
6598
|
+
await rm(staging, { recursive: true, force: true });
|
|
6599
|
+
throw cause;
|
|
6600
|
+
}
|
|
6601
|
+
return {
|
|
6602
|
+
...result,
|
|
6603
|
+
tape: {
|
|
6604
|
+
...result.tape,
|
|
6605
|
+
jsonUri: projectUri(projectRoot2, finalPaths.tapeJson),
|
|
6606
|
+
blobUri: projectUri(projectRoot2, finalPaths.tapeBlob)
|
|
6607
|
+
},
|
|
6608
|
+
profile: { ...result.profile, uri: projectUri(projectRoot2, finalPaths.profile) },
|
|
6609
|
+
capturePng: { ...result.capturePng, uri: projectUri(projectRoot2, finalPaths.capturePng) },
|
|
6610
|
+
png: { ...result.png, uri: projectUri(projectRoot2, finalPaths.freshReplayPng) },
|
|
6611
|
+
manifest,
|
|
6612
|
+
artifacts: manifest.artifacts.map(artifactRef)
|
|
6613
|
+
};
|
|
6614
|
+
}
|
|
6615
|
+
function browserFailure(phase, cause, pageErrors = []) {
|
|
6616
|
+
return {
|
|
6617
|
+
ok: false,
|
|
6618
|
+
error: {
|
|
6619
|
+
code: "tool-preview-browser-host-failed",
|
|
6620
|
+
expected: "a real Chromium page, project bootstrap, WebGPU device, and bounded preview run",
|
|
6621
|
+
hint: "Inspect the Browser Host phase and repair the project or local WebGPU capability.",
|
|
6622
|
+
detail: {
|
|
6623
|
+
phase,
|
|
6624
|
+
cause: transportCause(cause),
|
|
6625
|
+
pageErrors
|
|
6626
|
+
}
|
|
6627
|
+
}
|
|
6628
|
+
};
|
|
6629
|
+
}
|
|
6630
|
+
async function runBrowserPreviewHost(projectRoot2, recipe, snapshot, runId, signal, bootstrapRoot = "project-bootstrap", resource, options = {}) {
|
|
6631
|
+
const facts = await readProjectFacts(projectRoot2);
|
|
6632
|
+
if (!facts.ok)
|
|
6633
|
+
return { ok: false, error: { ...facts.error, detail: facts.error.detail } };
|
|
6634
|
+
const config = await createViteConfig(facts.value, "serve", "/", { bootstrapRoot });
|
|
6635
|
+
const cacheDir = await mkdtemp(join(facts.value.root, ".forgeax", ".browser-host-vite-"));
|
|
6636
|
+
let server;
|
|
6637
|
+
try {
|
|
6638
|
+
const port = await allocateLoopbackPort();
|
|
6639
|
+
server = await createServer$1({
|
|
6640
|
+
...config,
|
|
6641
|
+
cacheDir,
|
|
6642
|
+
logLevel: "silent",
|
|
6643
|
+
optimizeDeps: {
|
|
6644
|
+
...config.optimizeDeps,
|
|
6645
|
+
// Tool runs must observe the current workspace build, even when a
|
|
6646
|
+
// persistent runner retains Vite's optimized-dependency cache.
|
|
6647
|
+
force: true
|
|
6648
|
+
},
|
|
6649
|
+
server: {
|
|
6650
|
+
...config.server,
|
|
6651
|
+
host: "127.0.0.1",
|
|
6652
|
+
port,
|
|
6653
|
+
// Vite's port 0 means its default 5173. Bind the probed port exactly so
|
|
6654
|
+
// another project or runner cannot be mistaken for this Browser Host.
|
|
6655
|
+
strictPort: true
|
|
6656
|
+
}
|
|
6657
|
+
});
|
|
6658
|
+
} catch (cause) {
|
|
6659
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
6660
|
+
throw cause;
|
|
6661
|
+
}
|
|
6662
|
+
const hostStartedAtMs = performance.now();
|
|
6663
|
+
let phase = "server-listen";
|
|
6664
|
+
let browser;
|
|
6665
|
+
let page;
|
|
6666
|
+
let pageErrors = [];
|
|
6667
|
+
let responseDiagnostics = [];
|
|
6668
|
+
const launchBrowser = (headless) => chromium.launch({
|
|
6669
|
+
channel: process.env.FORGEAX_CHROME_CHANNEL ?? "chrome",
|
|
6670
|
+
headless,
|
|
6671
|
+
args: [
|
|
6672
|
+
"--enable-unsafe-webgpu",
|
|
6673
|
+
"--enable-features=Vulkan,UseSkiaRenderer,SharedArrayBuffer",
|
|
6674
|
+
"--use-vulkan=swiftshader",
|
|
6675
|
+
"--use-angle=swiftshader",
|
|
6676
|
+
"--disable-vulkan-surface",
|
|
6677
|
+
"--ignore-gpu-blocklist",
|
|
6678
|
+
"--disable-gpu-driver-bug-workarounds",
|
|
6679
|
+
"--disable-dawn-features=disallow_unsafe_apis",
|
|
6680
|
+
"--autoplay-policy=no-user-gesture-required"
|
|
6681
|
+
]
|
|
6682
|
+
});
|
|
6683
|
+
const observePage = (target) => {
|
|
6684
|
+
target.on("pageerror", (error) => pageErrors.push(error.message));
|
|
6685
|
+
target.on("console", (message) => {
|
|
6686
|
+
if (message.type() === "error" || message.type() === "warning") {
|
|
6687
|
+
pageErrors.push(`${message.type()}: ${message.text()}`);
|
|
6688
|
+
}
|
|
6689
|
+
});
|
|
6690
|
+
target.on("response", (response) => {
|
|
6691
|
+
if (response.status() >= 400) {
|
|
6692
|
+
responseDiagnostics.push(
|
|
6693
|
+
response.text().then((body) => {
|
|
6694
|
+
pageErrors.push(`HTTP ${response.status()}: ${response.url()} ${body}`);
|
|
6695
|
+
}).catch(() => {
|
|
6696
|
+
pageErrors.push(`HTTP ${response.status()}: ${response.url()}`);
|
|
6697
|
+
})
|
|
6698
|
+
);
|
|
6699
|
+
}
|
|
6700
|
+
});
|
|
6701
|
+
};
|
|
6702
|
+
const closeBrowserProcess = async () => {
|
|
6703
|
+
await page?.close().catch(() => void 0);
|
|
6704
|
+
page = void 0;
|
|
6705
|
+
await browser?.close().catch(() => void 0);
|
|
6706
|
+
browser = void 0;
|
|
6707
|
+
};
|
|
6708
|
+
const abort = () => {
|
|
6709
|
+
void closeBrowserProcess();
|
|
6710
|
+
void server.close();
|
|
6711
|
+
};
|
|
6712
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
6713
|
+
try {
|
|
6714
|
+
if (signal.aborted) throw new Error("Browser Host aborted before launch");
|
|
6715
|
+
await server.listen();
|
|
6716
|
+
const address = server.httpServer?.address();
|
|
6717
|
+
if (address === null || address === void 0 || typeof address === "string") {
|
|
6718
|
+
throw new Error("Vite Browser Host did not expose a loopback TCP address");
|
|
6719
|
+
}
|
|
6720
|
+
phase = "server-transform";
|
|
6721
|
+
const entryTransform = await server.transformRequest("/main.ts");
|
|
6722
|
+
if (entryTransform === null) {
|
|
6723
|
+
throw new Error("Vite Browser Host entry transform returned no module");
|
|
6724
|
+
}
|
|
6725
|
+
phase = "capture-browser-launch";
|
|
6726
|
+
browser = await launchBrowser(recipe.presentation === "hidden");
|
|
6727
|
+
phase = "capture-page-bootstrap";
|
|
6728
|
+
page = await browser.newPage({
|
|
6729
|
+
viewport: recipe.viewport,
|
|
6730
|
+
deviceScaleFactor: 1
|
|
6731
|
+
});
|
|
6732
|
+
observePage(page);
|
|
6733
|
+
const captureUrl = new URL(`http://127.0.0.1:${address.port}/`);
|
|
6734
|
+
captureUrl.searchParams.set("forgeax-tool-recipe", JSON.stringify(recipe));
|
|
6735
|
+
captureUrl.searchParams.set("forgeax-tool-snapshot", JSON.stringify(snapshot));
|
|
6736
|
+
captureUrl.searchParams.set("forgeax-tool-run-id", runId);
|
|
6737
|
+
if (resource !== void 0)
|
|
6738
|
+
captureUrl.searchParams.set("forgeax-resource-preview", JSON.stringify(resource));
|
|
6739
|
+
await page.goto(captureUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
|
|
6740
|
+
await page.waitForFunction(
|
|
6741
|
+
() => globalThis.__forgeaxToolHost?.ready === true,
|
|
6742
|
+
void 0,
|
|
6743
|
+
{ timeout: 45e3 }
|
|
6744
|
+
);
|
|
6745
|
+
phase = "capture-run";
|
|
6746
|
+
const captured = await page.evaluate(async () => {
|
|
6747
|
+
const host = globalThis.__forgeaxToolHost;
|
|
6748
|
+
const value2 = await host.capture();
|
|
6749
|
+
return JSON.parse(
|
|
6750
|
+
JSON.stringify(
|
|
6751
|
+
value2,
|
|
6752
|
+
(_key, nested2) => nested2 instanceof Error ? {
|
|
6753
|
+
...nested2,
|
|
6754
|
+
name: nested2.name,
|
|
6755
|
+
message: nested2.message,
|
|
6756
|
+
stack: nested2.stack
|
|
6757
|
+
} : nested2
|
|
6758
|
+
)
|
|
6759
|
+
);
|
|
6760
|
+
});
|
|
6761
|
+
await Promise.all(responseDiagnostics);
|
|
6762
|
+
if (!captured.ok) return browserFailure("capture-run", captured.error, pageErrors);
|
|
6763
|
+
if (pageErrors.length > 0)
|
|
6764
|
+
return browserFailure("capture-page-runtime", pageErrors[0], pageErrors);
|
|
6765
|
+
const capturePng = await page.screenshot({ type: "png" });
|
|
6766
|
+
const capturedResult = {
|
|
6767
|
+
...captured.result,
|
|
6768
|
+
capturePng: {
|
|
6769
|
+
uri: `data:image/png;base64,${capturePng.toString("base64")}`,
|
|
6770
|
+
width: recipe.viewport.width,
|
|
6771
|
+
height: recipe.viewport.height
|
|
6772
|
+
}
|
|
6773
|
+
};
|
|
6774
|
+
await closeBrowserProcess();
|
|
6775
|
+
pageErrors = [];
|
|
6776
|
+
responseDiagnostics = [];
|
|
6777
|
+
phase = "replay-browser-launch";
|
|
6778
|
+
browser = await launchBrowser(true);
|
|
6779
|
+
phase = "replay-page-bootstrap";
|
|
6780
|
+
page = await browser.newPage({
|
|
6781
|
+
viewport: recipe.viewport,
|
|
6782
|
+
deviceScaleFactor: 1
|
|
6783
|
+
});
|
|
6784
|
+
observePage(page);
|
|
6785
|
+
const replayUrl = new URL(`http://127.0.0.1:${address.port}/`);
|
|
6786
|
+
replayUrl.searchParams.set("forgeax-tool-replay", "1");
|
|
6787
|
+
await page.goto(replayUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
|
|
6788
|
+
await page.waitForFunction(
|
|
6789
|
+
() => globalThis.__forgeaxToolReplayHost?.ready === true,
|
|
6790
|
+
void 0,
|
|
6791
|
+
{ timeout: 45e3 }
|
|
6792
|
+
);
|
|
6793
|
+
phase = "replay-run";
|
|
6794
|
+
const result = await page.evaluate(async (serializedCapture) => {
|
|
6795
|
+
const host = globalThis.__forgeaxToolReplayHost;
|
|
6796
|
+
const value2 = await host.run(JSON.parse(serializedCapture));
|
|
6797
|
+
return JSON.parse(
|
|
6798
|
+
JSON.stringify(
|
|
6799
|
+
value2,
|
|
6800
|
+
(_key, nested2) => nested2 instanceof Error ? {
|
|
6801
|
+
...nested2,
|
|
6802
|
+
name: nested2.name,
|
|
6803
|
+
message: nested2.message,
|
|
6804
|
+
stack: nested2.stack
|
|
6805
|
+
} : nested2
|
|
6806
|
+
)
|
|
6807
|
+
);
|
|
6808
|
+
}, JSON.stringify(capturedResult));
|
|
6809
|
+
await Promise.all(responseDiagnostics);
|
|
6810
|
+
if (!result.ok) return browserFailure("replay-run", result.error, pageErrors);
|
|
6811
|
+
if (pageErrors.length > 0)
|
|
6812
|
+
return browserFailure("replay-page-runtime", pageErrors[0], pageErrors);
|
|
6813
|
+
const endedAtMs = performance.now();
|
|
6814
|
+
const phases = result.result.operationTiming.phases;
|
|
6815
|
+
const observedDurationMs = phases === void 0 ? 0 : Object.values(phases).reduce(
|
|
6816
|
+
(total, observation) => total + (observation.status === "observed" ? observation.durationMs : 0),
|
|
6817
|
+
0
|
|
6818
|
+
);
|
|
6819
|
+
const durationMs = endedAtMs - hostStartedAtMs;
|
|
6820
|
+
const value = {
|
|
6821
|
+
...result.result,
|
|
6822
|
+
operationTiming: {
|
|
6823
|
+
...result.result.operationTiming,
|
|
6824
|
+
startedAtMs: hostStartedAtMs,
|
|
6825
|
+
endedAtMs,
|
|
6826
|
+
durationMs,
|
|
6827
|
+
...phases === void 0 ? { unattributedMs: durationMs } : {
|
|
6828
|
+
phases: {
|
|
6829
|
+
...phases,
|
|
6830
|
+
transport: {
|
|
6831
|
+
status: "observed",
|
|
6832
|
+
durationMs: Math.max(0, durationMs - observedDurationMs)
|
|
6833
|
+
}
|
|
6834
|
+
},
|
|
6835
|
+
unattributedMs: 0
|
|
6836
|
+
}
|
|
6837
|
+
},
|
|
6838
|
+
actualCarrier: recipe.presentation === "hidden" ? "headless-private" : "headed-private"
|
|
6839
|
+
};
|
|
6840
|
+
phase = "artifact-publish";
|
|
6841
|
+
return {
|
|
6842
|
+
ok: true,
|
|
6843
|
+
value: options.publish === false ? value : await publishPreviewArtifacts(projectRoot2, runId, value)
|
|
6844
|
+
};
|
|
6845
|
+
} catch (cause) {
|
|
6846
|
+
await Promise.all(responseDiagnostics);
|
|
6847
|
+
return browserFailure(phase, cause, pageErrors);
|
|
6848
|
+
} finally {
|
|
6849
|
+
signal.removeEventListener("abort", abort);
|
|
6850
|
+
await closeBrowserProcess();
|
|
6851
|
+
await server.close().catch(() => void 0);
|
|
6852
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
function runBrowserResourcePreviewHost(projectRoot2, recipe, snapshot, runId, signal, resource, options = {}) {
|
|
6856
|
+
return runBrowserPreviewHost(
|
|
6857
|
+
projectRoot2,
|
|
6858
|
+
recipe,
|
|
6859
|
+
snapshot,
|
|
6860
|
+
runId,
|
|
6861
|
+
signal,
|
|
6862
|
+
"resource-bootstrap",
|
|
6863
|
+
resource,
|
|
6864
|
+
options
|
|
6865
|
+
);
|
|
6866
|
+
}
|
|
6867
|
+
var init_browser_host = __esm({
|
|
6868
|
+
"src/tools/browser-host.ts"() {
|
|
6869
|
+
init_host();
|
|
6870
|
+
init_project();
|
|
6871
|
+
}
|
|
6872
|
+
});
|
|
6873
|
+
|
|
6874
|
+
// src/tools/native-preview.ts
|
|
6875
|
+
var native_preview_exports = {};
|
|
6876
|
+
__export(native_preview_exports, {
|
|
6877
|
+
isNativePreviewTool: () => isNativePreviewTool,
|
|
6878
|
+
runNativePreviewTool: () => runNativePreviewTool
|
|
6879
|
+
});
|
|
6880
|
+
function isNativePreviewTool(id) {
|
|
6881
|
+
return previewToolIds.has(id);
|
|
6882
|
+
}
|
|
6883
|
+
async function runNativePreviewTool(contribution, args2, options, projectRoot2) {
|
|
6884
|
+
const kind = contribution.descriptor.id.split(".")[0];
|
|
6885
|
+
if (kind !== "material" && kind !== "mesh" && kind !== "texture" && kind !== "vfx") {
|
|
6886
|
+
throw new Error(`unsupported native preview operation ${contribution.descriptor.id}`);
|
|
6887
|
+
}
|
|
6888
|
+
const parsed = contribution.descriptor.argsSchema.parse(args2);
|
|
6889
|
+
if (!parsed.ok) {
|
|
6890
|
+
return {
|
|
6891
|
+
outcome: "failed",
|
|
6892
|
+
failure: {
|
|
6893
|
+
code: "tool-invalid-args",
|
|
6894
|
+
expected: "preview arguments to match the operation argsSchema",
|
|
6895
|
+
hint: "Pass { guid } and an optional power-of-two size from AssetRegistry catalog identity.",
|
|
6896
|
+
detail: { message: parsed.error, value: null }
|
|
6897
|
+
},
|
|
6898
|
+
artifacts: []
|
|
6899
|
+
};
|
|
6900
|
+
}
|
|
6901
|
+
const request = parsed.value;
|
|
6902
|
+
const { guid, size = RESOURCE_PREVIEW_DEFAULT_SIZE } = request;
|
|
6903
|
+
const selectedPreviewPlugin = nativePreviewPlugins.find(
|
|
6904
|
+
([, plugin]) => plugin.tools.some(
|
|
6905
|
+
(tool) => tool.descriptor.id === contribution.descriptor.id
|
|
6906
|
+
)
|
|
6907
|
+
);
|
|
6908
|
+
if (selectedPreviewPlugin === void 0) {
|
|
6909
|
+
throw new Error(`native preview plugin does not export ${contribution.descriptor.id}`);
|
|
6910
|
+
}
|
|
6911
|
+
const selectedPluginName = selectedPreviewPlugin[0];
|
|
6912
|
+
const snapshot = options.snapshot ?? { revision: 0, digest: `sha256:project:${projectRoot2}` };
|
|
6913
|
+
const previewRunId = `${contribution.descriptor.id}:${crypto.randomUUID()}`;
|
|
6914
|
+
const browser = await runBrowserResourcePreviewHost(
|
|
6915
|
+
projectRoot2,
|
|
6916
|
+
createToolPreviewRecipe({
|
|
6917
|
+
presentation: "hidden",
|
|
6918
|
+
viewport: { width: size, height: size },
|
|
6919
|
+
frames: 32
|
|
6920
|
+
}),
|
|
6921
|
+
snapshot,
|
|
6922
|
+
previewRunId,
|
|
6923
|
+
options.signal ?? new AbortController().signal,
|
|
6924
|
+
{ kind, guid, size },
|
|
6925
|
+
{ publish: false }
|
|
6926
|
+
);
|
|
6927
|
+
if (!browser.ok || browser.value.resource === void 0) {
|
|
6928
|
+
const failure2 = browser.ok ? {
|
|
6929
|
+
code: "tool-preview-bootstrap-failed",
|
|
6930
|
+
expected: "the Browser resource bootstrap to return AssetRegistry owner facts",
|
|
6931
|
+
hint: "Inspect resource-bootstrap and retry after the owner publishes the GUID payload.",
|
|
6932
|
+
detail: { phase: "resource-owner" }
|
|
6933
|
+
} : browser.error;
|
|
6934
|
+
return {
|
|
6935
|
+
outcome: "failed",
|
|
6936
|
+
failure: {
|
|
6937
|
+
code: "tool-domain-failed",
|
|
6938
|
+
expected: failure2.expected ?? "the Browser resource bootstrap to succeed",
|
|
6939
|
+
hint: failure2.hint ?? "Inspect the Browser resource bootstrap failure and retry.",
|
|
6940
|
+
detail: {
|
|
6941
|
+
code: failure2.code,
|
|
6942
|
+
...failure2.detail === void 0 ? {} : { payload: failure2.detail }
|
|
6943
|
+
}
|
|
6944
|
+
},
|
|
6945
|
+
artifacts: []
|
|
6946
|
+
};
|
|
6947
|
+
}
|
|
6948
|
+
const resource = browser.value.resource;
|
|
6949
|
+
const subjectDrawn = kind === "texture" ? browser.value.drawCalls > 0 : browser.value.drawCalls > 2;
|
|
6950
|
+
if (!subjectDrawn) {
|
|
6951
|
+
return {
|
|
6952
|
+
outcome: "failed",
|
|
6953
|
+
failure: {
|
|
6954
|
+
code: "tool-domain-failed",
|
|
6955
|
+
expected: "the captured resource frame to contain a subject draw in addition to canonical presentation passes",
|
|
6956
|
+
hint: "Inspect material readiness and the RHI tape before retrying the same GUID preview.",
|
|
6957
|
+
detail: {
|
|
6958
|
+
code: "tool-preview-subject-not-rendered",
|
|
6959
|
+
payload: { kind, drawCalls: browser.value.drawCalls }
|
|
6960
|
+
}
|
|
6961
|
+
},
|
|
6962
|
+
artifacts: []
|
|
6963
|
+
};
|
|
6964
|
+
}
|
|
6965
|
+
const ctx = new Context();
|
|
6966
|
+
try {
|
|
6967
|
+
const host = createNativePreviewHost({
|
|
6968
|
+
runId: `${contribution.descriptor.id}:native`,
|
|
6969
|
+
snapshot,
|
|
6970
|
+
projectRoot: projectRoot2,
|
|
6971
|
+
backend: "webgpu",
|
|
6972
|
+
signal: options.signal ?? new AbortController().signal,
|
|
6973
|
+
assets: {
|
|
6974
|
+
loadByGuid: async () => ({
|
|
6975
|
+
ok: true,
|
|
6976
|
+
value: resource.asset,
|
|
6977
|
+
...resource.digest === void 0 ? {} : { digest: resource.digest },
|
|
6978
|
+
...resource.ownerFacts === void 0 ? {} : { ownerFacts: resource.ownerFacts }
|
|
6979
|
+
})
|
|
6980
|
+
},
|
|
6981
|
+
renderer: {
|
|
6982
|
+
rendererReady: browser.value.trace.events.includes("renderer-created"),
|
|
6983
|
+
worldReady: browser.value.trace.events.includes("world-updated"),
|
|
6984
|
+
drawCalls: browser.value.drawCalls,
|
|
6985
|
+
nonBlackPixels: browser.value.nonBlackPixels,
|
|
6986
|
+
...resource.observation === void 0 ? {} : { observation: resource.observation },
|
|
6987
|
+
...kind === "vfx" && resource.observation !== void 0 ? {
|
|
6988
|
+
vfx: {
|
|
6989
|
+
dispatches: typeof resource.observation.dispatches === "number" ? resource.observation.dispatches : 0,
|
|
6990
|
+
indirectDraws: typeof resource.observation.indirectDraws === "number" ? resource.observation.indirectDraws : 0,
|
|
6991
|
+
subjectOutputs: typeof resource.observation.subjectOutputs === "number" ? resource.observation.subjectOutputs : 0
|
|
6992
|
+
}
|
|
6993
|
+
} : {},
|
|
6994
|
+
texture: { drawCalls: browser.value.drawCalls }
|
|
6995
|
+
},
|
|
6996
|
+
artifacts: browser.value.artifacts
|
|
6997
|
+
});
|
|
6998
|
+
const { loader } = await installCatalogLoader(
|
|
6999
|
+
ctx,
|
|
7000
|
+
createNativePreviewCatalog(host, selectedPluginName),
|
|
7001
|
+
"host"
|
|
7002
|
+
);
|
|
7003
|
+
await loader.root.update(
|
|
7004
|
+
projectPluginEntries(
|
|
7005
|
+
previewPluginEntries.filter(({ name }) => name === selectedPluginName),
|
|
7006
|
+
"host",
|
|
7007
|
+
"host"
|
|
7008
|
+
)
|
|
7009
|
+
);
|
|
7010
|
+
await loader.await();
|
|
7011
|
+
const nativeContribution = nativePreviewTools.find(
|
|
7012
|
+
(candidate) => candidate.descriptor.id === contribution.descriptor.id
|
|
7013
|
+
);
|
|
7014
|
+
if (nativeContribution === void 0) {
|
|
7015
|
+
throw new Error(`native preview plugin does not export ${contribution.descriptor.id}`);
|
|
7016
|
+
}
|
|
7017
|
+
const terminal = await createToolRuntime([nativeContribution]).run(nativeContribution, args2, {
|
|
7018
|
+
...options,
|
|
7019
|
+
snapshot,
|
|
7020
|
+
capabilityResolver: createContextCapabilityResolver(ctx)
|
|
7021
|
+
}).terminal;
|
|
7022
|
+
if (terminal.outcome !== "succeeded") return terminal;
|
|
7023
|
+
const domainResult = terminal.result;
|
|
7024
|
+
let published;
|
|
7025
|
+
try {
|
|
7026
|
+
published = await publishPreviewArtifacts(projectRoot2, previewRunId, browser.value, {
|
|
7027
|
+
snapshot,
|
|
7028
|
+
subject: domainResult.subject,
|
|
7029
|
+
presentation: domainResult.presentation,
|
|
7030
|
+
oracle: domainResult.oracle
|
|
7031
|
+
});
|
|
7032
|
+
} catch (cause) {
|
|
7033
|
+
return {
|
|
7034
|
+
outcome: "failed",
|
|
7035
|
+
failure: {
|
|
7036
|
+
code: "tool-domain-failed",
|
|
7037
|
+
expected: "the preview report and all capture artifacts to publish atomically",
|
|
7038
|
+
hint: "Inspect the artifact manifest identity or digest failure and retry the same ToolRun.",
|
|
7039
|
+
detail: {
|
|
7040
|
+
code: "tool-artifact-manifest-invalid",
|
|
7041
|
+
payload: cause instanceof Error ? cause.message : String(cause)
|
|
7042
|
+
}
|
|
7043
|
+
},
|
|
7044
|
+
artifacts: []
|
|
7045
|
+
};
|
|
7046
|
+
}
|
|
7047
|
+
const report = published.manifest.artifacts.find((artifact) => artifact.role === "report");
|
|
7048
|
+
if (report === void 0) {
|
|
7049
|
+
throw new Error("resource preview publisher returned no report artifact");
|
|
7050
|
+
}
|
|
7051
|
+
return {
|
|
7052
|
+
...terminal,
|
|
7053
|
+
result: {
|
|
7054
|
+
...terminal.result,
|
|
7055
|
+
actualCarrier: browser.value.actualCarrier,
|
|
7056
|
+
report: {
|
|
7057
|
+
kind: "tool-result",
|
|
7058
|
+
digest: report.digest,
|
|
7059
|
+
uri: report.uri,
|
|
7060
|
+
mediaType: report.mediaType,
|
|
7061
|
+
sizeBytes: report.byteLength
|
|
7062
|
+
},
|
|
7063
|
+
artifacts: published.artifacts
|
|
7064
|
+
},
|
|
7065
|
+
artifacts: published.artifacts
|
|
7066
|
+
};
|
|
7067
|
+
} finally {
|
|
7068
|
+
await ctx.fiber.dispose();
|
|
7069
|
+
}
|
|
7070
|
+
}
|
|
7071
|
+
var previewPluginEntries, previewToolIds;
|
|
7072
|
+
var init_native_preview = __esm({
|
|
7073
|
+
"src/tools/native-preview.ts"() {
|
|
7074
|
+
init_browser_host();
|
|
7075
|
+
init_preview_catalog();
|
|
7076
|
+
previewPluginEntries = nativePreviewPlugins.map(([name]) => ({
|
|
7077
|
+
id: `forgeax-preview-${name.slice(name.lastIndexOf("/") + 1)}`,
|
|
7078
|
+
name,
|
|
7079
|
+
realm: "host"
|
|
7080
|
+
}));
|
|
7081
|
+
previewToolIds = new Set(nativePreviewTools.map(({ descriptor }) => descriptor.id));
|
|
7082
|
+
}
|
|
7083
|
+
});
|
|
4371
7084
|
|
|
4372
7085
|
// src/cli-output.ts
|
|
4373
7086
|
function agentOnboardingLines(value) {
|
|
@@ -4376,6 +7089,12 @@ function agentOnboardingLines(value) {
|
|
|
4376
7089
|
if (onboarding === null || typeof onboarding !== "object" || Array.isArray(onboarding)) return [];
|
|
4377
7090
|
const candidate = onboarding;
|
|
4378
7091
|
const lines = Array.isArray(candidate.read) ? candidate.read.filter((path) => typeof path === "string").map((path) => `[forgeax] read: ${path}`) : [];
|
|
7092
|
+
const selection = candidate.templateSelection;
|
|
7093
|
+
if (selection !== null && typeof selection === "object" && !Array.isArray(selection)) {
|
|
7094
|
+
lines.push("[forgeax] template required: use game-3d for a 3D game; otherwise use empty");
|
|
7095
|
+
lines.push("[forgeax] create 3D: forgeax new <directory> --template game-3d");
|
|
7096
|
+
lines.push("[forgeax] create other: forgeax new <directory> --template empty");
|
|
7097
|
+
}
|
|
4379
7098
|
if (typeof candidate.next?.cwd === "string" && Array.isArray(candidate.next.argv) && candidate.next.argv.every((part) => typeof part === "string")) {
|
|
4380
7099
|
lines.push(`[forgeax] next cwd: ${candidate.next.cwd}`);
|
|
4381
7100
|
lines.push(`[forgeax] next: ${candidate.next.argv.join(" ")}`);
|
|
@@ -4396,12 +7115,15 @@ function sdkUpdateLines(value) {
|
|
|
4396
7115
|
];
|
|
4397
7116
|
}
|
|
4398
7117
|
function renderForgeaxUsage() {
|
|
4399
|
-
return "Usage: forgeax new [directory]
|
|
7118
|
+
return "Usage: forgeax new [directory] --template empty|game-3d\n forgeax <init|doctor|test|dev|build|package|serve|preview> [directory]\n forgeax capture [directory] [--backend auto|software|hardware] [--require-ui] [--deterministic] [--headless] [--output PATH]\n forgeax engine <status|doctor|unlink> [--root directory]\n forgeax engine use-local <engine-directory> [--root directory]\n forgeax package [directory] [--format web-zip|single-html] [--output PATH]\n forgeax run <rhi.capture|rhi.summary|rhi.inspect> [options]\n forgeax run <operation-id> --input <request.json>\n forgeax exec <program.mjs> [--json]\n forgeax asset <add|verify|inspect|list> [subject]\n forgeax shader check [path]\n forgeax plugin <install|uninstall> <module-or-id> [options]\n forgeax skill <install|verify> [--root directory]\n forgeax sdk install <directory> [--version VERSION]\n";
|
|
4400
7119
|
}
|
|
4401
7120
|
|
|
4402
7121
|
// src/cli.ts
|
|
4403
7122
|
init_commands();
|
|
4404
7123
|
|
|
7124
|
+
// src/tools/commands.ts
|
|
7125
|
+
init_software_capture();
|
|
7126
|
+
|
|
4405
7127
|
// src/tools/client.ts
|
|
4406
7128
|
init_catalog();
|
|
4407
7129
|
function decorateResourcePreviewTerminal(terminal) {
|
|
@@ -4464,7 +7186,13 @@ async function createToolClient(options) {
|
|
|
4464
7186
|
const contribution = runtime.get(id);
|
|
4465
7187
|
if (contribution === void 0) return missingTool(id);
|
|
4466
7188
|
const binding = bindingById.get(id);
|
|
4467
|
-
const
|
|
7189
|
+
const nativePreview = binding === void 0 && id.endsWith(".preview") ? await Promise.resolve().then(() => (init_native_preview(), native_preview_exports)) : void 0;
|
|
7190
|
+
const terminal = nativePreview?.isNativePreviewTool(id) === true ? await nativePreview.runNativePreviewTool(
|
|
7191
|
+
contribution,
|
|
7192
|
+
args2,
|
|
7193
|
+
runOptions,
|
|
7194
|
+
options.projectRoot
|
|
7195
|
+
) : binding !== void 0 && runProjectTool2 !== void 0 ? await runProjectTool2(binding, args2, runOptions) : realmDispatch === void 0 ? await runtime.run(contribution, args2, runOptions).terminal : await realmDispatch.run(id, args2, runOptions);
|
|
4468
7196
|
return decorateResourcePreviewTerminal(terminal);
|
|
4469
7197
|
}
|
|
4470
7198
|
};
|
|
@@ -4572,6 +7300,7 @@ async function execCommand(options) {
|
|
|
4572
7300
|
};
|
|
4573
7301
|
}
|
|
4574
7302
|
const root = options.root ?? process.cwd();
|
|
7303
|
+
const browser = createBrowserCapture(root);
|
|
4575
7304
|
try {
|
|
4576
7305
|
const module = await import(pathToFileURL(resolve(root, options.program)).href);
|
|
4577
7306
|
const program = module.default ?? module.run;
|
|
@@ -4579,7 +7308,8 @@ async function execCommand(options) {
|
|
|
4579
7308
|
throw new TypeError("operation program must export a default function or named run function");
|
|
4580
7309
|
}
|
|
4581
7310
|
const client = await createToolClient({ projectRoot: root });
|
|
4582
|
-
const
|
|
7311
|
+
const context = { ...client, browser };
|
|
7312
|
+
const value = await program(context);
|
|
4583
7313
|
if (!isSerializableValue(value)) {
|
|
4584
7314
|
throw new TypeError("tool program returned a non-serializable live value");
|
|
4585
7315
|
}
|
|
@@ -4594,6 +7324,8 @@ async function execCommand(options) {
|
|
|
4594
7324
|
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
4595
7325
|
}
|
|
4596
7326
|
};
|
|
7327
|
+
} finally {
|
|
7328
|
+
await browser.close();
|
|
4597
7329
|
}
|
|
4598
7330
|
}
|
|
4599
7331
|
|
|
@@ -4602,8 +7334,8 @@ init_types();
|
|
|
4602
7334
|
var rawArgs = process.argv.slice(2);
|
|
4603
7335
|
var args = [...rawArgs];
|
|
4604
7336
|
var primary = args.shift();
|
|
4605
|
-
var nested = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? args.shift() : void 0;
|
|
4606
|
-
var command = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? `${primary}.${nested ?? ""}` : primary;
|
|
7337
|
+
var nested = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" || primary === "engine" ? args.shift() : void 0;
|
|
7338
|
+
var command = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" || primary === "engine" ? `${primary}.${nested ?? ""}` : primary;
|
|
4607
7339
|
var json = args.includes("--json");
|
|
4608
7340
|
var dryRun = args.includes("--dry-run");
|
|
4609
7341
|
var noInstall = args.includes("--no-install");
|
|
@@ -4616,6 +7348,8 @@ var optionValueIndexes = new Set(
|
|
|
4616
7348
|
[
|
|
4617
7349
|
"--artifact",
|
|
4618
7350
|
"--base",
|
|
7351
|
+
"--backend",
|
|
7352
|
+
"--browser",
|
|
4619
7353
|
"--digest",
|
|
4620
7354
|
"--id",
|
|
4621
7355
|
"--realm",
|
|
@@ -4623,10 +7357,14 @@ var optionValueIndexes = new Set(
|
|
|
4623
7357
|
"--out-dir",
|
|
4624
7358
|
"--input",
|
|
4625
7359
|
"--output",
|
|
7360
|
+
"--format",
|
|
4626
7361
|
"--root",
|
|
4627
7362
|
"--port",
|
|
4628
7363
|
"--template",
|
|
7364
|
+
"--height",
|
|
4629
7365
|
"--version",
|
|
7366
|
+
"--wait-ms",
|
|
7367
|
+
"--width",
|
|
4630
7368
|
"--work-index"
|
|
4631
7369
|
].flatMap((name) => {
|
|
4632
7370
|
const index = args.indexOf(name);
|
|
@@ -4646,6 +7384,11 @@ var commands = [
|
|
|
4646
7384
|
"dev",
|
|
4647
7385
|
"build",
|
|
4648
7386
|
"package",
|
|
7387
|
+
"capture",
|
|
7388
|
+
"engine.status",
|
|
7389
|
+
"engine.use-local",
|
|
7390
|
+
"engine.unlink",
|
|
7391
|
+
"engine.doctor",
|
|
4649
7392
|
"serve",
|
|
4650
7393
|
"preview",
|
|
4651
7394
|
"run",
|
|
@@ -4706,12 +7449,122 @@ async function run(value) {
|
|
|
4706
7449
|
}
|
|
4707
7450
|
case "package": {
|
|
4708
7451
|
const output = option("--output");
|
|
7452
|
+
const format = option("--format");
|
|
7453
|
+
if (args.includes("--format") && format === void 0) {
|
|
7454
|
+
return {
|
|
7455
|
+
ok: false,
|
|
7456
|
+
error: {
|
|
7457
|
+
code: "cli-parse-error",
|
|
7458
|
+
expected: "--format to be followed by web-zip or single-html",
|
|
7459
|
+
hint: "Pass --format web-zip or --format single-html.",
|
|
7460
|
+
detail: { option: "--format", received: null }
|
|
7461
|
+
}
|
|
7462
|
+
};
|
|
7463
|
+
}
|
|
7464
|
+
if (format !== void 0 && format !== "web-zip" && format !== "single-html") {
|
|
7465
|
+
return {
|
|
7466
|
+
ok: false,
|
|
7467
|
+
error: {
|
|
7468
|
+
code: "cli-parse-error",
|
|
7469
|
+
expected: "--format to be web-zip or single-html",
|
|
7470
|
+
hint: "Use web-zip for HTTPS hosting or single-html for a self-contained file:// delivery.",
|
|
7471
|
+
detail: { option: "--format", received: format }
|
|
7472
|
+
}
|
|
7473
|
+
};
|
|
7474
|
+
}
|
|
4709
7475
|
return packageCommand({
|
|
4710
7476
|
root: projectRoot,
|
|
4711
7477
|
json,
|
|
4712
|
-
...output === void 0 ? {} : { output }
|
|
7478
|
+
...output === void 0 ? {} : { output },
|
|
7479
|
+
...format === void 0 ? {} : { format }
|
|
7480
|
+
});
|
|
7481
|
+
}
|
|
7482
|
+
case "capture": {
|
|
7483
|
+
const parsedPort = parseProjectPortOption(option("--port"), args.includes("--port"));
|
|
7484
|
+
if (!parsedPort.ok) return parsedPort;
|
|
7485
|
+
const software = args.includes("--software");
|
|
7486
|
+
const requestedBackend = option("--backend");
|
|
7487
|
+
if (requestedBackend !== void 0 && requestedBackend !== "auto" && requestedBackend !== "software" && requestedBackend !== "hardware") {
|
|
7488
|
+
return {
|
|
7489
|
+
ok: false,
|
|
7490
|
+
error: {
|
|
7491
|
+
code: "cli-parse-error",
|
|
7492
|
+
expected: "--backend to be auto, software, or hardware",
|
|
7493
|
+
hint: "Use auto for a portable capture, or assert one browser rendering lane explicitly.",
|
|
7494
|
+
detail: { option: "--backend", received: requestedBackend }
|
|
7495
|
+
}
|
|
7496
|
+
};
|
|
7497
|
+
}
|
|
7498
|
+
if (software && requestedBackend !== void 0 && requestedBackend !== "software") {
|
|
7499
|
+
return {
|
|
7500
|
+
ok: false,
|
|
7501
|
+
error: {
|
|
7502
|
+
code: "cli-parse-error",
|
|
7503
|
+
expected: "--software and --backend to select the same capture lane",
|
|
7504
|
+
hint: "Use --backend software (or only the legacy --software flag).",
|
|
7505
|
+
detail: { software: true, backend: requestedBackend }
|
|
7506
|
+
}
|
|
7507
|
+
};
|
|
7508
|
+
}
|
|
7509
|
+
const parseInteger = (name, fallback, minimum, maximum) => {
|
|
7510
|
+
const raw = option(name);
|
|
7511
|
+
if (raw === void 0) return { ok: true, value: fallback };
|
|
7512
|
+
const parsed = Number(raw);
|
|
7513
|
+
return Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum ? { ok: true, value: parsed } : {
|
|
7514
|
+
ok: false,
|
|
7515
|
+
error: {
|
|
7516
|
+
code: "cli-parse-error",
|
|
7517
|
+
expected: `${name} to be an integer from ${minimum} to ${maximum}`,
|
|
7518
|
+
hint: "Pass a bounded deterministic capture value.",
|
|
7519
|
+
detail: { option: name, received: raw }
|
|
7520
|
+
}
|
|
7521
|
+
};
|
|
7522
|
+
};
|
|
7523
|
+
const width = parseInteger("--width", 1280, 64, 8192);
|
|
7524
|
+
if (!width.ok) return width;
|
|
7525
|
+
const height = parseInteger("--height", 720, 64, 8192);
|
|
7526
|
+
if (!height.ok) return height;
|
|
7527
|
+
const waitMs = parseInteger("--wait-ms", 4e3, 0, 12e4);
|
|
7528
|
+
if (!waitMs.ok) return waitMs;
|
|
7529
|
+
const output = option("--output");
|
|
7530
|
+
const browser = option("--browser");
|
|
7531
|
+
return browserCaptureCommand({
|
|
7532
|
+
root: projectRoot,
|
|
7533
|
+
json,
|
|
7534
|
+
backend: requestedBackend ?? (software ? "software" : "auto"),
|
|
7535
|
+
...software ? { software: true } : {},
|
|
7536
|
+
width: width.value,
|
|
7537
|
+
height: height.value,
|
|
7538
|
+
waitMs: waitMs.value,
|
|
7539
|
+
requireUi: args.includes("--require-ui"),
|
|
7540
|
+
deterministic: args.includes("--deterministic"),
|
|
7541
|
+
...args.includes("--headless") ? { headless: true } : {},
|
|
7542
|
+
...parsedPort.value === void 0 ? {} : { port: parsedPort.value },
|
|
7543
|
+
...output === void 0 ? {} : { output },
|
|
7544
|
+
...browser === void 0 ? {} : { browser }
|
|
4713
7545
|
});
|
|
4714
7546
|
}
|
|
7547
|
+
case "engine.status":
|
|
7548
|
+
return engineStatusCommand({ root: scopedRoot});
|
|
7549
|
+
case "engine.use-local": {
|
|
7550
|
+
const path = positionals[0];
|
|
7551
|
+
if (path === void 0) {
|
|
7552
|
+
return {
|
|
7553
|
+
ok: false,
|
|
7554
|
+
error: {
|
|
7555
|
+
code: "cli-parse-error",
|
|
7556
|
+
expected: "forgeax engine use-local <engine-directory>",
|
|
7557
|
+
hint: "Pass the local Engine source checkout or SDK source directory.",
|
|
7558
|
+
detail: {}
|
|
7559
|
+
}
|
|
7560
|
+
};
|
|
7561
|
+
}
|
|
7562
|
+
return engineUseLocalCommand({ root: scopedRoot, path, dryRun, json });
|
|
7563
|
+
}
|
|
7564
|
+
case "engine.unlink":
|
|
7565
|
+
return engineUnlinkCommand({ root: scopedRoot, dryRun});
|
|
7566
|
+
case "engine.doctor":
|
|
7567
|
+
return engineDoctorCommand({ root: scopedRoot});
|
|
4715
7568
|
case "preview": {
|
|
4716
7569
|
const options = projectServerOptions();
|
|
4717
7570
|
return options.ok ? previewCommand(options.value) : options;
|
|
@@ -4892,7 +7745,29 @@ async function run(value) {
|
|
|
4892
7745
|
}
|
|
4893
7746
|
};
|
|
4894
7747
|
}
|
|
4895
|
-
|
|
7748
|
+
const fieldsOption = option("--fields");
|
|
7749
|
+
const fields = fieldsOption?.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
|
|
7750
|
+
const allowedFields = /* @__PURE__ */ new Set(["bindings", "pipeline", "pixels"]);
|
|
7751
|
+
if (fields?.some((field) => !allowedFields.has(field))) {
|
|
7752
|
+
return {
|
|
7753
|
+
ok: false,
|
|
7754
|
+
error: {
|
|
7755
|
+
code: "cli-parse-error",
|
|
7756
|
+
expected: "--fields to contain pipeline, bindings, and/or pixels",
|
|
7757
|
+
hint: "Use a comma-separated subset such as --fields pipeline,bindings.",
|
|
7758
|
+
detail: { fields: fieldsOption }
|
|
7759
|
+
}
|
|
7760
|
+
};
|
|
7761
|
+
}
|
|
7762
|
+
return runRhiDebugCommand(
|
|
7763
|
+
operation,
|
|
7764
|
+
{
|
|
7765
|
+
artifact,
|
|
7766
|
+
workIndex,
|
|
7767
|
+
...fields === void 0 ? {} : { fields }
|
|
7768
|
+
},
|
|
7769
|
+
context
|
|
7770
|
+
);
|
|
4896
7771
|
}
|
|
4897
7772
|
}
|
|
4898
7773
|
}
|