@forgeax/engine-devkit 0.1.7 → 0.1.20
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 +21 -2
- package/dist/.tsbuildinfo +1 -1
- 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/bootstrap-commands.d.ts.map +1 -1
- package/dist/cli-output.d.ts.map +1 -1
- package/dist/cli.mjs +1777 -133
- package/dist/cli.mjs.map +1 -1
- 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/host.d.ts +6 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2317 -1125
- package/dist/index.mjs.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 +33 -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 +38 -0
- package/dist/software-capture.d.ts.map +1 -1
- package/dist/tools/client.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 +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +33 -31
package/dist/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { deflateRawSync } from 'zlib';
|
|
3
3
|
import { createHash, randomUUID } from 'crypto';
|
|
4
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 } from 'path';
|
|
5
|
+
import { resolve, extname, dirname, basename, relative, isAbsolute, sep, join } from 'path';
|
|
6
6
|
import { GameProjectSchema } from '@forgeax/engine-project';
|
|
7
7
|
import { readFileSync, existsSync } from 'fs';
|
|
8
8
|
import { createRequire } from 'module';
|
|
@@ -14,23 +14,25 @@ import { gltfImporter } from '@forgeax/engine-gltf';
|
|
|
14
14
|
import { imageImporter } from '@forgeax/engine-image/image-importer';
|
|
15
15
|
import { BUILTIN_MESH_ASSETS } from '@forgeax/engine-pack/builtin';
|
|
16
16
|
import { scanInventory } from '@forgeax/engine-pack/scanner';
|
|
17
|
-
import { describeResourcePreviewFailure, validateCanonicalKitReceipt,
|
|
17
|
+
import { describeResourcePreviewFailure, validateCanonicalKitReceipt, RESOURCE_PREVIEW_DEFAULT_SIZE, createNativePreviewHost, bindPreviewHost, createResourcePreviewReport, validatePreviewArtifactManifest as validatePreviewArtifactManifest$1 } from '@forgeax/engine-preview';
|
|
18
18
|
import { createMaterialPackCooker } from '@forgeax/engine-shader-compiler';
|
|
19
19
|
import { createStandaloneRuntimeAssetBinding, ok, err } from '@forgeax/engine-types';
|
|
20
20
|
import { createParticleCodeNativeCookerFromRoots } from '@forgeax/engine-vfx-compiler';
|
|
21
21
|
import { pluginPack, reloadAssetHost } from '@forgeax/engine-vite-plugin-pack';
|
|
22
|
+
import { vitePluginRhiDebug } from '@forgeax/engine-vite-plugin-rhi-debug';
|
|
22
23
|
import { forgeaxShader } from '@forgeax/engine-vite-plugin-shader';
|
|
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
30
|
import { spawn, execFile } from 'child_process';
|
|
27
|
-
import { tmpdir } from 'os';
|
|
28
31
|
import { promisify } from 'util';
|
|
29
32
|
import { decodeTape, openReplay, buildFrameModel, replayDeviceRequest, createRhiDebugError } from '@forgeax/engine-rhi-debug';
|
|
30
33
|
import { rhi, createShaderModule } from '@forgeax/engine-rhi-webgpu';
|
|
31
|
-
import { build, preview, createServer as createServer$1 } from 'vite';
|
|
32
34
|
import { createServer } from 'net';
|
|
33
|
-
import { FORGEAX_FRAME_SUBMITTED_DATASET } from '@forgeax/engine-app';
|
|
35
|
+
import { FORGEAX_FRAME_SUBMITTED_DATASET, createToolPreviewRecipe } from '@forgeax/engine-app';
|
|
34
36
|
import { parseImage } from '@forgeax/engine-image/parse-image';
|
|
35
37
|
import { chromium } from 'playwright';
|
|
36
38
|
import { startVitest } from 'vitest/node';
|
|
@@ -38,9 +40,9 @@ import materialPreviewPlugin from '@forgeax/engine-preview/material';
|
|
|
38
40
|
import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
|
|
39
41
|
import texturePreviewPlugin from '@forgeax/engine-preview/texture';
|
|
40
42
|
import vfxPreviewPlugin from '@forgeax/engine-preview/vfx';
|
|
41
|
-
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';
|
|
42
44
|
import { Context, isToolPlugin } from '@forgeax/engine-plugin';
|
|
43
|
-
import { installCatalogLoader, projectPluginEntries } from '@forgeax/engine-plugin/loader';
|
|
45
|
+
import { installCatalogLoader, projectPluginEntries, createContextCapabilityResolver } from '@forgeax/engine-plugin/loader';
|
|
44
46
|
|
|
45
47
|
var __defProp = Object.defineProperty;
|
|
46
48
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -148,6 +150,20 @@ function mediaType(path) {
|
|
|
148
150
|
if (path.endsWith(".wasm")) return "application/wasm";
|
|
149
151
|
if (path.endsWith(".png")) return "image/png";
|
|
150
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";
|
|
151
167
|
return "application/octet-stream";
|
|
152
168
|
}
|
|
153
169
|
async function filesUnder(root, directory = root) {
|
|
@@ -836,6 +852,7 @@ var init_engine_binding = __esm({
|
|
|
836
852
|
// src/host.ts
|
|
837
853
|
var host_exports = {};
|
|
838
854
|
__export(host_exports, {
|
|
855
|
+
createEngineWorkspaceResolverForProject: () => createEngineWorkspaceResolverForProject,
|
|
839
856
|
createViteConfig: () => createViteConfig,
|
|
840
857
|
devKitDdcRoots: () => devKitDdcRoots,
|
|
841
858
|
ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
|
|
@@ -1006,6 +1023,9 @@ async function createEngineWorkspaceResolver(projectRoot2) {
|
|
|
1006
1023
|
}
|
|
1007
1024
|
};
|
|
1008
1025
|
}
|
|
1026
|
+
async function createEngineWorkspaceResolverForProject(projectRoot2) {
|
|
1027
|
+
return createEngineWorkspaceResolver(projectRoot2);
|
|
1028
|
+
}
|
|
1009
1029
|
async function consumerEngineAliases(projectRoot2) {
|
|
1010
1030
|
const root = resolve(projectRoot2, "node_modules", ".pnpm", "node_modules", "@forgeax");
|
|
1011
1031
|
try {
|
|
@@ -1322,6 +1342,44 @@ async function previewOwnerFacts(assets, resource, payload) {
|
|
|
1322
1342
|
return undefined;
|
|
1323
1343
|
}
|
|
1324
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
|
+
|
|
1325
1383
|
async function prepareProject(app) {
|
|
1326
1384
|
await prepareAssetRegistry(app.assets);
|
|
1327
1385
|
previewWorld = app.world;
|
|
@@ -1506,6 +1564,7 @@ await app.pluginContext.plugin(gameHostPlugin({
|
|
|
1506
1564
|
...(defaultSceneRoot === undefined ? {} : { defaultSceneRoot }),
|
|
1507
1565
|
uiRoot: uiRoot instanceof HTMLElement ? uiRoot : document.body,
|
|
1508
1566
|
setPointerLockAllowed: (allowed) => app.input?.setPointerLockAllowed?.(allowed),
|
|
1567
|
+
...(import.meta.env.DEV ? { gameProjection } : {}),
|
|
1509
1568
|
}));
|
|
1510
1569
|
const { loader: pluginLoader } = await installCatalogLoader(app.pluginContext, pluginCatalog, 'engine');
|
|
1511
1570
|
await pluginLoader.root.update(projectPluginEntries(pluginEntries, 'engine'));
|
|
@@ -1542,7 +1601,10 @@ if (query.has('forgeax-tool-replay')) {
|
|
|
1542
1601
|
if (current === undefined) return current;
|
|
1543
1602
|
if (resource?.kind !== 'vfx') {
|
|
1544
1603
|
if (resource?.kind === 'material' || resource?.kind === 'mesh' || resource?.kind === 'texture') {
|
|
1545
|
-
|
|
1604
|
+
const subjectDrawn = resource.kind === 'texture'
|
|
1605
|
+
? app.renderer.drawCalls > 0
|
|
1606
|
+
: app.renderer.drawCalls > 2;
|
|
1607
|
+
if (!subjectDrawn) return current;
|
|
1546
1608
|
if (resource.kind === 'mesh') {
|
|
1547
1609
|
const asset = current.asset;
|
|
1548
1610
|
const aabb = asset?.aabb;
|
|
@@ -1652,6 +1714,7 @@ if (query.has('forgeax-tool-replay')) {
|
|
|
1652
1714
|
);
|
|
1653
1715
|
if (!result.ok) throw result.error;
|
|
1654
1716
|
const app = result.value;
|
|
1717
|
+
if (import.meta.env.DEV) exposeGameInspection(app);
|
|
1655
1718
|
await prepareProject(app);
|
|
1656
1719
|
${bootstrapPlugin}
|
|
1657
1720
|
app.start().unwrap();
|
|
@@ -1796,6 +1859,7 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
|
|
|
1796
1859
|
const consumerAliases = await consumerEngineAliases(facts.root);
|
|
1797
1860
|
const plugins = [
|
|
1798
1861
|
...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
|
|
1862
|
+
...command2 === "serve" && process.env.FORGEAX_ENGINE_RHI_DEBUG === "1" ? [vitePluginRhiDebug({ rootDir: facts.root })] : [],
|
|
1799
1863
|
forgeaxShader(),
|
|
1800
1864
|
pluginPack({
|
|
1801
1865
|
roots,
|
|
@@ -1840,6 +1904,737 @@ var init_host = __esm({
|
|
|
1840
1904
|
hostRequire = createRequire(import.meta.url);
|
|
1841
1905
|
}
|
|
1842
1906
|
});
|
|
1907
|
+
function errorResult(code, expected, hint, detail = {}) {
|
|
1908
|
+
return { ok: false, error: new SingleHtmlError(code, expected, hint, detail) };
|
|
1909
|
+
}
|
|
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
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
function normalizePath(value) {
|
|
1917
|
+
let decoded;
|
|
1918
|
+
try {
|
|
1919
|
+
decoded = decodeURIComponent(value.split(/[?#]/, 1)[0] ?? value);
|
|
1920
|
+
} catch {
|
|
1921
|
+
return void 0;
|
|
1922
|
+
}
|
|
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;
|
|
1929
|
+
}
|
|
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}`)
|
|
2026
|
+
);
|
|
2027
|
+
return suffix.length === 1 ? suffix[0]?.[1] : void 0;
|
|
2028
|
+
}
|
|
2029
|
+
function resourcePath(reference, basePath) {
|
|
2030
|
+
try {
|
|
2031
|
+
return normalizePath(new URL(reference, `https://forgeax.invalid/${basePath}`).pathname);
|
|
2032
|
+
} catch {
|
|
2033
|
+
return void 0;
|
|
2034
|
+
}
|
|
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);
|
|
2040
|
+
}
|
|
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
|
+
}
|
|
2050
|
+
}
|
|
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."
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
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 }
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
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."
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
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 }
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
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
|
+
});
|
|
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}`)
|
|
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
|
+
}
|
|
2353
|
+
}
|
|
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 })]);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
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
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
});
|
|
1843
2638
|
|
|
1844
2639
|
// src/types.ts
|
|
1845
2640
|
function parseProjectPortOption(value, provided) {
|
|
@@ -2118,11 +2913,13 @@ var init_package = __esm({
|
|
|
2118
2913
|
"@forgeax/engine-types": "workspace:*",
|
|
2119
2914
|
"@forgeax/engine-vfx-compiler": "workspace:*",
|
|
2120
2915
|
"@forgeax/engine-vite-plugin-pack": "workspace:*",
|
|
2916
|
+
"@forgeax/engine-vite-plugin-rhi-debug": "workspace:*",
|
|
2121
2917
|
"@forgeax/engine-vite-plugin-shader": "workspace:*",
|
|
2122
2918
|
jiti: "1.21.7",
|
|
2919
|
+
parse5: "7.3.0",
|
|
2123
2920
|
playwright: "1.60.0",
|
|
2124
2921
|
vite: "8.0.10",
|
|
2125
|
-
vitest: "4.
|
|
2922
|
+
vitest: "4.1.11",
|
|
2126
2923
|
webgpu: "^0.4.0"
|
|
2127
2924
|
},
|
|
2128
2925
|
devDependencies: {
|
|
@@ -2297,7 +3094,7 @@ var init_init = __esm({
|
|
|
2297
3094
|
"@webgpu/types": "0.1.71",
|
|
2298
3095
|
tsx: "4.23.1",
|
|
2299
3096
|
typescript: "6.0.3",
|
|
2300
|
-
vitest: "4.
|
|
3097
|
+
vitest: "4.1.11"
|
|
2301
3098
|
};
|
|
2302
3099
|
}
|
|
2303
3100
|
});
|
|
@@ -2319,15 +3116,12 @@ async function findSdkContext() {
|
|
|
2319
3116
|
const templates = new Map(
|
|
2320
3117
|
manifest.templates.map((template) => [template.id, resolve(cursor, template.root)])
|
|
2321
3118
|
);
|
|
2322
|
-
const defaultTemplate = manifest.templates.find((template) => template.default);
|
|
2323
|
-
if (defaultTemplate === void 0) throw new Error("sdk-default-template-missing");
|
|
2324
3119
|
const store = resolve(cursor, "store", "pnpm");
|
|
2325
3120
|
return {
|
|
2326
3121
|
root: cursor,
|
|
2327
3122
|
manifest,
|
|
2328
3123
|
...await readable(store) ? { store } : {},
|
|
2329
|
-
templates
|
|
2330
|
-
defaultTemplate: defaultTemplate.id
|
|
3124
|
+
templates
|
|
2331
3125
|
};
|
|
2332
3126
|
}
|
|
2333
3127
|
const parent = dirname(cursor);
|
|
@@ -2347,10 +3141,14 @@ function agentOnboarding(sdk, projectRoot2) {
|
|
|
2347
3141
|
resolve(root, "skills", "forgeax-engine-sdk", "SKILL.md"),
|
|
2348
3142
|
resolve(root, "skills", "forgeax-engine-sdk", "references", "feature-catalog.md")
|
|
2349
3143
|
],
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
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"] } }
|
|
2354
3152
|
};
|
|
2355
3153
|
}
|
|
2356
3154
|
function sdkInitPath(sdk) {
|
|
@@ -2432,8 +3230,8 @@ async function requireSdkInitialization(sdk) {
|
|
|
2432
3230
|
return { ok: true, value: state };
|
|
2433
3231
|
}
|
|
2434
3232
|
async function copyBootstrapInputs(sdk, root) {
|
|
2435
|
-
const template = sdk.templates.get(
|
|
2436
|
-
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");
|
|
2437
3235
|
for (const name of ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"]) {
|
|
2438
3236
|
await cp(resolve(template, name), resolve(root, name));
|
|
2439
3237
|
}
|
|
@@ -2625,13 +3423,13 @@ async function pathKind(path) {
|
|
|
2625
3423
|
throw cause;
|
|
2626
3424
|
}
|
|
2627
3425
|
}
|
|
2628
|
-
async function
|
|
3426
|
+
async function filesUnder3(root, directory = root) {
|
|
2629
3427
|
const files = [];
|
|
2630
3428
|
for (const name of (await readdir(directory)).sort()) {
|
|
2631
3429
|
const path = resolve(directory, name);
|
|
2632
3430
|
const info = await lstat(path);
|
|
2633
3431
|
if (info.isSymbolicLink()) throw new Error(`skill-source-symlink: ${path}`);
|
|
2634
|
-
if (info.isDirectory()) files.push(...await
|
|
3432
|
+
if (info.isDirectory()) files.push(...await filesUnder3(root, path));
|
|
2635
3433
|
else if (info.isFile()) files.push(path);
|
|
2636
3434
|
}
|
|
2637
3435
|
return files;
|
|
@@ -2643,7 +3441,7 @@ async function skillRow(sourceRoot, id) {
|
|
|
2643
3441
|
if (await pathKind(resolve(root, "SKILL.md")) !== "file") {
|
|
2644
3442
|
throw new Error(`skill-entry-missing: ${id}/SKILL.md`);
|
|
2645
3443
|
}
|
|
2646
|
-
const files = await
|
|
3444
|
+
const files = await filesUnder3(root);
|
|
2647
3445
|
return {
|
|
2648
3446
|
id,
|
|
2649
3447
|
root: `skills/${id}`,
|
|
@@ -2660,8 +3458,8 @@ async function discoverProjectSkills(root) {
|
|
|
2660
3458
|
return Promise.all(ids.map((id) => skillRow(sourceRoot, id)));
|
|
2661
3459
|
}
|
|
2662
3460
|
async function sameFiles(leftRoot, rightRoot) {
|
|
2663
|
-
const left = await
|
|
2664
|
-
const right = await
|
|
3461
|
+
const left = await filesUnder3(leftRoot);
|
|
3462
|
+
const right = await filesUnder3(rightRoot);
|
|
2665
3463
|
const leftNames = left.map((path) => slash(relative(leftRoot, path)));
|
|
2666
3464
|
const rightNames = right.map((path) => slash(relative(rightRoot, path)));
|
|
2667
3465
|
if (JSON.stringify(leftNames) !== JSON.stringify(rightNames)) return false;
|
|
@@ -3085,8 +3883,8 @@ async function initCommand(options = {}) {
|
|
|
3085
3883
|
const applied = await applyInitPlan(facts.value, plan.value, options);
|
|
3086
3884
|
if (!applied.ok || options.dryRun === true) return applied;
|
|
3087
3885
|
if (sdk !== void 0) {
|
|
3088
|
-
const template = sdk.templates.get(
|
|
3089
|
-
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");
|
|
3090
3888
|
await copyFile(
|
|
3091
3889
|
resolve(template, "pnpm-lock.yaml"),
|
|
3092
3890
|
resolve(facts.value.root, "pnpm-lock.yaml")
|
|
@@ -3154,7 +3952,18 @@ async function newCommand(options = {}) {
|
|
|
3154
3952
|
}
|
|
3155
3953
|
};
|
|
3156
3954
|
}
|
|
3157
|
-
|
|
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;
|
|
3158
3967
|
const template = sdk.templates.get(templateId);
|
|
3159
3968
|
if (template === void 0) {
|
|
3160
3969
|
return {
|
|
@@ -4072,28 +4881,32 @@ function observedBackend(runtime) {
|
|
|
4072
4881
|
return isSoftwareAdapter(runtime) ? "software" : "hardware";
|
|
4073
4882
|
}
|
|
4074
4883
|
async function resolveBrowserExecutable(requested) {
|
|
4884
|
+
if (requested !== void 0 && requested.length > 0) {
|
|
4885
|
+
return await pathExists2(requested) ? requested : void 0;
|
|
4886
|
+
}
|
|
4075
4887
|
const candidates = [
|
|
4076
|
-
requested,
|
|
4077
4888
|
process.env.FORGEAX_BROWSER_EXECUTABLE,
|
|
4078
4889
|
"/opt/google/chrome-beta/chrome",
|
|
4079
4890
|
"/usr/bin/google-chrome",
|
|
4080
4891
|
"/usr/bin/google-chrome-stable",
|
|
4081
4892
|
"/usr/bin/chromium",
|
|
4082
|
-
"/usr/bin/chromium-browser"
|
|
4893
|
+
"/usr/bin/chromium-browser",
|
|
4894
|
+
typeof chromium.executablePath === "function" ? chromium.executablePath() : void 0
|
|
4083
4895
|
].filter((candidate) => candidate !== void 0 && candidate.length > 0);
|
|
4084
4896
|
for (const candidate of candidates) {
|
|
4085
4897
|
if (await pathExists2(candidate)) return candidate;
|
|
4086
4898
|
}
|
|
4087
4899
|
return void 0;
|
|
4088
4900
|
}
|
|
4089
|
-
function browserLaunchArgs(backend) {
|
|
4090
|
-
const common = [
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
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
|
+
}
|
|
4097
4910
|
if (backend !== "software") return common;
|
|
4098
4911
|
return [
|
|
4099
4912
|
...common,
|
|
@@ -4184,6 +4997,7 @@ async function runtimeWitness(page) {
|
|
|
4184
4997
|
adapterError = String(cause);
|
|
4185
4998
|
}
|
|
4186
4999
|
const frameId = Number(document.documentElement.dataset[datasetKey]);
|
|
5000
|
+
const singleHtmlState = globalThis.__forgeaxSingleHtml;
|
|
4187
5001
|
return {
|
|
4188
5002
|
title: document.title,
|
|
4189
5003
|
canvas: canvas instanceof HTMLCanvasElement ? { width: canvas.width, height: canvas.height } : null,
|
|
@@ -4196,6 +5010,7 @@ async function runtimeWitness(page) {
|
|
|
4196
5010
|
adapterError,
|
|
4197
5011
|
engineFrameId: Number.isSafeInteger(frameId) && frameId > 0 ? frameId : null,
|
|
4198
5012
|
captureReady: document.documentElement.dataset.forgeaxCaptureReady ?? null,
|
|
5013
|
+
singleHtml: singleHtmlState?.witness?.() ?? null,
|
|
4199
5014
|
userAgent: navigator.userAgent
|
|
4200
5015
|
};
|
|
4201
5016
|
}, FORGEAX_FRAME_SUBMITTED_DATASET);
|
|
@@ -4211,8 +5026,81 @@ async function writeRunReport(report) {
|
|
|
4211
5026
|
await writeFile(report.report, `${JSON.stringify(report, null, 2)}
|
|
4212
5027
|
`, "utf8");
|
|
4213
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
|
+
}
|
|
4214
5078
|
async function openBrowserCaptureSession(root, options) {
|
|
4215
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
|
+
}
|
|
4216
5104
|
if (options.software === true && options.backend !== void 0 && options.backend !== "software") {
|
|
4217
5105
|
fail(
|
|
4218
5106
|
"browser-capture-option-conflict",
|
|
@@ -4250,27 +5138,32 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4250
5138
|
facts.value.root,
|
|
4251
5139
|
options.report ?? resolve(outputDirectory, "run.json")
|
|
4252
5140
|
);
|
|
4253
|
-
const port = options.port === void 0 || options.port === 0 ? await reservePort() : options.port;
|
|
5141
|
+
const port = target.kind === "project" ? options.port === void 0 || options.port === 0 ? await reservePort() : options.port : void 0;
|
|
4254
5142
|
let server;
|
|
4255
5143
|
let browser;
|
|
4256
5144
|
let page;
|
|
4257
5145
|
let display;
|
|
4258
5146
|
try {
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
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
|
+
}
|
|
4266
5157
|
display = await startVirtualDisplay(width, height);
|
|
4267
5158
|
const captureDisplay = display;
|
|
4268
5159
|
const icd = await lavapipeIcd();
|
|
4269
5160
|
const consoleErrors = [];
|
|
4270
5161
|
const pageErrors = [];
|
|
5162
|
+
const requestRecords = [];
|
|
5163
|
+
const requestByObject = /* @__PURE__ */ new Map();
|
|
4271
5164
|
const hasDisplay = display.value !== void 0;
|
|
4272
5165
|
const headless = options.headless ?? (!hasDisplay && process.platform !== "linux");
|
|
4273
|
-
const baseUrl = new URL(server
|
|
5166
|
+
const baseUrl = target.kind === "project" ? new URL(server?.resolvedUrls?.local[0] ?? `http://127.0.0.1:${port ?? 0}/`) : void 0;
|
|
4274
5167
|
const openPage = async (launchBackend) => {
|
|
4275
5168
|
const browserEnvironment = {
|
|
4276
5169
|
...Object.fromEntries(
|
|
@@ -4284,7 +5177,7 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4284
5177
|
const launchOptions = {
|
|
4285
5178
|
headless,
|
|
4286
5179
|
env: browserEnvironment,
|
|
4287
|
-
args: browserLaunchArgs(launchBackend),
|
|
5180
|
+
args: browserLaunchArgs(launchBackend, launchProfile),
|
|
4288
5181
|
...browserPath === void 0 ? {} : { executablePath: browserPath }
|
|
4289
5182
|
};
|
|
4290
5183
|
browser = await chromium.launch(launchOptions);
|
|
@@ -4301,7 +5194,14 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4301
5194
|
if (message.type() === "error") consoleErrors.push(message.text());
|
|
4302
5195
|
});
|
|
4303
5196
|
page.on("pageerror", (error) => pageErrors.push(String(error)));
|
|
4304
|
-
const captureUrl2 = new URL(
|
|
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));
|
|
4305
5205
|
if (options.deterministic === true) captureUrl2.searchParams.set("forgeaxCapture", "1");
|
|
4306
5206
|
await page.goto(captureUrl2.href, { waitUntil: "domcontentloaded", timeout: 12e4 });
|
|
4307
5207
|
await page.waitForFunction(
|
|
@@ -4327,10 +5227,12 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4327
5227
|
page = void 0;
|
|
4328
5228
|
consoleErrors.length = 0;
|
|
4329
5229
|
pageErrors.length = 0;
|
|
5230
|
+
requestRecords.length = 0;
|
|
5231
|
+
requestByObject.clear();
|
|
4330
5232
|
runtime = await openPage("software");
|
|
4331
5233
|
observed = observedBackend(runtime);
|
|
4332
5234
|
}
|
|
4333
|
-
const captureUrl = new URL(
|
|
5235
|
+
const captureUrl = target.kind === "single-html" ? new URL(pathToFileURL(target.path).href) : new URL(baseUrl?.href ?? "http://127.0.0.1/");
|
|
4334
5236
|
if (options.deterministic === true) captureUrl.searchParams.set("forgeaxCapture", "1");
|
|
4335
5237
|
if (page === void 0 || browser === void 0) {
|
|
4336
5238
|
fail(
|
|
@@ -4340,6 +5242,7 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4340
5242
|
);
|
|
4341
5243
|
}
|
|
4342
5244
|
const captures = [];
|
|
5245
|
+
let latestRuntime = runtime;
|
|
4343
5246
|
let closed = false;
|
|
4344
5247
|
const report = {
|
|
4345
5248
|
schemaVersion: "2.0.0",
|
|
@@ -4348,6 +5251,8 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4348
5251
|
mode: "browser-compositor",
|
|
4349
5252
|
root: facts.value.root,
|
|
4350
5253
|
url: captureUrl.href,
|
|
5254
|
+
target,
|
|
5255
|
+
launchProfile,
|
|
4351
5256
|
report: reportPath,
|
|
4352
5257
|
backendRequested: backend,
|
|
4353
5258
|
backend: observed,
|
|
@@ -4366,14 +5271,19 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4366
5271
|
display: display.value ?? null,
|
|
4367
5272
|
lavapipeIcd: icd ?? null,
|
|
4368
5273
|
captures,
|
|
5274
|
+
requests: summarizeRequests(requestRecords),
|
|
5275
|
+
singleHtml: runtime.singleHtml,
|
|
4369
5276
|
consoleErrors,
|
|
4370
5277
|
pageErrors,
|
|
4371
5278
|
boundary: "Browser-compositor capture is visual iteration evidence, not physical-GPU performance, HDR-display output, or release acceptance."
|
|
4372
5279
|
};
|
|
4373
5280
|
await writeRunReport(report);
|
|
4374
5281
|
const updateReport = async () => {
|
|
5282
|
+
const requests = summarizeRequests(requestRecords);
|
|
4375
5283
|
Object.assign(report, {
|
|
4376
|
-
|
|
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)
|
|
4377
5287
|
});
|
|
4378
5288
|
await writeRunReport(report);
|
|
4379
5289
|
};
|
|
@@ -4438,12 +5348,14 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4438
5348
|
}
|
|
4439
5349
|
}
|
|
4440
5350
|
const runtime2 = await runtimeWitness(activePage);
|
|
5351
|
+
latestRuntime = runtime2;
|
|
5352
|
+
const requestSummary = summarizeRequests(requestRecords);
|
|
4441
5353
|
const uiPresent = runtime2.domUi.rootChildren > 0;
|
|
4442
5354
|
const requireUi = captureOptions.requireUi ?? options.requireUi ?? false;
|
|
4443
5355
|
const captureBackend = observedBackend(runtime2);
|
|
4444
5356
|
if (captureBackend !== "unknown") Object.assign(report, { backend: captureBackend });
|
|
4445
5357
|
const backendMatches = backend === "auto" || captureBackend === backend;
|
|
4446
|
-
const ok2 = runtime2.canvas !== null && captured.pixels.rendered && backendMatches && runtime2.engineFrameId !== null && consoleErrors.length === 0 && pageErrors.length === 0 && (expectedReady === void 0 || runtime2.captureReady === expectedReady) && (!requireUi || uiPresent);
|
|
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);
|
|
4447
5359
|
const index = captures.length + 1;
|
|
4448
5360
|
const output = resolve(
|
|
4449
5361
|
facts.value.root,
|
|
@@ -4649,31 +5561,47 @@ function releaseSlug(name) {
|
|
|
4649
5561
|
async function packageCommand(options = {}) {
|
|
4650
5562
|
const facts = await readProjectFacts(options.root);
|
|
4651
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
|
+
}
|
|
4652
5585
|
const built = await buildCommand({
|
|
4653
5586
|
root: facts.value.root,
|
|
4654
5587
|
base: "./",
|
|
4655
5588
|
...options.json === void 0 ? {} : { json: options.json }
|
|
4656
5589
|
});
|
|
4657
5590
|
if (!built.ok) return built;
|
|
4658
|
-
const distRoot = resolve(facts.value.root, "dist");
|
|
4659
5591
|
const verified = await verifyDist(distRoot);
|
|
4660
5592
|
if (!verified.ok) return verified;
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
expected: "the release archive to live outside the derived dist directory",
|
|
4672
|
-
hint: "Use --output release/<game>-web.zip or another path outside dist.",
|
|
4673
|
-
detail: { archive, distRoot }
|
|
4674
|
-
}
|
|
4675
|
-
};
|
|
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
|
+
});
|
|
4676
5603
|
}
|
|
5604
|
+
const archive = output;
|
|
4677
5605
|
const temporaryArchive = `${archive}.partial-${process.pid}`;
|
|
4678
5606
|
const checksumPath = `${archive}.sha256`;
|
|
4679
5607
|
const temporaryChecksum = `${checksumPath}.partial-${process.pid}`;
|
|
@@ -4686,11 +5614,11 @@ async function packageCommand(options = {}) {
|
|
|
4686
5614
|
paths.map(async (path) => ({ path, bytes: await readFile(resolve(distRoot, path)) }))
|
|
4687
5615
|
);
|
|
4688
5616
|
const bytes = createZip(entries2);
|
|
4689
|
-
const
|
|
5617
|
+
const sha2562 = createHash("sha256").update(bytes).digest("hex");
|
|
4690
5618
|
const manifestBytes = await readFile(resolve(distRoot, "forgeax-dist.json"));
|
|
4691
5619
|
await mkdir(dirname(archive), { recursive: true });
|
|
4692
5620
|
await writeFile(temporaryArchive, bytes);
|
|
4693
|
-
await writeFile(temporaryChecksum, `${
|
|
5621
|
+
await writeFile(temporaryChecksum, `${sha2562} ${basename(archive)}
|
|
4694
5622
|
`);
|
|
4695
5623
|
await rename(temporaryChecksum, checksumPath);
|
|
4696
5624
|
await rename(temporaryArchive, archive);
|
|
@@ -4706,7 +5634,7 @@ async function packageCommand(options = {}) {
|
|
|
4706
5634
|
delivery: "bundled-runtime",
|
|
4707
5635
|
wasm: verified.value.artifacts.some((artifact) => artifact.path.endsWith(".wasm"))
|
|
4708
5636
|
},
|
|
4709
|
-
archive: { path: archive, bytes: bytes.byteLength, sha256 },
|
|
5637
|
+
archive: { path: archive, bytes: bytes.byteLength, sha256: sha2562 },
|
|
4710
5638
|
checksumPath,
|
|
4711
5639
|
distManifestSha256: createHash("sha256").update(manifestBytes).digest("hex"),
|
|
4712
5640
|
run: {
|
|
@@ -4730,12 +5658,12 @@ async function devCommand(options = {}) {
|
|
|
4730
5658
|
const previous = process.cwd();
|
|
4731
5659
|
let server;
|
|
4732
5660
|
try {
|
|
4733
|
-
const { createServer:
|
|
5661
|
+
const { createServer: createServer3 } = await import('vite');
|
|
4734
5662
|
const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_host(), host_exports));
|
|
4735
5663
|
const port = resolveProjectPort(options.port);
|
|
4736
5664
|
const vitePort = { ...port, port: await materializeViteDevPort(port.port) };
|
|
4737
5665
|
process.chdir(facts.value.root);
|
|
4738
|
-
server = await
|
|
5666
|
+
server = await createServer3(
|
|
4739
5667
|
await createViteConfig2(facts.value, "serve", "/", {
|
|
4740
5668
|
server: vitePort
|
|
4741
5669
|
})
|
|
@@ -4843,6 +5771,7 @@ var init_commands = __esm({
|
|
|
4843
5771
|
init_dist();
|
|
4844
5772
|
init_host();
|
|
4845
5773
|
init_project();
|
|
5774
|
+
init_single_html();
|
|
4846
5775
|
init_types();
|
|
4847
5776
|
init_assets();
|
|
4848
5777
|
init_bootstrap_commands();
|
|
@@ -4857,6 +5786,19 @@ var init_commands = __esm({
|
|
|
4857
5786
|
init_operations();
|
|
4858
5787
|
}
|
|
4859
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
|
+
}
|
|
4860
5802
|
var nativePreviewPlugins, nativePreviewTools, nativePreviewDescriptors;
|
|
4861
5803
|
var init_preview_catalog = __esm({
|
|
4862
5804
|
"src/tools/preview-catalog.ts"() {
|
|
@@ -5331,11 +6273,11 @@ async function runProjectTool(binding, args2, options) {
|
|
|
5331
6273
|
);
|
|
5332
6274
|
await loader.root.update(entries2);
|
|
5333
6275
|
await loader.await();
|
|
5334
|
-
const { createToolRuntime:
|
|
5335
|
-
const { createContextCapabilityResolver } = await import('@forgeax/engine-plugin');
|
|
5336
|
-
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, {
|
|
5337
6279
|
...options,
|
|
5338
|
-
capabilityResolver:
|
|
6280
|
+
capabilityResolver: createContextCapabilityResolver2(ctx)
|
|
5339
6281
|
}).terminal;
|
|
5340
6282
|
} catch (cause) {
|
|
5341
6283
|
return activationFailure(cause);
|
|
@@ -5347,6 +6289,50 @@ var init_project_tools = __esm({
|
|
|
5347
6289
|
"src/tools/project-tools.ts"() {
|
|
5348
6290
|
}
|
|
5349
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
|
+
});
|
|
5350
6336
|
function analyzePreviewArtifacts(request) {
|
|
5351
6337
|
const required = request.required ?? [];
|
|
5352
6338
|
const validated = request.manifest.schemaVersion === "1.0.0" ? required.some((kind) => kind !== "rhi-tape" && kind !== "png" && kind !== "profile-capture") ? {
|
|
@@ -5357,7 +6343,7 @@ function analyzePreviewArtifacts(request) {
|
|
|
5357
6343
|
hint: "Request one of rhi-tape, png, or profile-capture for a v1 manifest.",
|
|
5358
6344
|
detail: { reason: "unsupported required evidence kind" }
|
|
5359
6345
|
}
|
|
5360
|
-
} : validateArtifactManifest(request.manifest, required) : validatePreviewArtifactManifest(
|
|
6346
|
+
} : validateArtifactManifest(request.manifest, required) : validatePreviewArtifactManifest$1(
|
|
5361
6347
|
request.manifest,
|
|
5362
6348
|
(request.required ?? []).filter(
|
|
5363
6349
|
(role) => role === "report" || role === "rhi-tape" || role === "capture" || role === "fresh-replay" || role === "profile-capture" || role === "contact-sheet"
|
|
@@ -5397,14 +6383,6 @@ function createPreviewContributions() {
|
|
|
5397
6383
|
createOfflineAnalysisContribution()
|
|
5398
6384
|
];
|
|
5399
6385
|
}
|
|
5400
|
-
function createDomainPreviewContributions() {
|
|
5401
|
-
return [
|
|
5402
|
-
createMaterialPreviewContribution(),
|
|
5403
|
-
createMeshPreviewContribution(),
|
|
5404
|
-
createVfxPreviewContribution(),
|
|
5405
|
-
createTexturePreviewContribution()
|
|
5406
|
-
];
|
|
5407
|
-
}
|
|
5408
6386
|
var init_preview_contributions = __esm({
|
|
5409
6387
|
"src/tools/preview-contributions.ts"() {
|
|
5410
6388
|
init_catalog();
|
|
@@ -5413,50 +6391,6 @@ var init_preview_contributions = __esm({
|
|
|
5413
6391
|
}
|
|
5414
6392
|
});
|
|
5415
6393
|
|
|
5416
|
-
// src/tools/contributions.ts
|
|
5417
|
-
var contributions_exports = {};
|
|
5418
|
-
__export(contributions_exports, {
|
|
5419
|
-
createAuthorContribution: () => createAuthorContribution,
|
|
5420
|
-
createBuildContribution: () => createBuildContribution,
|
|
5421
|
-
createDefaultContributions: () => createDefaultContributions
|
|
5422
|
-
});
|
|
5423
|
-
function commandFailure2(error) {
|
|
5424
|
-
return { ok: false, error: { ...error, detail: error.detail } };
|
|
5425
|
-
}
|
|
5426
|
-
function createBuildContribution(projectRoot2 = process.cwd()) {
|
|
5427
|
-
return defineTool(
|
|
5428
|
-
projectBuildDescriptor,
|
|
5429
|
-
async (options) => {
|
|
5430
|
-
const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5431
|
-
const result = await buildCommand2({ ...options, root: options.root ?? projectRoot2 });
|
|
5432
|
-
return result.ok ? result.value : commandFailure2(result.error);
|
|
5433
|
-
}
|
|
5434
|
-
);
|
|
5435
|
-
}
|
|
5436
|
-
function createAuthorContribution(projectRoot2 = process.cwd()) {
|
|
5437
|
-
return defineTool(
|
|
5438
|
-
authorPluginInstallDescriptor,
|
|
5439
|
-
async (options) => {
|
|
5440
|
-
const { pluginInstallCommand: pluginInstallCommand2 } = await Promise.resolve().then(() => (init_plugin_authoring(), plugin_authoring_exports));
|
|
5441
|
-
const result = await pluginInstallCommand2({ ...options, root: options.root ?? projectRoot2 });
|
|
5442
|
-
return result.ok ? result.value : commandFailure2(result.error);
|
|
5443
|
-
}
|
|
5444
|
-
);
|
|
5445
|
-
}
|
|
5446
|
-
function createDefaultContributions(projectRoot2 = process.cwd()) {
|
|
5447
|
-
return [
|
|
5448
|
-
createBuildContribution(projectRoot2),
|
|
5449
|
-
createAuthorContribution(projectRoot2),
|
|
5450
|
-
...createDomainPreviewContributions()
|
|
5451
|
-
];
|
|
5452
|
-
}
|
|
5453
|
-
var init_contributions = __esm({
|
|
5454
|
-
"src/tools/contributions.ts"() {
|
|
5455
|
-
init_catalog();
|
|
5456
|
-
init_preview_contributions();
|
|
5457
|
-
}
|
|
5458
|
-
});
|
|
5459
|
-
|
|
5460
6394
|
// src/tools/runtime.ts
|
|
5461
6395
|
var runtime_exports = {};
|
|
5462
6396
|
__export(runtime_exports, {
|
|
@@ -5474,6 +6408,679 @@ var init_runtime = __esm({
|
|
|
5474
6408
|
init_preview_contributions();
|
|
5475
6409
|
}
|
|
5476
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
|
+
});
|
|
5477
7084
|
|
|
5478
7085
|
// src/cli-output.ts
|
|
5479
7086
|
function agentOnboardingLines(value) {
|
|
@@ -5482,6 +7089,12 @@ function agentOnboardingLines(value) {
|
|
|
5482
7089
|
if (onboarding === null || typeof onboarding !== "object" || Array.isArray(onboarding)) return [];
|
|
5483
7090
|
const candidate = onboarding;
|
|
5484
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
|
+
}
|
|
5485
7098
|
if (typeof candidate.next?.cwd === "string" && Array.isArray(candidate.next.argv) && candidate.next.argv.every((part) => typeof part === "string")) {
|
|
5486
7099
|
lines.push(`[forgeax] next cwd: ${candidate.next.cwd}`);
|
|
5487
7100
|
lines.push(`[forgeax] next: ${candidate.next.argv.join(" ")}`);
|
|
@@ -5502,7 +7115,7 @@ function sdkUpdateLines(value) {
|
|
|
5502
7115
|
];
|
|
5503
7116
|
}
|
|
5504
7117
|
function renderForgeaxUsage() {
|
|
5505
|
-
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";
|
|
5506
7119
|
}
|
|
5507
7120
|
|
|
5508
7121
|
// src/cli.ts
|
|
@@ -5573,7 +7186,13 @@ async function createToolClient(options) {
|
|
|
5573
7186
|
const contribution = runtime.get(id);
|
|
5574
7187
|
if (contribution === void 0) return missingTool(id);
|
|
5575
7188
|
const binding = bindingById.get(id);
|
|
5576
|
-
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);
|
|
5577
7196
|
return decorateResourcePreviewTerminal(terminal);
|
|
5578
7197
|
}
|
|
5579
7198
|
};
|
|
@@ -5738,6 +7357,7 @@ var optionValueIndexes = new Set(
|
|
|
5738
7357
|
"--out-dir",
|
|
5739
7358
|
"--input",
|
|
5740
7359
|
"--output",
|
|
7360
|
+
"--format",
|
|
5741
7361
|
"--root",
|
|
5742
7362
|
"--port",
|
|
5743
7363
|
"--template",
|
|
@@ -5829,10 +7449,34 @@ async function run(value) {
|
|
|
5829
7449
|
}
|
|
5830
7450
|
case "package": {
|
|
5831
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
|
+
}
|
|
5832
7475
|
return packageCommand({
|
|
5833
7476
|
root: projectRoot,
|
|
5834
7477
|
json,
|
|
5835
|
-
...output === void 0 ? {} : { output }
|
|
7478
|
+
...output === void 0 ? {} : { output },
|
|
7479
|
+
...format === void 0 ? {} : { format }
|
|
5836
7480
|
});
|
|
5837
7481
|
}
|
|
5838
7482
|
case "capture": {
|