@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/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { deflateRawSync } from 'zlib';
|
|
2
2
|
import { createHash, randomUUID } from 'crypto';
|
|
3
|
-
import { readFile, writeFile, access, readdir, stat, realpath, rm, mkdir,
|
|
4
|
-
import { resolve, relative, sep, isAbsolute,
|
|
3
|
+
import { readFile, writeFile, access, readdir, stat, realpath, rm, mkdir, mkdtemp, rename, copyFile, cp, symlink, unlink, lstat, readlink, rmdir } from 'fs/promises';
|
|
4
|
+
import { resolve, relative, sep, isAbsolute, dirname, basename, extname, join } from 'path';
|
|
5
5
|
import { GameProjectSchema } from '@forgeax/engine-project';
|
|
6
6
|
import { existsSync } from 'fs';
|
|
7
7
|
import { createRequire } from 'module';
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
8
|
+
import { pathToFileURL, fileURLToPath } from 'url';
|
|
9
9
|
import { audioImporter } from '@forgeax/engine-audio-webaudio/audio-importer';
|
|
10
10
|
import { fbxImporter } from '@forgeax/engine-fbx';
|
|
11
11
|
import { fontImporter } from '@forgeax/engine-font/font-importer';
|
|
@@ -13,21 +13,23 @@ import { gltfImporter } from '@forgeax/engine-gltf';
|
|
|
13
13
|
import { imageImporter } from '@forgeax/engine-image/image-importer';
|
|
14
14
|
import { BUILTIN_MESH_ASSETS } from '@forgeax/engine-pack/builtin';
|
|
15
15
|
import { scanInventory } from '@forgeax/engine-pack/scanner';
|
|
16
|
-
import { validatePreviewArtifactManifest,
|
|
16
|
+
import { validatePreviewArtifactManifest, RESOURCE_PREVIEW_DEFAULT_SIZE, createNativePreviewHost, validateCanonicalKitReceipt, bindPreviewHost, createResourcePreviewReport, describeResourcePreviewFailure } from '@forgeax/engine-preview';
|
|
17
17
|
import { createMaterialPackCooker } from '@forgeax/engine-shader-compiler';
|
|
18
18
|
import { createStandaloneRuntimeAssetBinding, err, ok } from '@forgeax/engine-types';
|
|
19
19
|
import { createParticleCodeNativeCookerFromRoots } from '@forgeax/engine-vfx-compiler';
|
|
20
20
|
import { pluginPack, reloadAssetHost } from '@forgeax/engine-vite-plugin-pack';
|
|
21
|
+
import { vitePluginRhiDebug } from '@forgeax/engine-vite-plugin-rhi-debug';
|
|
21
22
|
import { forgeaxShader } from '@forgeax/engine-vite-plugin-shader';
|
|
23
|
+
import { tmpdir } from 'os';
|
|
24
|
+
import { parse } from 'parse5';
|
|
25
|
+
import { build, preview, createServer as createServer$2, parseAst, Visitor } from 'vite';
|
|
22
26
|
import { runCliGltf } from '@forgeax/engine-gltf/cli-gltf';
|
|
23
27
|
import { scanEntries } from '@forgeax/engine-pack/cli-asset';
|
|
24
28
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
25
29
|
import { execFile, spawn } from 'child_process';
|
|
26
|
-
import { tmpdir } from 'os';
|
|
27
30
|
import { promisify } from 'util';
|
|
28
31
|
import { replayDeviceRequest, createRhiDebugError, decodeTape, openReplay, buildFrameModel } from '@forgeax/engine-rhi-debug';
|
|
29
32
|
import { rhi, createShaderModule } from '@forgeax/engine-rhi-webgpu';
|
|
30
|
-
import { build, preview, createServer as createServer$2 } from 'vite';
|
|
31
33
|
import { createServer as createServer$1 } from 'net';
|
|
32
34
|
import { createToolPreviewRecipe, createToolPreviewHost, FORGEAX_FRAME_SUBMITTED_DATASET } from '@forgeax/engine-app';
|
|
33
35
|
import { parseImage } from '@forgeax/engine-image/parse-image';
|
|
@@ -37,10 +39,10 @@ import materialPreviewPlugin from '@forgeax/engine-preview/material';
|
|
|
37
39
|
import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
|
|
38
40
|
import texturePreviewPlugin from '@forgeax/engine-preview/texture';
|
|
39
41
|
import vfxPreviewPlugin from '@forgeax/engine-preview/vfx';
|
|
40
|
-
import {
|
|
42
|
+
import { defineTool, validateArtifactManifest, createToolRuntime, validateRealmBootstrapPayload, createCarrierStateMachine, capabilityUnavailableError, createServiceCapability, cancellationError, disconnectedError, domainFailureError, createAuthenticatedLoopbackTransport, createPreviewArtifactManifest, validatePreviewArtifactManifest as validatePreviewArtifactManifest$1 } from '@forgeax/engine-tool-runtime';
|
|
41
43
|
export { createAuthenticatedLoopbackTransport, createCapabilityToken, createMigrationRecipe, createRealmCapabilityMatrix, createServiceCapability, probeMigrationTarget } from '@forgeax/engine-tool-runtime';
|
|
42
44
|
import { Context, isToolPlugin } from '@forgeax/engine-plugin';
|
|
43
|
-
import { installCatalogLoader, projectPluginEntries, bootstrapCatalogLoader } from '@forgeax/engine-plugin/loader';
|
|
45
|
+
import { installCatalogLoader, projectPluginEntries, createContextCapabilityResolver, bootstrapCatalogLoader } from '@forgeax/engine-plugin/loader';
|
|
44
46
|
import { createServer } from 'http';
|
|
45
47
|
|
|
46
48
|
var __defProp = Object.defineProperty;
|
|
@@ -149,6 +151,20 @@ function mediaType(path) {
|
|
|
149
151
|
if (path.endsWith(".wasm")) return "application/wasm";
|
|
150
152
|
if (path.endsWith(".png")) return "image/png";
|
|
151
153
|
if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
|
|
154
|
+
if (path.endsWith(".css")) return "text/css";
|
|
155
|
+
if (path.endsWith(".svg")) return "image/svg+xml";
|
|
156
|
+
if (path.endsWith(".webp")) return "image/webp";
|
|
157
|
+
if (path.endsWith(".gif")) return "image/gif";
|
|
158
|
+
if (path.endsWith(".mp3")) return "audio/mpeg";
|
|
159
|
+
if (path.endsWith(".wav")) return "audio/wav";
|
|
160
|
+
if (path.endsWith(".ogg")) return "audio/ogg";
|
|
161
|
+
if (path.endsWith(".mp4")) return "video/mp4";
|
|
162
|
+
if (path.endsWith(".webm")) return "video/webm";
|
|
163
|
+
if (path.endsWith(".woff")) return "font/woff";
|
|
164
|
+
if (path.endsWith(".woff2")) return "font/woff2";
|
|
165
|
+
if (path.endsWith(".ttf")) return "font/ttf";
|
|
166
|
+
if (path.endsWith(".otf")) return "font/otf";
|
|
167
|
+
if (path.endsWith(".wgsl") || path.endsWith(".glsl")) return "text/plain";
|
|
152
168
|
return "application/octet-stream";
|
|
153
169
|
}
|
|
154
170
|
async function filesUnder(root, directory = root) {
|
|
@@ -840,6 +856,7 @@ var init_engine_binding = __esm({
|
|
|
840
856
|
// src/host.ts
|
|
841
857
|
var host_exports = {};
|
|
842
858
|
__export(host_exports, {
|
|
859
|
+
createEngineWorkspaceResolverForProject: () => createEngineWorkspaceResolverForProject,
|
|
843
860
|
createViteConfig: () => createViteConfig,
|
|
844
861
|
devKitDdcRoots: () => devKitDdcRoots,
|
|
845
862
|
ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
|
|
@@ -1010,6 +1027,9 @@ async function createEngineWorkspaceResolver(projectRoot) {
|
|
|
1010
1027
|
}
|
|
1011
1028
|
};
|
|
1012
1029
|
}
|
|
1030
|
+
async function createEngineWorkspaceResolverForProject(projectRoot) {
|
|
1031
|
+
return createEngineWorkspaceResolver(projectRoot);
|
|
1032
|
+
}
|
|
1013
1033
|
async function consumerEngineAliases(projectRoot) {
|
|
1014
1034
|
const root = resolve(projectRoot, "node_modules", ".pnpm", "node_modules", "@forgeax");
|
|
1015
1035
|
try {
|
|
@@ -1326,6 +1346,44 @@ async function previewOwnerFacts(assets, resource, payload) {
|
|
|
1326
1346
|
return undefined;
|
|
1327
1347
|
}
|
|
1328
1348
|
|
|
1349
|
+
const gameProjectionDefinitions = new Map();
|
|
1350
|
+
function registerGameProjection(kind, definition) {
|
|
1351
|
+
if (gameProjectionDefinitions.has(definition.id)) {
|
|
1352
|
+
throw new Error('forgeax: duplicate game projection id ' + definition.id);
|
|
1353
|
+
}
|
|
1354
|
+
const entry = { kind, definition };
|
|
1355
|
+
gameProjectionDefinitions.set(definition.id, entry);
|
|
1356
|
+
return () => {
|
|
1357
|
+
if (gameProjectionDefinitions.get(definition.id) === entry) {
|
|
1358
|
+
gameProjectionDefinitions.delete(definition.id);
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
const gameProjection = {
|
|
1363
|
+
registerAction: (definition) => registerGameProjection('action', definition),
|
|
1364
|
+
registerRead: (definition) => registerGameProjection('read', definition),
|
|
1365
|
+
};
|
|
1366
|
+
function exposeGameInspection(app) {
|
|
1367
|
+
globalThis.__forgeaxGameInspection = {
|
|
1368
|
+
list() {
|
|
1369
|
+
return {
|
|
1370
|
+
reads: Array.from(gameProjectionDefinitions.entries())
|
|
1371
|
+
.filter(([, entry]) => entry.kind === 'read')
|
|
1372
|
+
.map(([id]) => id),
|
|
1373
|
+
};
|
|
1374
|
+
},
|
|
1375
|
+
async read(id) {
|
|
1376
|
+
const entry = gameProjectionDefinitions.get(id);
|
|
1377
|
+
if (entry?.kind !== 'read') throw new Error('forgeax: game read projection not found ' + id);
|
|
1378
|
+
return entry.definition.read();
|
|
1379
|
+
},
|
|
1380
|
+
renderer() {
|
|
1381
|
+
const inspection = app.renderer.inspect();
|
|
1382
|
+
return { state: inspection.state, frameId: inspection.frame.frameId };
|
|
1383
|
+
},
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1329
1387
|
async function prepareProject(app) {
|
|
1330
1388
|
await prepareAssetRegistry(app.assets);
|
|
1331
1389
|
previewWorld = app.world;
|
|
@@ -1510,6 +1568,7 @@ await app.pluginContext.plugin(gameHostPlugin({
|
|
|
1510
1568
|
...(defaultSceneRoot === undefined ? {} : { defaultSceneRoot }),
|
|
1511
1569
|
uiRoot: uiRoot instanceof HTMLElement ? uiRoot : document.body,
|
|
1512
1570
|
setPointerLockAllowed: (allowed) => app.input?.setPointerLockAllowed?.(allowed),
|
|
1571
|
+
...(import.meta.env.DEV ? { gameProjection } : {}),
|
|
1513
1572
|
}));
|
|
1514
1573
|
const { loader: pluginLoader } = await installCatalogLoader(app.pluginContext, pluginCatalog, 'engine');
|
|
1515
1574
|
await pluginLoader.root.update(projectPluginEntries(pluginEntries, 'engine'));
|
|
@@ -1546,7 +1605,10 @@ if (query.has('forgeax-tool-replay')) {
|
|
|
1546
1605
|
if (current === undefined) return current;
|
|
1547
1606
|
if (resource?.kind !== 'vfx') {
|
|
1548
1607
|
if (resource?.kind === 'material' || resource?.kind === 'mesh' || resource?.kind === 'texture') {
|
|
1549
|
-
|
|
1608
|
+
const subjectDrawn = resource.kind === 'texture'
|
|
1609
|
+
? app.renderer.drawCalls > 0
|
|
1610
|
+
: app.renderer.drawCalls > 2;
|
|
1611
|
+
if (!subjectDrawn) return current;
|
|
1550
1612
|
if (resource.kind === 'mesh') {
|
|
1551
1613
|
const asset = current.asset;
|
|
1552
1614
|
const aabb = asset?.aabb;
|
|
@@ -1656,6 +1718,7 @@ if (query.has('forgeax-tool-replay')) {
|
|
|
1656
1718
|
);
|
|
1657
1719
|
if (!result.ok) throw result.error;
|
|
1658
1720
|
const app = result.value;
|
|
1721
|
+
if (import.meta.env.DEV) exposeGameInspection(app);
|
|
1659
1722
|
await prepareProject(app);
|
|
1660
1723
|
${bootstrapPlugin}
|
|
1661
1724
|
app.start().unwrap();
|
|
@@ -1800,6 +1863,7 @@ async function createViteConfig(facts, command, base = "/", options = {}) {
|
|
|
1800
1863
|
const consumerAliases = await consumerEngineAliases(facts.root);
|
|
1801
1864
|
const plugins = [
|
|
1802
1865
|
...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
|
|
1866
|
+
...command === "serve" && process.env.FORGEAX_ENGINE_RHI_DEBUG === "1" ? [vitePluginRhiDebug({ rootDir: facts.root })] : [],
|
|
1803
1867
|
forgeaxShader(),
|
|
1804
1868
|
pluginPack({
|
|
1805
1869
|
roots,
|
|
@@ -1844,6 +1908,737 @@ var init_host = __esm({
|
|
|
1844
1908
|
hostRequire = createRequire(import.meta.url);
|
|
1845
1909
|
}
|
|
1846
1910
|
});
|
|
1911
|
+
function errorResult(code, expected, hint, detail = {}) {
|
|
1912
|
+
return { ok: false, error: new SingleHtmlError(code, expected, hint, detail) };
|
|
1913
|
+
}
|
|
1914
|
+
function toErrorResult(cause, code, expected, hint) {
|
|
1915
|
+
if (cause instanceof SingleHtmlError) return { ok: false, error: cause };
|
|
1916
|
+
return errorResult(code, expected, hint, {
|
|
1917
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
function normalizePath(value) {
|
|
1921
|
+
let decoded;
|
|
1922
|
+
try {
|
|
1923
|
+
decoded = decodeURIComponent(value.split(/[?#]/, 1)[0] ?? value);
|
|
1924
|
+
} catch {
|
|
1925
|
+
return void 0;
|
|
1926
|
+
}
|
|
1927
|
+
const normalized = decoded.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
1928
|
+
const segments = normalized.split("/");
|
|
1929
|
+
if (normalized.length === 0 || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
1930
|
+
return void 0;
|
|
1931
|
+
}
|
|
1932
|
+
return normalized;
|
|
1933
|
+
}
|
|
1934
|
+
function safeScriptText(value) {
|
|
1935
|
+
return value.replace(/<\/script/gi, "<\\/script");
|
|
1936
|
+
}
|
|
1937
|
+
function hashBytes(bytes) {
|
|
1938
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
1939
|
+
}
|
|
1940
|
+
function contentTypeForDataUri(value) {
|
|
1941
|
+
const semicolon = value.indexOf(";");
|
|
1942
|
+
return semicolon < 0 ? value : value.slice(0, semicolon);
|
|
1943
|
+
}
|
|
1944
|
+
function dataUri(bytes, type) {
|
|
1945
|
+
return `data:${contentTypeForDataUri(type)};base64,${Buffer.from(bytes).toString("base64")}`;
|
|
1946
|
+
}
|
|
1947
|
+
function attributeValueRange(source, location) {
|
|
1948
|
+
const raw = source.slice(location.startOffset, location.endOffset);
|
|
1949
|
+
const equals = raw.indexOf("=");
|
|
1950
|
+
if (equals < 0) return void 0;
|
|
1951
|
+
let cursor = equals + 1;
|
|
1952
|
+
while (/\s/.test(raw[cursor] ?? "")) cursor += 1;
|
|
1953
|
+
const quote = raw[cursor] === '"' || raw[cursor] === "'" ? raw[cursor] : void 0;
|
|
1954
|
+
if (quote !== void 0) {
|
|
1955
|
+
const start = cursor + 1;
|
|
1956
|
+
const end = raw.lastIndexOf(quote);
|
|
1957
|
+
return end <= start ? void 0 : { startOffset: location.startOffset + start, endOffset: location.startOffset + end };
|
|
1958
|
+
}
|
|
1959
|
+
return {
|
|
1960
|
+
startOffset: location.startOffset + cursor,
|
|
1961
|
+
endOffset: location.endOffset
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
function scanHtmlElements(source) {
|
|
1965
|
+
const document2 = parse(source, { sourceCodeLocationInfo: true });
|
|
1966
|
+
const elements = [];
|
|
1967
|
+
const visit = (node) => {
|
|
1968
|
+
if (node.nodeName === "#document" || node.nodeName === "#document-fragment") {
|
|
1969
|
+
for (const child of node.childNodes) visit(child);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
if (node.nodeName === "#text" || node.nodeName === "#comment" || node.nodeName === "#documentType") {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const element = node;
|
|
1976
|
+
const location = element.sourceCodeLocation;
|
|
1977
|
+
if (location?.startTag === void 0) {
|
|
1978
|
+
for (const child of element.childNodes) visit(child);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const name = element.tagName.toLowerCase();
|
|
1982
|
+
const attributes = element.attrs.map(({ name: attributeName, value }) => ({
|
|
1983
|
+
name: attributeName.toLowerCase(),
|
|
1984
|
+
value
|
|
1985
|
+
}));
|
|
1986
|
+
const attributeLocations = {};
|
|
1987
|
+
for (const attribute2 of attributes) {
|
|
1988
|
+
const attributeLocation = location.attrs?.[attribute2.name];
|
|
1989
|
+
if (attributeLocation !== void 0) attributeLocations[attribute2.name] = attributeLocation;
|
|
1990
|
+
}
|
|
1991
|
+
elements.push({
|
|
1992
|
+
name,
|
|
1993
|
+
start: location.startTag.startOffset,
|
|
1994
|
+
openEnd: location.startTag.endOffset,
|
|
1995
|
+
end: location.endTag?.endOffset ?? location.startTag.endOffset,
|
|
1996
|
+
...name === "script" || name === "style" ? {
|
|
1997
|
+
contentStart: location.startTag.endOffset,
|
|
1998
|
+
contentEnd: location.endTag?.startOffset ?? location.startTag.endOffset
|
|
1999
|
+
} : {},
|
|
2000
|
+
attributes,
|
|
2001
|
+
attributeLocations
|
|
2002
|
+
});
|
|
2003
|
+
for (const child of element.childNodes) visit(child);
|
|
2004
|
+
};
|
|
2005
|
+
visit(document2);
|
|
2006
|
+
return elements;
|
|
2007
|
+
}
|
|
2008
|
+
function attribute(element, name) {
|
|
2009
|
+
return element.attributes.find((candidate) => candidate.name === name);
|
|
2010
|
+
}
|
|
2011
|
+
function attributeValue(element, name) {
|
|
2012
|
+
return attribute(element, name)?.value;
|
|
2013
|
+
}
|
|
2014
|
+
function isModuleScript(element) {
|
|
2015
|
+
return element.name === "script" && attributeValue(element, "type")?.toLowerCase() === "module";
|
|
2016
|
+
}
|
|
2017
|
+
function isStylesheet(element) {
|
|
2018
|
+
return element.name === "link" && attributeValue(element, "rel")?.toLowerCase().split(/\s+/).includes("stylesheet") === true;
|
|
2019
|
+
}
|
|
2020
|
+
function isModulePreload(element) {
|
|
2021
|
+
return element.name === "link" && attributeValue(element, "rel")?.toLowerCase().split(/\s+/).includes("modulepreload") === true;
|
|
2022
|
+
}
|
|
2023
|
+
function resourceForPath(path, resources) {
|
|
2024
|
+
const normalized = normalizePath(path);
|
|
2025
|
+
if (normalized === void 0) return void 0;
|
|
2026
|
+
const direct = resources.get(normalized);
|
|
2027
|
+
if (direct !== void 0) return direct;
|
|
2028
|
+
const suffix = [...resources.entries()].filter(
|
|
2029
|
+
([candidate]) => candidate.endsWith(`/${normalized}`)
|
|
2030
|
+
);
|
|
2031
|
+
return suffix.length === 1 ? suffix[0]?.[1] : void 0;
|
|
2032
|
+
}
|
|
2033
|
+
function resourcePath(reference, basePath) {
|
|
2034
|
+
try {
|
|
2035
|
+
return normalizePath(new URL(reference, `https://forgeax.invalid/${basePath}`).pathname);
|
|
2036
|
+
} catch {
|
|
2037
|
+
return void 0;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
function applyReplacements(source, replacements) {
|
|
2041
|
+
const unique = /* @__PURE__ */ new Map();
|
|
2042
|
+
for (const replacement of replacements) {
|
|
2043
|
+
unique.set(`${replacement.start}:${replacement.end}`, replacement);
|
|
2044
|
+
}
|
|
2045
|
+
const ordered = [...unique.values()].sort((left, right) => left.start - right.start);
|
|
2046
|
+
for (let index = 1; index < ordered.length; index += 1) {
|
|
2047
|
+
const previous = ordered[index - 1];
|
|
2048
|
+
const current = ordered[index];
|
|
2049
|
+
if (previous !== void 0 && current !== void 0 && previous.end > current.start) {
|
|
2050
|
+
throw new Error(
|
|
2051
|
+
`single-html AST replacements overlap (${previous.start}:${previous.end} and ${current.start}:${current.end})`
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return ordered.reverse().reduce(
|
|
2056
|
+
(value, replacement) => value.slice(0, replacement.start) + replacement.value + value.slice(replacement.end),
|
|
2057
|
+
source
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
function inlineCss(css, cssPath, resources) {
|
|
2061
|
+
const replacements = [];
|
|
2062
|
+
const pattern = /url\(\s*(["']?)(.*?)\1\s*\)/gi;
|
|
2063
|
+
for (; ; ) {
|
|
2064
|
+
const match = pattern.exec(css);
|
|
2065
|
+
if (match === null) break;
|
|
2066
|
+
const reference = match[2]?.trim();
|
|
2067
|
+
if (reference === void 0 || reference.length === 0 || reference.startsWith("data:") || reference.startsWith("#")) {
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
if (/^(?:https?:|blob:)/i.test(reference)) {
|
|
2071
|
+
return errorResult(
|
|
2072
|
+
"single-html-css-external-resource",
|
|
2073
|
+
"CSS URL references to be embedded or data/blob URLs",
|
|
2074
|
+
"Move the CSS resource into the verified dist closure before packaging.",
|
|
2075
|
+
{ cssPath, reference }
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
const path = resourcePath(reference, cssPath);
|
|
2079
|
+
const resource = path === void 0 ? void 0 : resourceForPath(path, resources);
|
|
2080
|
+
if (resource === void 0) {
|
|
2081
|
+
return errorResult(
|
|
2082
|
+
"single-html-css-asset-missing",
|
|
2083
|
+
"every local CSS URL to resolve to an embedded dist artifact",
|
|
2084
|
+
"Add the referenced asset to the project asset closure and rebuild.",
|
|
2085
|
+
{ cssPath, reference, path: path ?? null }
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
replacements.push({
|
|
2089
|
+
start: match.index,
|
|
2090
|
+
end: match.index + match[0].length,
|
|
2091
|
+
value: `url(${dataUri(resource.bytes, resource.mediaType)})`
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
return { ok: true, value: applyReplacements(css, replacements) };
|
|
2095
|
+
}
|
|
2096
|
+
function collectEntrySource(html) {
|
|
2097
|
+
const entries2 = scanHtmlElements(html).filter((element2) => isModuleScript(element2));
|
|
2098
|
+
const external = entries2.filter((element2) => attributeValue(element2, "src") !== void 0);
|
|
2099
|
+
const inline = entries2.filter((element2) => attributeValue(element2, "src") === void 0);
|
|
2100
|
+
if (external.length === 0 && inline.length === 0) {
|
|
2101
|
+
return errorResult(
|
|
2102
|
+
"single-html-entry-missing",
|
|
2103
|
+
"production index.html to contain one module entry",
|
|
2104
|
+
"Rebuild the game with a module entry in its generated host."
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
if (external.length > 1 || inline.length > 1) {
|
|
2108
|
+
return errorResult(
|
|
2109
|
+
"single-html-entry-ambiguous",
|
|
2110
|
+
"production index.html to contain exactly one module entry",
|
|
2111
|
+
"Converge the generated host to one ForgeaX module entry before packaging.",
|
|
2112
|
+
{ external: external.length, inline: inline.length }
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
const element = external[0] ?? inline[0];
|
|
2116
|
+
if (element === void 0) throw new Error("single-html entry selection was unexpectedly empty");
|
|
2117
|
+
const src = attributeValue(element, "src");
|
|
2118
|
+
if (src === void 0 && element.contentStart !== void 0 && element.contentEnd !== void 0) {
|
|
2119
|
+
return { ok: true, value: html.slice(element.contentStart, element.contentEnd) };
|
|
2120
|
+
}
|
|
2121
|
+
if (src === void 0) {
|
|
2122
|
+
return errorResult(
|
|
2123
|
+
"single-html-entry-missing",
|
|
2124
|
+
"a module entry source",
|
|
2125
|
+
"Add a module entry to index.html."
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
if (/^(?:https?:|file:|data:|blob:)/i.test(src)) {
|
|
2129
|
+
return errorResult(
|
|
2130
|
+
"single-html-entry-external",
|
|
2131
|
+
"the module entry to be a project-local dist artifact",
|
|
2132
|
+
"Rebuild the game so the generated host points at a local production module.",
|
|
2133
|
+
{ src }
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
return { ok: true, value: src };
|
|
2137
|
+
}
|
|
2138
|
+
async function filesUnder2(root, directory = root) {
|
|
2139
|
+
const result = [];
|
|
2140
|
+
for (const name of (await readdir(directory)).sort()) {
|
|
2141
|
+
const path = resolve(directory, name);
|
|
2142
|
+
const info = await stat(path);
|
|
2143
|
+
if (info.isDirectory()) result.push(...await filesUnder2(root, path));
|
|
2144
|
+
else if (info.isFile()) result.push(path);
|
|
2145
|
+
}
|
|
2146
|
+
return result;
|
|
2147
|
+
}
|
|
2148
|
+
function isPreloadHelperSource(value) {
|
|
2149
|
+
if (typeof value !== "string") return false;
|
|
2150
|
+
const path = value.split(/[?#]/, 1)[0] ?? value;
|
|
2151
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
2152
|
+
return name.startsWith("preload-helper-") && name.endsWith(".js");
|
|
2153
|
+
}
|
|
2154
|
+
function isStaticImportSource(source) {
|
|
2155
|
+
return source.type === "Literal" || source.type === "StringLiteral" || source.type === "TemplateLiteral" && source.expressions?.length === 0;
|
|
2156
|
+
}
|
|
2157
|
+
function programReplacements(program) {
|
|
2158
|
+
const replacements = [];
|
|
2159
|
+
const preloadBindings = /* @__PURE__ */ new Set();
|
|
2160
|
+
new Visitor({
|
|
2161
|
+
ImportDeclaration(node) {
|
|
2162
|
+
if (!isPreloadHelperSource(node.source.value)) return;
|
|
2163
|
+
for (const specifier of node.specifiers) {
|
|
2164
|
+
if (specifier.type === "ImportSpecifier") preloadBindings.add(specifier.local.name);
|
|
2165
|
+
}
|
|
2166
|
+
},
|
|
2167
|
+
CallExpression(node) {
|
|
2168
|
+
if (node.callee.type !== "Identifier" || !preloadBindings.has(node.callee.name) || node.arguments.length < 2) {
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
const dependencies = node.arguments[1];
|
|
2172
|
+
if (dependencies !== void 0) {
|
|
2173
|
+
replacements.push({ start: dependencies.start, end: dependencies.end, value: "void 0" });
|
|
2174
|
+
}
|
|
2175
|
+
},
|
|
2176
|
+
ImportExpression(node) {
|
|
2177
|
+
if (isStaticImportSource(node.source)) return;
|
|
2178
|
+
replacements.push({
|
|
2179
|
+
start: node.start,
|
|
2180
|
+
end: node.start + "import".length,
|
|
2181
|
+
value: "globalThis.__forgeaxImport"
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
}).visit(program);
|
|
2185
|
+
return replacements;
|
|
2186
|
+
}
|
|
2187
|
+
function outputReplacements(program) {
|
|
2188
|
+
const replacements = [];
|
|
2189
|
+
new Visitor({
|
|
2190
|
+
ImportExpression(node) {
|
|
2191
|
+
replacements.push({
|
|
2192
|
+
start: node.start,
|
|
2193
|
+
end: node.start + "import".length,
|
|
2194
|
+
value: "globalThis.__forgeaxImport"
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
}).visit(program);
|
|
2198
|
+
return replacements;
|
|
2199
|
+
}
|
|
2200
|
+
function rewriteOutputJavaScript(code) {
|
|
2201
|
+
const program = parseAst(code);
|
|
2202
|
+
return applyReplacements(code, outputReplacements(program));
|
|
2203
|
+
}
|
|
2204
|
+
function residualImportStart(code) {
|
|
2205
|
+
let residualStart;
|
|
2206
|
+
new Visitor({
|
|
2207
|
+
ImportExpression(node) {
|
|
2208
|
+
residualStart ??= node.start;
|
|
2209
|
+
}
|
|
2210
|
+
}).visit(parseAst(code));
|
|
2211
|
+
return residualStart;
|
|
2212
|
+
}
|
|
2213
|
+
function isJavaScriptOutputAsset(fileName) {
|
|
2214
|
+
return /\.(?:c?js|mjs)$/i.test(fileName);
|
|
2215
|
+
}
|
|
2216
|
+
function singleHtmlBundlePlugin() {
|
|
2217
|
+
const plugin = {
|
|
2218
|
+
name: "forgeax-single-html-bundle-runtime",
|
|
2219
|
+
enforce: "post",
|
|
2220
|
+
transform: {
|
|
2221
|
+
order: "post",
|
|
2222
|
+
handler(code, _id, meta) {
|
|
2223
|
+
let program = meta?.ast;
|
|
2224
|
+
if (program === void 0) {
|
|
2225
|
+
try {
|
|
2226
|
+
program = parseAst(code);
|
|
2227
|
+
} catch {
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
const replacements = programReplacements(program);
|
|
2232
|
+
if (replacements.length === 0) return null;
|
|
2233
|
+
if (meta?.magicString !== void 0) {
|
|
2234
|
+
for (const replacement of replacements) {
|
|
2235
|
+
meta.magicString.overwrite(replacement.start, replacement.end, replacement.value);
|
|
2236
|
+
}
|
|
2237
|
+
return meta.magicString.hasChanged() ? meta.magicString : null;
|
|
2238
|
+
}
|
|
2239
|
+
return applyReplacements(code, replacements);
|
|
2240
|
+
}
|
|
2241
|
+
},
|
|
2242
|
+
generateBundle(_outputOptions, bundle) {
|
|
2243
|
+
for (const output of Object.values(bundle)) {
|
|
2244
|
+
if (output.type === "chunk") {
|
|
2245
|
+
output.code = rewriteOutputJavaScript(output.code);
|
|
2246
|
+
} else if (isJavaScriptOutputAsset(output.fileName) && output.source !== void 0) {
|
|
2247
|
+
const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString("utf8");
|
|
2248
|
+
output.source = rewriteOutputJavaScript(source);
|
|
2249
|
+
} else {
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
2252
|
+
const code = output.type === "chunk" ? output.code : String(output.source);
|
|
2253
|
+
const residualStart = residualImportStart(code);
|
|
2254
|
+
if (residualStart !== void 0) {
|
|
2255
|
+
this.error(
|
|
2256
|
+
`single-html bundle left a native dynamic import in ${output.fileName} at ${residualStart}`
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
},
|
|
2261
|
+
resolveFileUrl: ({ fileName }) => JSON.stringify(`${GENERATED_PREFIX}${fileName}`)
|
|
2262
|
+
};
|
|
2263
|
+
return plugin;
|
|
2264
|
+
}
|
|
2265
|
+
async function bundleSingleHtmlEntry(distRootInput, indexHtml, projectRoot) {
|
|
2266
|
+
const distRoot = resolve(distRootInput);
|
|
2267
|
+
const selected = collectEntrySource(indexHtml);
|
|
2268
|
+
if (!selected.ok) return selected;
|
|
2269
|
+
const source = selected.value;
|
|
2270
|
+
const entryPath = normalizePath(source);
|
|
2271
|
+
const temporaryRoot = await mkdtemp(resolve(tmpdir(), "forgeax-single-html-bundle-"));
|
|
2272
|
+
try {
|
|
2273
|
+
let inputPath;
|
|
2274
|
+
if (entryPath !== void 0) {
|
|
2275
|
+
const candidate = resolve(distRoot, entryPath);
|
|
2276
|
+
try {
|
|
2277
|
+
const candidateInfo = await stat(candidate);
|
|
2278
|
+
if (candidateInfo.isFile()) inputPath = candidate;
|
|
2279
|
+
} catch {
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
if (inputPath === void 0) {
|
|
2283
|
+
inputPath = resolve(temporaryRoot, "inline-entry.mjs");
|
|
2284
|
+
await writeFile(inputPath, source, "utf8");
|
|
2285
|
+
}
|
|
2286
|
+
const outputRoot = resolve(temporaryRoot, "dist");
|
|
2287
|
+
const resolver = projectRoot === void 0 ? void 0 : await createEngineWorkspaceResolverForProject(projectRoot);
|
|
2288
|
+
await build({
|
|
2289
|
+
configFile: false,
|
|
2290
|
+
root: distRoot,
|
|
2291
|
+
base: "./",
|
|
2292
|
+
logLevel: "error",
|
|
2293
|
+
plugins: [singleHtmlBundlePlugin(), ...resolver === void 0 ? [] : [resolver]],
|
|
2294
|
+
experimental: {
|
|
2295
|
+
renderBuiltUrl: (filename) => `forgeax-resource:///${GENERATED_PREFIX}${filename}`
|
|
2296
|
+
},
|
|
2297
|
+
build: {
|
|
2298
|
+
outDir: outputRoot,
|
|
2299
|
+
emptyOutDir: true,
|
|
2300
|
+
target: "esnext",
|
|
2301
|
+
minify: false,
|
|
2302
|
+
sourcemap: false,
|
|
2303
|
+
assetsInlineLimit: 0,
|
|
2304
|
+
modulePreload: false,
|
|
2305
|
+
rolldownOptions: {
|
|
2306
|
+
input: inputPath,
|
|
2307
|
+
output: {
|
|
2308
|
+
codeSplitting: false,
|
|
2309
|
+
entryFileNames: "entry.mjs",
|
|
2310
|
+
chunkFileNames: "assets/[name]-[hash].mjs",
|
|
2311
|
+
assetFileNames: "assets/[name]-[hash][extname]"
|
|
2312
|
+
},
|
|
2313
|
+
experimental: { nativeMagicString: true }
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
});
|
|
2317
|
+
const paths = (await filesUnder2(outputRoot)).map(
|
|
2318
|
+
(path) => relative(outputRoot, path).split(sep).join("/")
|
|
2319
|
+
);
|
|
2320
|
+
const entry = paths.find((path) => path === "entry.mjs");
|
|
2321
|
+
if (entry === void 0) {
|
|
2322
|
+
return errorResult(
|
|
2323
|
+
"single-html-bundle-entry-missing",
|
|
2324
|
+
"Vite to emit exactly one entry.mjs",
|
|
2325
|
+
"Inspect the production entry and the single-html bundler output.",
|
|
2326
|
+
{ outputRoot, paths }
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
const generatedPaths = paths.filter((path) => path !== entry);
|
|
2330
|
+
const artifacts = [];
|
|
2331
|
+
for (const path of generatedPaths) {
|
|
2332
|
+
const bytes = await readFile(resolve(outputRoot, path));
|
|
2333
|
+
artifacts.push({
|
|
2334
|
+
path: `${GENERATED_PREFIX}${path}`,
|
|
2335
|
+
bytes,
|
|
2336
|
+
mediaType: mediaType(path)
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
const entryBytes = await readFile(resolve(outputRoot, entry));
|
|
2340
|
+
return {
|
|
2341
|
+
ok: true,
|
|
2342
|
+
value: {
|
|
2343
|
+
entrySource: entryBytes.toString("utf8"),
|
|
2344
|
+
artifacts
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
} catch (cause) {
|
|
2348
|
+
return toErrorResult(
|
|
2349
|
+
cause,
|
|
2350
|
+
"single-html-bundle-failed",
|
|
2351
|
+
"the production module entry to converge into one executable bundle",
|
|
2352
|
+
"Repair the production entry or inspect the Vite/Rolldown diagnostic before packaging."
|
|
2353
|
+
);
|
|
2354
|
+
} finally {
|
|
2355
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
function workerPrelude(resources) {
|
|
2359
|
+
const payload = resources.map((resource) => ({
|
|
2360
|
+
path: resource.path,
|
|
2361
|
+
mime: resource.mediaType,
|
|
2362
|
+
data: Buffer.from(resource.bytes).toString("base64")
|
|
2363
|
+
}));
|
|
2364
|
+
const template = `globalThis.process??={env:{},versions:{node:'0.0.0'},platform:'browser',argv:[]};
|
|
2365
|
+
const __forgeaxWorkerPayload=${JSON.stringify(payload)};
|
|
2366
|
+
const __forgeaxSourcePath=__FORGEAX_SOURCE_PATH__;
|
|
2367
|
+
const __forgeaxNativeURL=globalThis.URL;
|
|
2368
|
+
const __forgeaxNativeFetch=globalThis.fetch?.bind(globalThis);
|
|
2369
|
+
const __forgeaxNativePostMessage=globalThis.postMessage?.bind(globalThis);
|
|
2370
|
+
const __forgeaxOwnedUrls=new Set();
|
|
2371
|
+
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;};
|
|
2372
|
+
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);};
|
|
2373
|
+
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;};
|
|
2374
|
+
const __forgeaxNormalize=(value)=>{try{return decodeURIComponent(String(value).split(/[?#]/,1)[0]).replaceAll('\\\\','/').replace(/^\\/+/, '');}catch{return undefined;}};
|
|
2375
|
+
const __forgeaxWithoutBundle=(value)=>String(value).replace(/^__forgeax-bundle\\//,'');
|
|
2376
|
+
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;};
|
|
2377
|
+
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);}};
|
|
2378
|
+
globalThis.__forgeaxURL=__forgeaxResourceURL;
|
|
2379
|
+
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);}}
|
|
2380
|
+
globalThis.URL=__ForgeaxURL;
|
|
2381
|
+
const __forgeaxNote=(kind,specifier)=>{const value={kind,specifier:String(specifier)};try{__forgeaxNativePostMessage?.({__forgeaxResourceMiss:value});}catch{}};
|
|
2382
|
+
const __forgeaxNoteHit=()=>{try{__forgeaxNativePostMessage?.({__forgeaxResourceHit:1});}catch{}};
|
|
2383
|
+
const __forgeaxNoteExternal=(kind,specifier)=>{const value={kind,specifier:String(specifier)};try{__forgeaxNativePostMessage?.({__forgeaxExternalRequest:value});}catch{}};
|
|
2384
|
+
const __forgeaxResponse=(entry)=>{__forgeaxNoteHit();return new Response(__forgeaxBytes(entry.data),{status:200,headers:{'Content-Type':entry.mime}});};
|
|
2385
|
+
const __forgeaxImportUrls=new Map();
|
|
2386
|
+
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);};
|
|
2387
|
+
globalThis.__forgeaxImport=__forgeaxImport;
|
|
2388
|
+
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'));};
|
|
2389
|
+
const __forgeaxWorkerPrelude=__FORGEAX_WORKER_FACTORY__;
|
|
2390
|
+
const __forgeaxNativeWorker=globalThis.Worker;
|
|
2391
|
+
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);}};
|
|
2392
|
+
globalThis.addEventListener?.('message',(event)=>{if(event.data?.__forgeaxResourceMiss||event.data?.__forgeaxResourceHit||event.data?.__forgeaxExternalRequest){try{__forgeaxNativePostMessage?.(event.data);}catch{}event.stopImmediatePropagation?.();}});
|
|
2393
|
+
globalThis.addEventListener?.('unload',()=>{for(const url of __forgeaxOwnedUrls)__forgeaxNativeURL.revokeObjectURL(url);__forgeaxOwnedUrls.clear();});`;
|
|
2394
|
+
const factoryToken = "__FORGEAX_WORKER_FACTORY__";
|
|
2395
|
+
const recursiveTemplate = template;
|
|
2396
|
+
const factorySource = `(sourcePath)=>{const source=${JSON.stringify(recursiveTemplate)};return source.replace(${JSON.stringify(factoryToken)},()=> '('+__forgeaxWorkerPrelude.toString()+')').replace('__FORGEAX_SOURCE_PATH__',()=>JSON.stringify(sourcePath));}`;
|
|
2397
|
+
return `const __forgeaxWorkerPrelude=${factorySource};`;
|
|
2398
|
+
}
|
|
2399
|
+
function runtimeBootstrap(resources) {
|
|
2400
|
+
const workerFactory = workerPrelude(resources);
|
|
2401
|
+
return `(() => {
|
|
2402
|
+
globalThis.process ??= { env: {}, versions: { node: '0.0.0' }, platform: 'browser', argv: [] };
|
|
2403
|
+
const nativeURL = globalThis.URL;
|
|
2404
|
+
const nativeFetch = globalThis.fetch?.bind(globalThis);
|
|
2405
|
+
const nodes = [...document.querySelectorAll('script[data-forgeax-asset]')];
|
|
2406
|
+
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');
|
|
2407
|
+
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; };
|
|
2408
|
+
const normalize = (value) => { try { return decodeURIComponent(String(value).split(/[?#]/, 1)[0]).replaceAll('\\\\', '/').replace(/^\\/+/, ''); } catch { return undefined; } };
|
|
2409
|
+
const withoutBundle = (value) => String(value).replace(/^__forgeax-bundle\\//, '');
|
|
2410
|
+
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; };
|
|
2411
|
+
const resourceMisses = [];
|
|
2412
|
+
const externalRequests = [];
|
|
2413
|
+
let resourceHits = 0;
|
|
2414
|
+
const noteMiss = (kind, specifier) => { resourceMisses.push({ kind, specifier: String(specifier), realm: 'main' }); };
|
|
2415
|
+
const noteExternal = (kind, specifier) => { externalRequests.push({ kind, specifier: String(specifier), realm: 'main' }); };
|
|
2416
|
+
const response = (entry) => { resourceHits += 1; return new Response(decode(entry.data), { status: 200, headers: { 'Content-Type': entry.mime } }); };
|
|
2417
|
+
const ownedBlobUrls = new Set();
|
|
2418
|
+
const makeBlobUrl = (entry) => { const url = nativeURL.createObjectURL(new Blob([decode(entry.data)], { type: entry.mime })); ownedBlobUrls.add(url); return url; };
|
|
2419
|
+
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); };
|
|
2420
|
+
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; };
|
|
2421
|
+
const importUrls = new Map();
|
|
2422
|
+
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); };
|
|
2423
|
+
globalThis.__forgeaxImport = forgeaxImport;
|
|
2424
|
+
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; } };
|
|
2425
|
+
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')); };
|
|
2426
|
+
${workerFactory}
|
|
2427
|
+
const nativeWorker = globalThis.Worker;
|
|
2428
|
+
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; };
|
|
2429
|
+
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); } };
|
|
2430
|
+
const witness = () => ({ ready: document.documentElement.dataset.forgeaxSingleHtmlReady === 'true', resourceHits, resourceMisses: resourceMisses.slice(), externalRequests: externalRequests.slice() });
|
|
2431
|
+
globalThis.__forgeaxSingleHtml = { lookup, entries: entries.map(({ path, mime }) => ({ path, mime })), witness };
|
|
2432
|
+
globalThis.addEventListener('pagehide', () => { for (const url of ownedBlobUrls) nativeURL.revokeObjectURL(url); ownedBlobUrls.clear(); }, { once: true });
|
|
2433
|
+
document.documentElement.dataset.forgeaxSingleHtmlReady = 'true';
|
|
2434
|
+
})();`;
|
|
2435
|
+
}
|
|
2436
|
+
function escapeAttribute(value) {
|
|
2437
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
2438
|
+
}
|
|
2439
|
+
function distResources(distRoot, manifest) {
|
|
2440
|
+
const rows = [
|
|
2441
|
+
...manifest.artifacts,
|
|
2442
|
+
{ path: "forgeax-dist.json" }
|
|
2443
|
+
];
|
|
2444
|
+
return Promise.all(
|
|
2445
|
+
rows.filter((artifact) => artifact.path !== "index.html").map(async (artifact) => ({
|
|
2446
|
+
path: artifact.path,
|
|
2447
|
+
bytes: await readFile(resolve(distRoot, artifact.path)),
|
|
2448
|
+
mediaType: "mediaType" in artifact ? artifact.mediaType : mediaType(artifact.path)
|
|
2449
|
+
}))
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
function buildHtml(indexHtml, distArtifacts, bundle) {
|
|
2453
|
+
const resources = /* @__PURE__ */ new Map();
|
|
2454
|
+
for (const resource of [...distArtifacts, ...bundle.artifacts]) {
|
|
2455
|
+
if (resources.has(resource.path)) {
|
|
2456
|
+
return errorResult(
|
|
2457
|
+
"single-html-asset-duplicate",
|
|
2458
|
+
"embedded resource paths to be unique",
|
|
2459
|
+
"Repair the generated bundle path collision before packaging.",
|
|
2460
|
+
{ path: resource.path }
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
resources.set(resource.path, resource);
|
|
2464
|
+
}
|
|
2465
|
+
const replacements = [];
|
|
2466
|
+
for (const element of scanHtmlElements(indexHtml)) {
|
|
2467
|
+
if (isModuleScript(element) || isModulePreload(element)) {
|
|
2468
|
+
replacements.push({ start: element.start, end: element.end, value: "" });
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
if (isStylesheet(element)) {
|
|
2472
|
+
const href = attributeValue(element, "href");
|
|
2473
|
+
if (href === void 0) {
|
|
2474
|
+
return errorResult(
|
|
2475
|
+
"single-html-stylesheet-missing",
|
|
2476
|
+
"stylesheet link to contain href",
|
|
2477
|
+
"Repair the generated HTML stylesheet link."
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
const path = resourcePath(href, "index.html");
|
|
2481
|
+
const stylesheet = path === void 0 ? void 0 : resourceForPath(path, resources);
|
|
2482
|
+
if (stylesheet === void 0) {
|
|
2483
|
+
return errorResult(
|
|
2484
|
+
"single-html-css-asset-missing",
|
|
2485
|
+
"stylesheet link to resolve to an embedded artifact",
|
|
2486
|
+
"Add the stylesheet to the dist closure and rebuild.",
|
|
2487
|
+
{ href, path: path ?? null }
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
const css = inlineCss(
|
|
2491
|
+
Buffer.from(stylesheet.bytes).toString("utf8"),
|
|
2492
|
+
stylesheet.path,
|
|
2493
|
+
resources
|
|
2494
|
+
);
|
|
2495
|
+
if (!css.ok) return css;
|
|
2496
|
+
replacements.push({
|
|
2497
|
+
start: element.start,
|
|
2498
|
+
end: element.end,
|
|
2499
|
+
value: `<style data-forgeax-inline-css>${css.value}</style>`
|
|
2500
|
+
});
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2503
|
+
if (element.name === "style" && element.contentStart !== void 0 && element.contentEnd !== void 0) {
|
|
2504
|
+
const css = inlineCss(
|
|
2505
|
+
indexHtml.slice(element.contentStart, element.contentEnd),
|
|
2506
|
+
"index.html",
|
|
2507
|
+
resources
|
|
2508
|
+
);
|
|
2509
|
+
if (!css.ok) return css;
|
|
2510
|
+
replacements.push({ start: element.contentStart, end: element.contentEnd, value: css.value });
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
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;
|
|
2514
|
+
const resourceAttributeLocation = resourceAttribute === void 0 ? void 0 : element.attributeLocations[resourceAttribute.name];
|
|
2515
|
+
const resourceAttributeRange = resourceAttribute === void 0 || resourceAttributeLocation === void 0 ? void 0 : attributeValueRange(indexHtml, resourceAttributeLocation);
|
|
2516
|
+
if (resourceAttribute?.value !== void 0 && resourceAttributeRange !== void 0 && !/^(?:data:|blob:|#)/i.test(resourceAttribute.value)) {
|
|
2517
|
+
const path = resourcePath(resourceAttribute.value, "index.html");
|
|
2518
|
+
const embedded = path === void 0 ? void 0 : resourceForPath(path, resources);
|
|
2519
|
+
if (embedded === void 0) {
|
|
2520
|
+
return errorResult(
|
|
2521
|
+
"single-html-asset-missing",
|
|
2522
|
+
"every local HTML resource to resolve to an embedded dist artifact",
|
|
2523
|
+
"Add the referenced asset to the project closure and rebuild.",
|
|
2524
|
+
{ value: resourceAttribute.value, path: path ?? null }
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
replacements.push({
|
|
2528
|
+
start: resourceAttributeRange.startOffset,
|
|
2529
|
+
end: resourceAttributeRange.endOffset,
|
|
2530
|
+
value: dataUri(embedded.bytes, embedded.mediaType)
|
|
2531
|
+
});
|
|
2532
|
+
continue;
|
|
2533
|
+
}
|
|
2534
|
+
if (element.name === "script" && attributeValue(element, "src") !== void 0) {
|
|
2535
|
+
return errorResult(
|
|
2536
|
+
"single-html-script-external",
|
|
2537
|
+
"non-module script sources to be absent from the generated host",
|
|
2538
|
+
"Move the script into the production module entry before packaging.",
|
|
2539
|
+
{ src: attributeValue(element, "src") }
|
|
2540
|
+
);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
const assetNodes = [...resources.values()].sort((left, right) => left.path.localeCompare(right.path)).map(
|
|
2544
|
+
(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>`
|
|
2545
|
+
).join("");
|
|
2546
|
+
const bootstrap = `<script type="application/javascript">${safeScriptText(runtimeBootstrap([...resources.values()]))}</script>`;
|
|
2547
|
+
const entry = `<script type="module">${safeScriptText(bundle.entrySource)}</script>`;
|
|
2548
|
+
const withResources = applyReplacements(indexHtml, replacements);
|
|
2549
|
+
const body = `${assetNodes}${bootstrap}${entry}`;
|
|
2550
|
+
const html = withResources.includes("</body>") ? withResources.replace(/<\/body>/i, () => `${body}</body>`) : `${withResources}${body}`;
|
|
2551
|
+
return { ok: true, value: { html, embeddedAssets: resources.size } };
|
|
2552
|
+
}
|
|
2553
|
+
async function writeSingleHtml(options) {
|
|
2554
|
+
const distRoot = resolve(options.distRoot);
|
|
2555
|
+
const output = resolve(options.output);
|
|
2556
|
+
const checksumPath = `${output}.sha256`;
|
|
2557
|
+
const temporary = `${output}.partial-${process.pid}`;
|
|
2558
|
+
const temporaryChecksum = `${checksumPath}.partial-${process.pid}`;
|
|
2559
|
+
try {
|
|
2560
|
+
const indexHtml = await readFile(resolve(distRoot, "index.html"), "utf8");
|
|
2561
|
+
const resources = await distResources(distRoot, options.manifest);
|
|
2562
|
+
const built = buildHtml(indexHtml, resources, options.bundle);
|
|
2563
|
+
if (!built.ok) return built;
|
|
2564
|
+
const bytes = Buffer.from(built.value.html, "utf8");
|
|
2565
|
+
const sha2562 = hashBytes(bytes);
|
|
2566
|
+
const manifestBytes = await readFile(resolve(distRoot, "forgeax-dist.json"));
|
|
2567
|
+
await mkdir(dirname(output), { recursive: true });
|
|
2568
|
+
await writeFile(temporary, bytes);
|
|
2569
|
+
await writeFile(temporaryChecksum, `${sha2562} ${basename(output)}
|
|
2570
|
+
`, "utf8");
|
|
2571
|
+
await rename(temporaryChecksum, checksumPath);
|
|
2572
|
+
await rename(temporary, output);
|
|
2573
|
+
return {
|
|
2574
|
+
ok: true,
|
|
2575
|
+
value: {
|
|
2576
|
+
schemaVersion: "1.0.0",
|
|
2577
|
+
format: SINGLE_HTML_FORMAT,
|
|
2578
|
+
target: "file",
|
|
2579
|
+
project: options.manifest.project,
|
|
2580
|
+
base: options.manifest.base,
|
|
2581
|
+
html: { path: output, bytes: bytes.byteLength, sha256: sha2562 },
|
|
2582
|
+
checksumPath,
|
|
2583
|
+
distManifestSha256: hashBytes(manifestBytes),
|
|
2584
|
+
embeddedAssets: built.value.embeddedAssets,
|
|
2585
|
+
run: {
|
|
2586
|
+
local: pathToFileURL(output).href,
|
|
2587
|
+
shared: "send the HTML file and open it in a desktop Chrome with WebGPU support"
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
} catch (cause) {
|
|
2592
|
+
return toErrorResult(
|
|
2593
|
+
cause,
|
|
2594
|
+
"single-html-write-failed",
|
|
2595
|
+
"the single HTML candidate and adjacent SHA-256 to be written atomically",
|
|
2596
|
+
"Repair the dist closure or output directory, then retry packaging."
|
|
2597
|
+
);
|
|
2598
|
+
} finally {
|
|
2599
|
+
await Promise.all([rm(temporary, { force: true }), rm(temporaryChecksum, { force: true })]);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
function packageFormatError(format) {
|
|
2603
|
+
return errorResult(
|
|
2604
|
+
"package-format-unsupported",
|
|
2605
|
+
"package format to be web-zip or single-html",
|
|
2606
|
+
"Use web-zip for HTTPS hosting or single-html for a self-contained file:// delivery.",
|
|
2607
|
+
{ format }
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
function packageOutputError(output, format) {
|
|
2611
|
+
const expectedSuffix = format === "single-html" ? ".html" : ".zip";
|
|
2612
|
+
return errorResult(
|
|
2613
|
+
"package-output-suffix-mismatch",
|
|
2614
|
+
`package output to end with ${expectedSuffix}`,
|
|
2615
|
+
`Use a ${expectedSuffix} output path for ${format}.`,
|
|
2616
|
+
{ output, format }
|
|
2617
|
+
);
|
|
2618
|
+
}
|
|
2619
|
+
var SINGLE_HTML_FORMAT, GENERATED_PREFIX, SingleHtmlError;
|
|
2620
|
+
var init_single_html = __esm({
|
|
2621
|
+
"src/single-html.ts"() {
|
|
2622
|
+
init_dist();
|
|
2623
|
+
init_host();
|
|
2624
|
+
SINGLE_HTML_FORMAT = "forgeax-single-html-game";
|
|
2625
|
+
GENERATED_PREFIX = "__forgeax-bundle/";
|
|
2626
|
+
SingleHtmlError = class extends Error {
|
|
2627
|
+
constructor(code, expected, hint, detail) {
|
|
2628
|
+
super(`${code}: ${hint}`);
|
|
2629
|
+
this.code = code;
|
|
2630
|
+
this.expected = expected;
|
|
2631
|
+
this.hint = hint;
|
|
2632
|
+
this.detail = detail;
|
|
2633
|
+
this.name = "SingleHtmlError";
|
|
2634
|
+
}
|
|
2635
|
+
code;
|
|
2636
|
+
expected;
|
|
2637
|
+
hint;
|
|
2638
|
+
detail;
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
});
|
|
1847
2642
|
|
|
1848
2643
|
// src/types.ts
|
|
1849
2644
|
function resolveProjectPort(port) {
|
|
@@ -2106,11 +2901,13 @@ var init_package = __esm({
|
|
|
2106
2901
|
"@forgeax/engine-types": "workspace:*",
|
|
2107
2902
|
"@forgeax/engine-vfx-compiler": "workspace:*",
|
|
2108
2903
|
"@forgeax/engine-vite-plugin-pack": "workspace:*",
|
|
2904
|
+
"@forgeax/engine-vite-plugin-rhi-debug": "workspace:*",
|
|
2109
2905
|
"@forgeax/engine-vite-plugin-shader": "workspace:*",
|
|
2110
2906
|
jiti: "1.21.7",
|
|
2907
|
+
parse5: "7.3.0",
|
|
2111
2908
|
playwright: "1.60.0",
|
|
2112
2909
|
vite: "8.0.10",
|
|
2113
|
-
vitest: "4.
|
|
2910
|
+
vitest: "4.1.11",
|
|
2114
2911
|
webgpu: "^0.4.0"
|
|
2115
2912
|
},
|
|
2116
2913
|
devDependencies: {
|
|
@@ -2285,7 +3082,7 @@ var init_init = __esm({
|
|
|
2285
3082
|
"@webgpu/types": "0.1.71",
|
|
2286
3083
|
tsx: "4.23.1",
|
|
2287
3084
|
typescript: "6.0.3",
|
|
2288
|
-
vitest: "4.
|
|
3085
|
+
vitest: "4.1.11"
|
|
2289
3086
|
};
|
|
2290
3087
|
}
|
|
2291
3088
|
});
|
|
@@ -2307,15 +3104,12 @@ async function findSdkContext() {
|
|
|
2307
3104
|
const templates = new Map(
|
|
2308
3105
|
manifest.templates.map((template) => [template.id, resolve(cursor, template.root)])
|
|
2309
3106
|
);
|
|
2310
|
-
const defaultTemplate = manifest.templates.find((template) => template.default);
|
|
2311
|
-
if (defaultTemplate === void 0) throw new Error("sdk-default-template-missing");
|
|
2312
3107
|
const store = resolve(cursor, "store", "pnpm");
|
|
2313
3108
|
return {
|
|
2314
3109
|
root: cursor,
|
|
2315
3110
|
manifest,
|
|
2316
3111
|
...await readable(store) ? { store } : {},
|
|
2317
|
-
templates
|
|
2318
|
-
defaultTemplate: defaultTemplate.id
|
|
3112
|
+
templates
|
|
2319
3113
|
};
|
|
2320
3114
|
}
|
|
2321
3115
|
const parent = dirname(cursor);
|
|
@@ -2335,10 +3129,14 @@ function agentOnboarding(sdk, projectRoot) {
|
|
|
2335
3129
|
resolve(root, "skills", "forgeax-engine-sdk", "SKILL.md"),
|
|
2336
3130
|
resolve(root, "skills", "forgeax-engine-sdk", "references", "feature-catalog.md")
|
|
2337
3131
|
],
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
3132
|
+
...projectRoot === void 0 ? {
|
|
3133
|
+
templateSelection: {
|
|
3134
|
+
required: true,
|
|
3135
|
+
game3d: "game-3d",
|
|
3136
|
+
otherwise: "empty"
|
|
3137
|
+
}
|
|
3138
|
+
} : {},
|
|
3139
|
+
...projectRoot === void 0 ? {} : { next: { cwd: root, argv: ["pnpm", "exec", "forgeax", "list", "--json"] } }
|
|
2342
3140
|
};
|
|
2343
3141
|
}
|
|
2344
3142
|
function sdkInitPath(sdk) {
|
|
@@ -2420,8 +3218,8 @@ async function requireSdkInitialization(sdk) {
|
|
|
2420
3218
|
return { ok: true, value: state };
|
|
2421
3219
|
}
|
|
2422
3220
|
async function copyBootstrapInputs(sdk, root) {
|
|
2423
|
-
const template = sdk.templates.get(
|
|
2424
|
-
if (template === void 0) throw new Error("sdk-
|
|
3221
|
+
const template = sdk.templates.get("empty");
|
|
3222
|
+
if (template === void 0) throw new Error("sdk-bootstrap-template-missing");
|
|
2425
3223
|
for (const name of ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"]) {
|
|
2426
3224
|
await cp(resolve(template, name), resolve(root, name));
|
|
2427
3225
|
}
|
|
@@ -2613,13 +3411,13 @@ async function pathKind(path) {
|
|
|
2613
3411
|
throw cause;
|
|
2614
3412
|
}
|
|
2615
3413
|
}
|
|
2616
|
-
async function
|
|
3414
|
+
async function filesUnder3(root, directory = root) {
|
|
2617
3415
|
const files = [];
|
|
2618
3416
|
for (const name of (await readdir(directory)).sort()) {
|
|
2619
3417
|
const path = resolve(directory, name);
|
|
2620
3418
|
const info = await lstat(path);
|
|
2621
3419
|
if (info.isSymbolicLink()) throw new Error(`skill-source-symlink: ${path}`);
|
|
2622
|
-
if (info.isDirectory()) files.push(...await
|
|
3420
|
+
if (info.isDirectory()) files.push(...await filesUnder3(root, path));
|
|
2623
3421
|
else if (info.isFile()) files.push(path);
|
|
2624
3422
|
}
|
|
2625
3423
|
return files;
|
|
@@ -2631,7 +3429,7 @@ async function skillRow(sourceRoot, id) {
|
|
|
2631
3429
|
if (await pathKind(resolve(root, "SKILL.md")) !== "file") {
|
|
2632
3430
|
throw new Error(`skill-entry-missing: ${id}/SKILL.md`);
|
|
2633
3431
|
}
|
|
2634
|
-
const files = await
|
|
3432
|
+
const files = await filesUnder3(root);
|
|
2635
3433
|
return {
|
|
2636
3434
|
id,
|
|
2637
3435
|
root: `skills/${id}`,
|
|
@@ -2648,8 +3446,8 @@ async function discoverProjectSkills(root) {
|
|
|
2648
3446
|
return Promise.all(ids.map((id) => skillRow(sourceRoot, id)));
|
|
2649
3447
|
}
|
|
2650
3448
|
async function sameFiles(leftRoot, rightRoot) {
|
|
2651
|
-
const left = await
|
|
2652
|
-
const right = await
|
|
3449
|
+
const left = await filesUnder3(leftRoot);
|
|
3450
|
+
const right = await filesUnder3(rightRoot);
|
|
2653
3451
|
const leftNames = left.map((path) => slash(relative(leftRoot, path)));
|
|
2654
3452
|
const rightNames = right.map((path) => slash(relative(rightRoot, path)));
|
|
2655
3453
|
if (JSON.stringify(leftNames) !== JSON.stringify(rightNames)) return false;
|
|
@@ -3073,8 +3871,8 @@ async function initCommand(options = {}) {
|
|
|
3073
3871
|
const applied = await applyInitPlan(facts.value, plan.value, options);
|
|
3074
3872
|
if (!applied.ok || options.dryRun === true) return applied;
|
|
3075
3873
|
if (sdk !== void 0) {
|
|
3076
|
-
const template = sdk.templates.get(
|
|
3077
|
-
if (template === void 0) throw new Error("sdk-
|
|
3874
|
+
const template = sdk.templates.get("empty");
|
|
3875
|
+
if (template === void 0) throw new Error("sdk-bootstrap-template-missing");
|
|
3078
3876
|
await copyFile(
|
|
3079
3877
|
resolve(template, "pnpm-lock.yaml"),
|
|
3080
3878
|
resolve(facts.value.root, "pnpm-lock.yaml")
|
|
@@ -3142,7 +3940,18 @@ async function newCommand(options = {}) {
|
|
|
3142
3940
|
}
|
|
3143
3941
|
};
|
|
3144
3942
|
}
|
|
3145
|
-
|
|
3943
|
+
if (options.template === void 0) {
|
|
3944
|
+
return {
|
|
3945
|
+
ok: false,
|
|
3946
|
+
error: {
|
|
3947
|
+
code: "sdk-template-required",
|
|
3948
|
+
expected: "forgeax new to select exactly one template with --template",
|
|
3949
|
+
hint: "Use --template game-3d for a 3D game; otherwise use --template empty.",
|
|
3950
|
+
detail: { templates: [...sdk.templates.keys()].sort() }
|
|
3951
|
+
}
|
|
3952
|
+
};
|
|
3953
|
+
}
|
|
3954
|
+
const templateId = options.template;
|
|
3146
3955
|
const template = sdk.templates.get(templateId);
|
|
3147
3956
|
if (template === void 0) {
|
|
3148
3957
|
return {
|
|
@@ -4060,28 +4869,32 @@ function observedBackend(runtime) {
|
|
|
4060
4869
|
return isSoftwareAdapter(runtime) ? "software" : "hardware";
|
|
4061
4870
|
}
|
|
4062
4871
|
async function resolveBrowserExecutable(requested) {
|
|
4872
|
+
if (requested !== void 0 && requested.length > 0) {
|
|
4873
|
+
return await pathExists2(requested) ? requested : void 0;
|
|
4874
|
+
}
|
|
4063
4875
|
const candidates = [
|
|
4064
|
-
requested,
|
|
4065
4876
|
process.env.FORGEAX_BROWSER_EXECUTABLE,
|
|
4066
4877
|
"/opt/google/chrome-beta/chrome",
|
|
4067
4878
|
"/usr/bin/google-chrome",
|
|
4068
4879
|
"/usr/bin/google-chrome-stable",
|
|
4069
4880
|
"/usr/bin/chromium",
|
|
4070
|
-
"/usr/bin/chromium-browser"
|
|
4881
|
+
"/usr/bin/chromium-browser",
|
|
4882
|
+
typeof chromium.executablePath === "function" ? chromium.executablePath() : void 0
|
|
4071
4883
|
].filter((candidate) => candidate !== void 0 && candidate.length > 0);
|
|
4072
4884
|
for (const candidate of candidates) {
|
|
4073
4885
|
if (await pathExists2(candidate)) return candidate;
|
|
4074
4886
|
}
|
|
4075
4887
|
return void 0;
|
|
4076
4888
|
}
|
|
4077
|
-
function browserLaunchArgs(backend) {
|
|
4078
|
-
const common = [
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4889
|
+
function browserLaunchArgs(backend, profile) {
|
|
4890
|
+
const common = ["--force-color-profile=srgb", "--force-device-scale-factor=1"];
|
|
4891
|
+
if (profile === "development") {
|
|
4892
|
+
common.unshift(
|
|
4893
|
+
"--enable-unsafe-webgpu",
|
|
4894
|
+
"--ignore-gpu-blocklist",
|
|
4895
|
+
"--disable-gpu-driver-bug-workarounds"
|
|
4896
|
+
);
|
|
4897
|
+
}
|
|
4085
4898
|
if (backend !== "software") return common;
|
|
4086
4899
|
return [
|
|
4087
4900
|
...common,
|
|
@@ -4172,6 +4985,7 @@ async function runtimeWitness(page) {
|
|
|
4172
4985
|
adapterError = String(cause);
|
|
4173
4986
|
}
|
|
4174
4987
|
const frameId = Number(document.documentElement.dataset[datasetKey]);
|
|
4988
|
+
const singleHtmlState = globalThis.__forgeaxSingleHtml;
|
|
4175
4989
|
return {
|
|
4176
4990
|
title: document.title,
|
|
4177
4991
|
canvas: canvas instanceof HTMLCanvasElement ? { width: canvas.width, height: canvas.height } : null,
|
|
@@ -4184,6 +4998,7 @@ async function runtimeWitness(page) {
|
|
|
4184
4998
|
adapterError,
|
|
4185
4999
|
engineFrameId: Number.isSafeInteger(frameId) && frameId > 0 ? frameId : null,
|
|
4186
5000
|
captureReady: document.documentElement.dataset.forgeaxCaptureReady ?? null,
|
|
5001
|
+
singleHtml: singleHtmlState?.witness?.() ?? null,
|
|
4187
5002
|
userAgent: navigator.userAgent
|
|
4188
5003
|
};
|
|
4189
5004
|
}, FORGEAX_FRAME_SUBMITTED_DATASET);
|
|
@@ -4199,8 +5014,81 @@ async function writeRunReport(report) {
|
|
|
4199
5014
|
await writeFile(report.report, `${JSON.stringify(report, null, 2)}
|
|
4200
5015
|
`, "utf8");
|
|
4201
5016
|
}
|
|
5017
|
+
function requestProtocol(url) {
|
|
5018
|
+
try {
|
|
5019
|
+
return new URL(url).protocol;
|
|
5020
|
+
} catch {
|
|
5021
|
+
return void 0;
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
function requestRecord(request, target, candidateUrl) {
|
|
5025
|
+
const url = request.url();
|
|
5026
|
+
const protocol = requestProtocol(url);
|
|
5027
|
+
const requestDocument = url.split(/[?#]/, 1)[0];
|
|
5028
|
+
const candidateDocument = candidateUrl.split(/[?#]/, 1)[0];
|
|
5029
|
+
return {
|
|
5030
|
+
url,
|
|
5031
|
+
resourceType: request.resourceType(),
|
|
5032
|
+
status: null,
|
|
5033
|
+
failed: false,
|
|
5034
|
+
failure: null,
|
|
5035
|
+
resourceMiss: target.kind === "single-html" && protocol === "file:" && requestDocument !== candidateDocument
|
|
5036
|
+
};
|
|
5037
|
+
}
|
|
5038
|
+
function summarizeRequests(records) {
|
|
5039
|
+
return {
|
|
5040
|
+
documents: records.filter((record) => record.resourceType === "document").length,
|
|
5041
|
+
http: records.filter((record) => requestProtocol(record.url) === "http:").length,
|
|
5042
|
+
https: records.filter((record) => requestProtocol(record.url) === "https:").length,
|
|
5043
|
+
failed: records.filter((record) => record.failed).length,
|
|
5044
|
+
resourceMisses: records.filter((record) => record.resourceMiss).length,
|
|
5045
|
+
entries: records.map((record) => ({ ...record }))
|
|
5046
|
+
};
|
|
5047
|
+
}
|
|
5048
|
+
function singleHtmlRuntimeClean(target, runtime, requests) {
|
|
5049
|
+
if (target.kind !== "single-html") return true;
|
|
5050
|
+
const singleHtml = runtime?.singleHtml;
|
|
5051
|
+
return singleHtml?.ready === true && singleHtml.resourceMisses.length === 0 && singleHtml.externalRequests.length === 0 && requests.http === 0 && requests.https === 0 && requests.failed === 0 && requests.resourceMisses === 0;
|
|
5052
|
+
}
|
|
5053
|
+
function markResponse(records, response) {
|
|
5054
|
+
const record = records.get(response.request());
|
|
5055
|
+
if (record === void 0) return;
|
|
5056
|
+
record.status = response.status();
|
|
5057
|
+
if (record.status >= 400) record.resourceMiss = true;
|
|
5058
|
+
}
|
|
5059
|
+
function markFailed(records, request) {
|
|
5060
|
+
const record = records.get(request);
|
|
5061
|
+
if (record === void 0) return;
|
|
5062
|
+
record.failed = true;
|
|
5063
|
+
record.failure = request.failure()?.errorText ?? "request failed";
|
|
5064
|
+
record.resourceMiss = true;
|
|
5065
|
+
}
|
|
4202
5066
|
async function openBrowserCaptureSession(root, options) {
|
|
4203
5067
|
const backend = options.backend ?? (options.software === true ? "software" : "auto");
|
|
5068
|
+
const launchProfile = options.launchProfile ?? "development";
|
|
5069
|
+
const requestedTarget = options.target ?? { kind: "project" };
|
|
5070
|
+
const target = requestedTarget.kind === "single-html" ? { kind: "single-html", path: resolve(requestedTarget.path) } : { kind: "project" };
|
|
5071
|
+
if (target.kind === "single-html") {
|
|
5072
|
+
if (!target.path.toLowerCase().endsWith(".html")) {
|
|
5073
|
+
fail(
|
|
5074
|
+
"browser-capture-target-invalid",
|
|
5075
|
+
"single-html target path to end with .html",
|
|
5076
|
+
"Pass the exact forgeax package --format single-html candidate.",
|
|
5077
|
+
{ path: target.path }
|
|
5078
|
+
);
|
|
5079
|
+
}
|
|
5080
|
+
try {
|
|
5081
|
+
const info = await stat(target.path);
|
|
5082
|
+
if (!info.isFile()) throw new Error("target is not a regular file");
|
|
5083
|
+
} catch (cause) {
|
|
5084
|
+
fail(
|
|
5085
|
+
"browser-capture-target-missing",
|
|
5086
|
+
"single-html target path to exist",
|
|
5087
|
+
"Package the game first, then pass its candidate HTML path.",
|
|
5088
|
+
{ path: target.path, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
5089
|
+
);
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
4204
5092
|
if (options.software === true && options.backend !== void 0 && options.backend !== "software") {
|
|
4205
5093
|
fail(
|
|
4206
5094
|
"browser-capture-option-conflict",
|
|
@@ -4238,27 +5126,32 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4238
5126
|
facts.value.root,
|
|
4239
5127
|
options.report ?? resolve(outputDirectory, "run.json")
|
|
4240
5128
|
);
|
|
4241
|
-
const port = options.port === void 0 || options.port === 0 ? await reservePort() : options.port;
|
|
5129
|
+
const port = target.kind === "project" ? options.port === void 0 || options.port === 0 ? await reservePort() : options.port : void 0;
|
|
4242
5130
|
let server;
|
|
4243
5131
|
let browser;
|
|
4244
5132
|
let page;
|
|
4245
5133
|
let display;
|
|
4246
5134
|
try {
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
5135
|
+
if (target.kind === "project") {
|
|
5136
|
+
const { createServer: createServer5 } = await import('vite');
|
|
5137
|
+
if (port === void 0) throw new Error("project capture did not reserve a port");
|
|
5138
|
+
server = await createServer5(
|
|
5139
|
+
await createViteConfig(facts.value, "serve", "/", {
|
|
5140
|
+
server: { port, strictPort: true }
|
|
5141
|
+
})
|
|
5142
|
+
);
|
|
5143
|
+
await server.listen(port);
|
|
5144
|
+
}
|
|
4254
5145
|
display = await startVirtualDisplay(width, height);
|
|
4255
5146
|
const captureDisplay = display;
|
|
4256
5147
|
const icd = await lavapipeIcd();
|
|
4257
5148
|
const consoleErrors = [];
|
|
4258
5149
|
const pageErrors = [];
|
|
5150
|
+
const requestRecords = [];
|
|
5151
|
+
const requestByObject = /* @__PURE__ */ new Map();
|
|
4259
5152
|
const hasDisplay = display.value !== void 0;
|
|
4260
5153
|
const headless = options.headless ?? (!hasDisplay && process.platform !== "linux");
|
|
4261
|
-
const baseUrl = new URL(server
|
|
5154
|
+
const baseUrl = target.kind === "project" ? new URL(server?.resolvedUrls?.local[0] ?? `http://127.0.0.1:${port ?? 0}/`) : void 0;
|
|
4262
5155
|
const openPage = async (launchBackend) => {
|
|
4263
5156
|
const browserEnvironment = {
|
|
4264
5157
|
...Object.fromEntries(
|
|
@@ -4272,7 +5165,7 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4272
5165
|
const launchOptions = {
|
|
4273
5166
|
headless,
|
|
4274
5167
|
env: browserEnvironment,
|
|
4275
|
-
args: browserLaunchArgs(launchBackend),
|
|
5168
|
+
args: browserLaunchArgs(launchBackend, launchProfile),
|
|
4276
5169
|
...browserPath === void 0 ? {} : { executablePath: browserPath }
|
|
4277
5170
|
};
|
|
4278
5171
|
browser = await chromium.launch(launchOptions);
|
|
@@ -4289,7 +5182,14 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4289
5182
|
if (message.type() === "error") consoleErrors.push(message.text());
|
|
4290
5183
|
});
|
|
4291
5184
|
page.on("pageerror", (error) => pageErrors.push(String(error)));
|
|
4292
|
-
const captureUrl2 = new URL(
|
|
5185
|
+
const captureUrl2 = target.kind === "single-html" ? new URL(pathToFileURL(target.path).href) : new URL(baseUrl?.href ?? "http://127.0.0.1/");
|
|
5186
|
+
page.on("request", (request) => {
|
|
5187
|
+
const record = requestRecord(request, target, captureUrl2.href);
|
|
5188
|
+
requestRecords.push(record);
|
|
5189
|
+
requestByObject.set(request, record);
|
|
5190
|
+
});
|
|
5191
|
+
page.on("response", (response) => markResponse(requestByObject, response));
|
|
5192
|
+
page.on("requestfailed", (request) => markFailed(requestByObject, request));
|
|
4293
5193
|
if (options.deterministic === true) captureUrl2.searchParams.set("forgeaxCapture", "1");
|
|
4294
5194
|
await page.goto(captureUrl2.href, { waitUntil: "domcontentloaded", timeout: 12e4 });
|
|
4295
5195
|
await page.waitForFunction(
|
|
@@ -4315,10 +5215,12 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4315
5215
|
page = void 0;
|
|
4316
5216
|
consoleErrors.length = 0;
|
|
4317
5217
|
pageErrors.length = 0;
|
|
5218
|
+
requestRecords.length = 0;
|
|
5219
|
+
requestByObject.clear();
|
|
4318
5220
|
runtime = await openPage("software");
|
|
4319
5221
|
observed = observedBackend(runtime);
|
|
4320
5222
|
}
|
|
4321
|
-
const captureUrl = new URL(
|
|
5223
|
+
const captureUrl = target.kind === "single-html" ? new URL(pathToFileURL(target.path).href) : new URL(baseUrl?.href ?? "http://127.0.0.1/");
|
|
4322
5224
|
if (options.deterministic === true) captureUrl.searchParams.set("forgeaxCapture", "1");
|
|
4323
5225
|
if (page === void 0 || browser === void 0) {
|
|
4324
5226
|
fail(
|
|
@@ -4328,6 +5230,7 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4328
5230
|
);
|
|
4329
5231
|
}
|
|
4330
5232
|
const captures = [];
|
|
5233
|
+
let latestRuntime = runtime;
|
|
4331
5234
|
let closed = false;
|
|
4332
5235
|
const report = {
|
|
4333
5236
|
schemaVersion: "2.0.0",
|
|
@@ -4336,6 +5239,8 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4336
5239
|
mode: "browser-compositor",
|
|
4337
5240
|
root: facts.value.root,
|
|
4338
5241
|
url: captureUrl.href,
|
|
5242
|
+
target,
|
|
5243
|
+
launchProfile,
|
|
4339
5244
|
report: reportPath,
|
|
4340
5245
|
backendRequested: backend,
|
|
4341
5246
|
backend: observed,
|
|
@@ -4354,14 +5259,19 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4354
5259
|
display: display.value ?? null,
|
|
4355
5260
|
lavapipeIcd: icd ?? null,
|
|
4356
5261
|
captures,
|
|
5262
|
+
requests: summarizeRequests(requestRecords),
|
|
5263
|
+
singleHtml: runtime.singleHtml,
|
|
4357
5264
|
consoleErrors,
|
|
4358
5265
|
pageErrors,
|
|
4359
5266
|
boundary: "Browser-compositor capture is visual iteration evidence, not physical-GPU performance, HDR-display output, or release acceptance."
|
|
4360
5267
|
};
|
|
4361
5268
|
await writeRunReport(report);
|
|
4362
5269
|
const updateReport = async () => {
|
|
5270
|
+
const requests = summarizeRequests(requestRecords);
|
|
4363
5271
|
Object.assign(report, {
|
|
4364
|
-
|
|
5272
|
+
requests,
|
|
5273
|
+
singleHtml: latestRuntime?.singleHtml ?? null,
|
|
5274
|
+
ok: captures.length > 0 && captures.every((capture) => capture.ok) && consoleErrors.length === 0 && pageErrors.length === 0 && singleHtmlRuntimeClean(target, latestRuntime, requests)
|
|
4365
5275
|
});
|
|
4366
5276
|
await writeRunReport(report);
|
|
4367
5277
|
};
|
|
@@ -4426,12 +5336,14 @@ async function openBrowserCaptureSession(root, options) {
|
|
|
4426
5336
|
}
|
|
4427
5337
|
}
|
|
4428
5338
|
const runtime2 = await runtimeWitness(activePage);
|
|
5339
|
+
latestRuntime = runtime2;
|
|
5340
|
+
const requestSummary = summarizeRequests(requestRecords);
|
|
4429
5341
|
const uiPresent = runtime2.domUi.rootChildren > 0;
|
|
4430
5342
|
const requireUi = captureOptions.requireUi ?? options.requireUi ?? false;
|
|
4431
5343
|
const captureBackend = observedBackend(runtime2);
|
|
4432
5344
|
if (captureBackend !== "unknown") Object.assign(report, { backend: captureBackend });
|
|
4433
5345
|
const backendMatches = backend === "auto" || captureBackend === backend;
|
|
4434
|
-
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);
|
|
5346
|
+
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);
|
|
4435
5347
|
const index = captures.length + 1;
|
|
4436
5348
|
const output = resolve(
|
|
4437
5349
|
facts.value.root,
|
|
@@ -4646,31 +5558,47 @@ function releaseSlug(name) {
|
|
|
4646
5558
|
async function packageCommand(options = {}) {
|
|
4647
5559
|
const facts = await readProjectFacts(options.root);
|
|
4648
5560
|
if (!facts.ok) return facts;
|
|
4649
|
-
const
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
if (!
|
|
5561
|
+
const format = options.format ?? "web-zip";
|
|
5562
|
+
if (format !== "web-zip" && format !== "single-html") return packageFormatError(String(format));
|
|
5563
|
+
const defaultOutput = format === "single-html" ? `release/${releaseSlug(facts.value.name)}-offline.html` : `release/${releaseSlug(facts.value.name)}-web.zip`;
|
|
5564
|
+
const output = resolve(facts.value.root, options.output ?? defaultOutput);
|
|
5565
|
+
const expectedSuffix = format === "single-html" ? ".html" : ".zip";
|
|
5566
|
+
if (!output.toLowerCase().endsWith(expectedSuffix)) {
|
|
5567
|
+
return packageOutputError(output, format);
|
|
5568
|
+
}
|
|
4655
5569
|
const distRoot = resolve(facts.value.root, "dist");
|
|
4656
|
-
const
|
|
4657
|
-
if (!
|
|
4658
|
-
const archive = resolve(
|
|
4659
|
-
facts.value.root,
|
|
4660
|
-
options.output ?? `release/${releaseSlug(facts.value.name)}-web.zip`
|
|
4661
|
-
);
|
|
4662
|
-
const archiveRelativeToDist = relative(distRoot, archive);
|
|
4663
|
-
if (archiveRelativeToDist === "" || !archiveRelativeToDist.startsWith("..")) {
|
|
5570
|
+
const outputRelativeToDist = relative(distRoot, output);
|
|
5571
|
+
if (outputRelativeToDist === "" || !outputRelativeToDist.startsWith("..") && outputRelativeToDist !== "..") {
|
|
4664
5572
|
return {
|
|
4665
5573
|
ok: false,
|
|
4666
5574
|
error: {
|
|
4667
5575
|
code: "release-output-inside-dist",
|
|
4668
|
-
expected: "the release
|
|
4669
|
-
hint: "Use --output release/<game>-web.zip or
|
|
4670
|
-
detail: {
|
|
5576
|
+
expected: "the release artifact to live outside the derived dist directory",
|
|
5577
|
+
hint: "Use --output release/<game>-web.zip or release/<game>-offline.html.",
|
|
5578
|
+
detail: { output, distRoot, format }
|
|
4671
5579
|
}
|
|
4672
5580
|
};
|
|
4673
5581
|
}
|
|
5582
|
+
const built = await buildCommand({
|
|
5583
|
+
root: facts.value.root,
|
|
5584
|
+
base: "./",
|
|
5585
|
+
...options.json === void 0 ? {} : { json: options.json }
|
|
5586
|
+
});
|
|
5587
|
+
if (!built.ok) return built;
|
|
5588
|
+
const verified = await verifyDist(distRoot);
|
|
5589
|
+
if (!verified.ok) return verified;
|
|
5590
|
+
if (format === "single-html") {
|
|
5591
|
+
const indexHtml = await readFile(resolve(distRoot, "index.html"), "utf8");
|
|
5592
|
+
const bundle = await bundleSingleHtmlEntry(distRoot, indexHtml, facts.value.root);
|
|
5593
|
+
if (!bundle.ok) return bundle;
|
|
5594
|
+
return writeSingleHtml({
|
|
5595
|
+
distRoot,
|
|
5596
|
+
output,
|
|
5597
|
+
manifest: verified.value,
|
|
5598
|
+
bundle: bundle.value
|
|
5599
|
+
});
|
|
5600
|
+
}
|
|
5601
|
+
const archive = output;
|
|
4674
5602
|
const temporaryArchive = `${archive}.partial-${process.pid}`;
|
|
4675
5603
|
const checksumPath = `${archive}.sha256`;
|
|
4676
5604
|
const temporaryChecksum = `${checksumPath}.partial-${process.pid}`;
|
|
@@ -4840,6 +5768,7 @@ var init_commands = __esm({
|
|
|
4840
5768
|
init_dist();
|
|
4841
5769
|
init_host();
|
|
4842
5770
|
init_project();
|
|
5771
|
+
init_single_html();
|
|
4843
5772
|
init_types();
|
|
4844
5773
|
init_assets();
|
|
4845
5774
|
init_bootstrap_commands();
|
|
@@ -4854,6 +5783,19 @@ var init_commands = __esm({
|
|
|
4854
5783
|
init_operations();
|
|
4855
5784
|
}
|
|
4856
5785
|
});
|
|
5786
|
+
function createNativePreviewCatalog(host, selectedName) {
|
|
5787
|
+
return new Map(
|
|
5788
|
+
nativePreviewPlugins.filter(([name]) => selectedName === void 0 || name === selectedName).map(([name, plugin]) => [
|
|
5789
|
+
name,
|
|
5790
|
+
{
|
|
5791
|
+
realm: "host",
|
|
5792
|
+
load: async () => ({
|
|
5793
|
+
default: bindPreviewHost(plugin, host)
|
|
5794
|
+
})
|
|
5795
|
+
}
|
|
5796
|
+
])
|
|
5797
|
+
);
|
|
5798
|
+
}
|
|
4857
5799
|
var nativePreviewPlugins, nativePreviewTools, nativePreviewDescriptors;
|
|
4858
5800
|
var init_preview_catalog = __esm({
|
|
4859
5801
|
"src/tools/preview-catalog.ts"() {
|
|
@@ -5350,11 +6292,11 @@ async function runProjectTool(binding, args, options) {
|
|
|
5350
6292
|
);
|
|
5351
6293
|
await loader.root.update(entries2);
|
|
5352
6294
|
await loader.await();
|
|
5353
|
-
const { createToolRuntime:
|
|
5354
|
-
const { createContextCapabilityResolver } = await import('@forgeax/engine-plugin');
|
|
5355
|
-
return await
|
|
6295
|
+
const { createToolRuntime: createToolRuntime6 } = await import('@forgeax/engine-tool-runtime');
|
|
6296
|
+
const { createContextCapabilityResolver: createContextCapabilityResolver2 } = await import('@forgeax/engine-plugin');
|
|
6297
|
+
return await createToolRuntime6([contribution]).run(contribution, args, {
|
|
5356
6298
|
...options,
|
|
5357
|
-
capabilityResolver:
|
|
6299
|
+
capabilityResolver: createContextCapabilityResolver2(ctx)
|
|
5358
6300
|
}).terminal;
|
|
5359
6301
|
} catch (cause) {
|
|
5360
6302
|
return activationFailure(cause);
|
|
@@ -5366,6 +6308,50 @@ var init_project_tools = __esm({
|
|
|
5366
6308
|
"src/tools/project-tools.ts"() {
|
|
5367
6309
|
}
|
|
5368
6310
|
});
|
|
6311
|
+
|
|
6312
|
+
// src/tools/contributions.ts
|
|
6313
|
+
var contributions_exports = {};
|
|
6314
|
+
__export(contributions_exports, {
|
|
6315
|
+
createAuthorContribution: () => createAuthorContribution,
|
|
6316
|
+
createBuildContribution: () => createBuildContribution,
|
|
6317
|
+
createDefaultContributions: () => createDefaultContributions
|
|
6318
|
+
});
|
|
6319
|
+
function commandFailure2(error) {
|
|
6320
|
+
return { ok: false, error: { ...error, detail: error.detail } };
|
|
6321
|
+
}
|
|
6322
|
+
function createBuildContribution(projectRoot = process.cwd()) {
|
|
6323
|
+
return defineTool(
|
|
6324
|
+
projectBuildDescriptor,
|
|
6325
|
+
async (options) => {
|
|
6326
|
+
const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
6327
|
+
const result = await buildCommand2({ ...options, root: options.root ?? projectRoot });
|
|
6328
|
+
return result.ok ? result.value : commandFailure2(result.error);
|
|
6329
|
+
}
|
|
6330
|
+
);
|
|
6331
|
+
}
|
|
6332
|
+
function createAuthorContribution(projectRoot = process.cwd()) {
|
|
6333
|
+
return defineTool(
|
|
6334
|
+
authorPluginInstallDescriptor,
|
|
6335
|
+
async (options) => {
|
|
6336
|
+
const { pluginInstallCommand: pluginInstallCommand2 } = await Promise.resolve().then(() => (init_plugin_authoring(), plugin_authoring_exports));
|
|
6337
|
+
const result = await pluginInstallCommand2({ ...options, root: options.root ?? projectRoot });
|
|
6338
|
+
return result.ok ? result.value : commandFailure2(result.error);
|
|
6339
|
+
}
|
|
6340
|
+
);
|
|
6341
|
+
}
|
|
6342
|
+
function createDefaultContributions(projectRoot = process.cwd()) {
|
|
6343
|
+
return [
|
|
6344
|
+
createBuildContribution(projectRoot),
|
|
6345
|
+
createAuthorContribution(projectRoot),
|
|
6346
|
+
...nativePreviewTools
|
|
6347
|
+
];
|
|
6348
|
+
}
|
|
6349
|
+
var init_contributions = __esm({
|
|
6350
|
+
"src/tools/contributions.ts"() {
|
|
6351
|
+
init_catalog();
|
|
6352
|
+
init_preview_catalog();
|
|
6353
|
+
}
|
|
6354
|
+
});
|
|
5369
6355
|
function analyzePreviewArtifacts(request) {
|
|
5370
6356
|
const required = request.required ?? [];
|
|
5371
6357
|
const validated = request.manifest.schemaVersion === "1.0.0" ? required.some((kind) => kind !== "rhi-tape" && kind !== "png" && kind !== "profile-capture") ? {
|
|
@@ -5416,14 +6402,6 @@ function createPreviewContributions() {
|
|
|
5416
6402
|
createOfflineAnalysisContribution()
|
|
5417
6403
|
];
|
|
5418
6404
|
}
|
|
5419
|
-
function createDomainPreviewContributions() {
|
|
5420
|
-
return [
|
|
5421
|
-
createMaterialPreviewContribution(),
|
|
5422
|
-
createMeshPreviewContribution(),
|
|
5423
|
-
createVfxPreviewContribution(),
|
|
5424
|
-
createTexturePreviewContribution()
|
|
5425
|
-
];
|
|
5426
|
-
}
|
|
5427
6405
|
var init_preview_contributions = __esm({
|
|
5428
6406
|
"src/tools/preview-contributions.ts"() {
|
|
5429
6407
|
init_catalog();
|
|
@@ -5432,50 +6410,6 @@ var init_preview_contributions = __esm({
|
|
|
5432
6410
|
}
|
|
5433
6411
|
});
|
|
5434
6412
|
|
|
5435
|
-
// src/tools/contributions.ts
|
|
5436
|
-
var contributions_exports = {};
|
|
5437
|
-
__export(contributions_exports, {
|
|
5438
|
-
createAuthorContribution: () => createAuthorContribution,
|
|
5439
|
-
createBuildContribution: () => createBuildContribution,
|
|
5440
|
-
createDefaultContributions: () => createDefaultContributions
|
|
5441
|
-
});
|
|
5442
|
-
function commandFailure2(error) {
|
|
5443
|
-
return { ok: false, error: { ...error, detail: error.detail } };
|
|
5444
|
-
}
|
|
5445
|
-
function createBuildContribution(projectRoot = process.cwd()) {
|
|
5446
|
-
return defineTool(
|
|
5447
|
-
projectBuildDescriptor,
|
|
5448
|
-
async (options) => {
|
|
5449
|
-
const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5450
|
-
const result = await buildCommand2({ ...options, root: options.root ?? projectRoot });
|
|
5451
|
-
return result.ok ? result.value : commandFailure2(result.error);
|
|
5452
|
-
}
|
|
5453
|
-
);
|
|
5454
|
-
}
|
|
5455
|
-
function createAuthorContribution(projectRoot = process.cwd()) {
|
|
5456
|
-
return defineTool(
|
|
5457
|
-
authorPluginInstallDescriptor,
|
|
5458
|
-
async (options) => {
|
|
5459
|
-
const { pluginInstallCommand: pluginInstallCommand2 } = await Promise.resolve().then(() => (init_plugin_authoring(), plugin_authoring_exports));
|
|
5460
|
-
const result = await pluginInstallCommand2({ ...options, root: options.root ?? projectRoot });
|
|
5461
|
-
return result.ok ? result.value : commandFailure2(result.error);
|
|
5462
|
-
}
|
|
5463
|
-
);
|
|
5464
|
-
}
|
|
5465
|
-
function createDefaultContributions(projectRoot = process.cwd()) {
|
|
5466
|
-
return [
|
|
5467
|
-
createBuildContribution(projectRoot),
|
|
5468
|
-
createAuthorContribution(projectRoot),
|
|
5469
|
-
...createDomainPreviewContributions()
|
|
5470
|
-
];
|
|
5471
|
-
}
|
|
5472
|
-
var init_contributions = __esm({
|
|
5473
|
-
"src/tools/contributions.ts"() {
|
|
5474
|
-
init_catalog();
|
|
5475
|
-
init_preview_contributions();
|
|
5476
|
-
}
|
|
5477
|
-
});
|
|
5478
|
-
|
|
5479
6413
|
// src/tools/runtime.ts
|
|
5480
6414
|
var runtime_exports = {};
|
|
5481
6415
|
__export(runtime_exports, {
|
|
@@ -5493,1078 +6427,1336 @@ var init_runtime = __esm({
|
|
|
5493
6427
|
init_preview_contributions();
|
|
5494
6428
|
}
|
|
5495
6429
|
});
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
init_commands();
|
|
5499
|
-
init_dist();
|
|
5500
|
-
init_engine_binding();
|
|
5501
|
-
init_init();
|
|
5502
|
-
init_project();
|
|
5503
|
-
init_operations();
|
|
5504
|
-
init_software_capture();
|
|
5505
|
-
|
|
5506
|
-
// src/tools/benchmark/statistics.ts
|
|
5507
|
-
function calculateSampleStatistics(samples) {
|
|
5508
|
-
if (samples.length === 0) throw new RangeError("benchmark samples must not be empty");
|
|
5509
|
-
const sorted = [...samples].sort((left, right) => left - right);
|
|
5510
|
-
const medianIndex = (sorted.length - 1) / 2;
|
|
5511
|
-
const median = interpolate(sorted, medianIndex);
|
|
5512
|
-
const p95 = interpolate(sorted, Math.ceil(sorted.length * 0.95) - 1);
|
|
5513
|
-
const max = sorted[sorted.length - 1];
|
|
5514
|
-
if (max === void 0) throw new RangeError("benchmark samples must not be empty");
|
|
5515
|
-
return { count: sorted.length, median, p95, max };
|
|
5516
|
-
}
|
|
5517
|
-
function interpolate(values, index) {
|
|
5518
|
-
const lower = Math.floor(index);
|
|
5519
|
-
const upper = Math.ceil(index);
|
|
5520
|
-
const lowerValue = values[lower];
|
|
5521
|
-
const upperValue = values[upper];
|
|
5522
|
-
if (lowerValue === void 0 || upperValue === void 0) {
|
|
5523
|
-
throw new RangeError("benchmark percentile index is out of range");
|
|
5524
|
-
}
|
|
5525
|
-
return lower === upper ? lowerValue : lowerValue + (upperValue - lowerValue) * (index - lower);
|
|
5526
|
-
}
|
|
5527
|
-
|
|
5528
|
-
// src/tools/benchmark/report.ts
|
|
5529
|
-
var DEFAULT_ADMISSION_THRESHOLDS = {
|
|
5530
|
-
sampleCountPerPhase: 30,
|
|
5531
|
-
medianImprovement: 0.2,
|
|
5532
|
-
p95Improvement: 0.1,
|
|
5533
|
-
maxRegression: 0.1,
|
|
5534
|
-
peakRssMultiplier: 1.25
|
|
5535
|
-
};
|
|
5536
|
-
function summarizeBenchmarkSamples(samples) {
|
|
5537
|
-
if (samples.length === 0) {
|
|
6430
|
+
function transportCause(cause) {
|
|
6431
|
+
if (cause instanceof Error) {
|
|
5538
6432
|
return {
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
cleanupPassed: false,
|
|
5543
|
-
evictionPassed: false,
|
|
5544
|
-
exclusivePhasesMs: {}
|
|
6433
|
+
name: cause.name,
|
|
6434
|
+
message: cause.message,
|
|
6435
|
+
...cause.stack === void 0 ? {} : { stack: cause.stack }
|
|
5545
6436
|
};
|
|
5546
6437
|
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
);
|
|
6438
|
+
if (cause === void 0) return "undefined";
|
|
6439
|
+
try {
|
|
6440
|
+
return JSON.parse(JSON.stringify(cause));
|
|
6441
|
+
} catch {
|
|
6442
|
+
return String(cause);
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
function dataUriBytes(uri) {
|
|
6446
|
+
const separator = uri.indexOf(",");
|
|
6447
|
+
if (separator < 0) throw new TypeError("preview artifact must be a data URI before publication");
|
|
6448
|
+
return Buffer.from(uri.slice(separator + 1), "base64");
|
|
6449
|
+
}
|
|
6450
|
+
function projectUri(projectRoot, path) {
|
|
6451
|
+
return relative(projectRoot, path).split(sep).join("/");
|
|
6452
|
+
}
|
|
6453
|
+
async function allocateLoopbackPort() {
|
|
6454
|
+
const probe = createServer$1();
|
|
6455
|
+
try {
|
|
6456
|
+
await new Promise((resolve19, reject) => {
|
|
6457
|
+
const onError = (error) => {
|
|
6458
|
+
probe.off("listening", onListening);
|
|
6459
|
+
reject(error);
|
|
6460
|
+
};
|
|
6461
|
+
const onListening = () => {
|
|
6462
|
+
probe.off("error", onError);
|
|
6463
|
+
resolve19();
|
|
6464
|
+
};
|
|
6465
|
+
probe.once("error", onError);
|
|
6466
|
+
probe.once("listening", onListening);
|
|
6467
|
+
probe.listen(0, "127.0.0.1");
|
|
6468
|
+
});
|
|
6469
|
+
const address = probe.address();
|
|
6470
|
+
if (address === null || typeof address === "string") {
|
|
6471
|
+
throw new Error("loopback port probe did not expose a TCP address");
|
|
6472
|
+
}
|
|
6473
|
+
return address.port;
|
|
6474
|
+
} finally {
|
|
6475
|
+
if (probe.listening) {
|
|
6476
|
+
await new Promise((resolve19) => probe.close(() => resolve19()));
|
|
6477
|
+
}
|
|
6478
|
+
}
|
|
6479
|
+
}
|
|
6480
|
+
function sha256(bytes) {
|
|
6481
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
6482
|
+
}
|
|
6483
|
+
function artifactRef(artifact) {
|
|
6484
|
+
const kind = artifact.kind === "report" || artifact.kind === "contact-sheet" ? "tool-result" : artifact.kind;
|
|
5558
6485
|
return {
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
exclusivePhasesMs
|
|
6486
|
+
kind,
|
|
6487
|
+
digest: artifact.digest,
|
|
6488
|
+
uri: artifact.uri,
|
|
6489
|
+
mediaType: artifact.mediaType,
|
|
6490
|
+
sizeBytes: artifact.byteLength
|
|
5565
6491
|
};
|
|
5566
6492
|
}
|
|
5567
|
-
function
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
const serviceReport = summarizeBenchmarkSamples(serviceSamples);
|
|
5573
|
-
const reasons = [];
|
|
5574
|
-
const expectedSamples = thresholds.sampleCountPerPhase * 2;
|
|
5575
|
-
const complete = privateSamples.length === expectedSamples && serviceSamples.length === expectedSamples;
|
|
5576
|
-
if (!complete) {
|
|
5577
|
-
reasons.push("sample count is incomplete");
|
|
5578
|
-
}
|
|
5579
|
-
if (samples.some((sample) => sample.recipeDigest !== recipe.digest)) {
|
|
5580
|
-
reasons.push("recipe identity drifted");
|
|
6493
|
+
async function publishPreviewArtifacts(projectRoot, runId, result, reportInput) {
|
|
6494
|
+
if (result.manifest.identity.runId !== runId) {
|
|
6495
|
+
throw new Error(
|
|
6496
|
+
`preview artifact run identity mismatch: manifest=${result.manifest.identity.runId} requested=${runId}`
|
|
6497
|
+
);
|
|
5581
6498
|
}
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
if (complete && serviceReport.durationMs.p95 > p95Limit)
|
|
5595
|
-
reasons.push("p95 improvement threshold missed");
|
|
5596
|
-
if (complete && serviceReport.durationMs.max > maxLimit)
|
|
5597
|
-
reasons.push("max regression threshold exceeded");
|
|
5598
|
-
if (complete && serviceReport.peakRssBytes.median > rssLimit)
|
|
5599
|
-
reasons.push("peak RSS threshold exceeded");
|
|
5600
|
-
return {
|
|
5601
|
-
schema: "forgeax.tool-service-admission.v1",
|
|
5602
|
-
recipe,
|
|
5603
|
-
thresholds,
|
|
5604
|
-
order,
|
|
5605
|
-
private: privateReport,
|
|
5606
|
-
service: serviceReport,
|
|
5607
|
-
valid: complete && !reasons.some(
|
|
5608
|
-
(reason) => [
|
|
5609
|
-
"recipe identity drifted",
|
|
5610
|
-
"cleanup failed",
|
|
5611
|
-
"eviction failed",
|
|
5612
|
-
"private and service samples were not alternated"
|
|
5613
|
-
].includes(reason)
|
|
5614
|
-
),
|
|
5615
|
-
admitted: reasons.length === 0,
|
|
5616
|
-
reasons
|
|
6499
|
+
const runsRoot = join(projectRoot, ".forgeax", "tool-runs");
|
|
6500
|
+
await mkdir(runsRoot, { recursive: true });
|
|
6501
|
+
const directoryName = runId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
6502
|
+
const published = join(runsRoot, directoryName);
|
|
6503
|
+
const finalPaths = {
|
|
6504
|
+
tapeJson: join(published, "rhi-tape.json"),
|
|
6505
|
+
tapeBlob: join(published, "rhi-tape.bin"),
|
|
6506
|
+
capturePng: join(published, "capture.png"),
|
|
6507
|
+
freshReplayPng: join(published, "fresh-replay.png"),
|
|
6508
|
+
profile: join(published, "profile.json"),
|
|
6509
|
+
manifest: join(published, "manifest.json"),
|
|
6510
|
+
report: join(published, "report.json")
|
|
5617
6511
|
};
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
const
|
|
5623
|
-
const
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
6512
|
+
const tapeJson = dataUriBytes(result.tape.jsonUri);
|
|
6513
|
+
const tapeBlob = dataUriBytes(result.tape.blobUri);
|
|
6514
|
+
const capturePng = dataUriBytes(result.capturePng.uri);
|
|
6515
|
+
const freshReplayPng = dataUriBytes(result.png.uri);
|
|
6516
|
+
const profile = dataUriBytes(result.profile.uri);
|
|
6517
|
+
const bytesByRole = {
|
|
6518
|
+
"rhi-tape": tapeJson,
|
|
6519
|
+
capture: capturePng,
|
|
6520
|
+
"fresh-replay": freshReplayPng,
|
|
6521
|
+
"profile-capture": profile
|
|
6522
|
+
};
|
|
6523
|
+
const uriByRole = {
|
|
6524
|
+
"rhi-tape": projectUri(projectRoot, finalPaths.tapeJson),
|
|
6525
|
+
capture: projectUri(projectRoot, finalPaths.capturePng),
|
|
6526
|
+
"fresh-replay": projectUri(projectRoot, finalPaths.freshReplayPng),
|
|
6527
|
+
"profile-capture": projectUri(projectRoot, finalPaths.profile)
|
|
6528
|
+
};
|
|
6529
|
+
const mediaTypeByRole = {
|
|
6530
|
+
"rhi-tape": "application/vnd.forgeax.rhi-tape+json",
|
|
6531
|
+
capture: "image/png",
|
|
6532
|
+
"fresh-replay": "image/png",
|
|
6533
|
+
"profile-capture": "application/vnd.forgeax.profile+json"
|
|
6534
|
+
};
|
|
6535
|
+
const requiredRoles = ["rhi-tape", "capture", "fresh-replay", "profile-capture"];
|
|
6536
|
+
const nonReportArtifacts = result.manifest.artifacts.filter((artifact) => artifact.role !== "report").map((artifact) => {
|
|
6537
|
+
if (!(artifact.role in bytesByRole)) {
|
|
6538
|
+
return artifact;
|
|
5643
6539
|
}
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
if (!cloneSafe.ok) return cloneSafe;
|
|
5650
|
-
if (!input.supportedRealms.includes(input.realm)) {
|
|
5651
|
-
return {
|
|
5652
|
-
ok: false,
|
|
5653
|
-
error: {
|
|
5654
|
-
code: "realm-capability-unavailable",
|
|
5655
|
-
realm: input.realm,
|
|
5656
|
-
supportedRealms: input.supportedRealms
|
|
5657
|
-
}
|
|
5658
|
-
};
|
|
5659
|
-
}
|
|
5660
|
-
const entries2 = projectPluginEntries(input.entries, input.realm, input.realm);
|
|
5661
|
-
if (input.realm === "build") {
|
|
5662
|
-
if (input.lifecycle === void 0) {
|
|
5663
|
-
return {
|
|
5664
|
-
ok: false,
|
|
5665
|
-
error: {
|
|
5666
|
-
code: "realm-lifecycle-adapter-missing",
|
|
5667
|
-
realm: input.realm,
|
|
5668
|
-
hint: "Inject a build lifecycle adapter that owns execution and stop cleanup."
|
|
5669
|
-
}
|
|
5670
|
-
};
|
|
6540
|
+
const role = artifact.role;
|
|
6541
|
+
const bytes = bytesByRole[role];
|
|
6542
|
+
const digest = sha256(bytes);
|
|
6543
|
+
if (artifact.digest !== digest) {
|
|
6544
|
+
throw new Error(`preview artifact digest mismatch for ${artifact.role}`);
|
|
5671
6545
|
}
|
|
5672
|
-
const lifecycle = await input.lifecycle.start({
|
|
5673
|
-
realm: input.realm,
|
|
5674
|
-
entries: entries2,
|
|
5675
|
-
catalog: input.catalog,
|
|
5676
|
-
catalogDigest: input.catalogDigest
|
|
5677
|
-
});
|
|
5678
|
-
return {
|
|
5679
|
-
ok: true,
|
|
5680
|
-
value: { realm: input.realm, catalogDigest: input.catalogDigest, entries: entries2, lifecycle }
|
|
5681
|
-
};
|
|
5682
|
-
}
|
|
5683
|
-
if (ctx === void 0) {
|
|
5684
6546
|
return {
|
|
5685
|
-
|
|
5686
|
-
|
|
6547
|
+
...artifact,
|
|
6548
|
+
uri: uriByRole[role],
|
|
6549
|
+
digest,
|
|
6550
|
+
byteLength: bytes.byteLength,
|
|
6551
|
+
mediaType: mediaTypeByRole[role]
|
|
5687
6552
|
};
|
|
6553
|
+
});
|
|
6554
|
+
if (requiredRoles.some((role) => !nonReportArtifacts.some((artifact) => artifact.role === role))) {
|
|
6555
|
+
throw new Error(
|
|
6556
|
+
"preview publication is missing capture, fresh-replay, tape, or profile artifact"
|
|
6557
|
+
);
|
|
5688
6558
|
}
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
6559
|
+
let report;
|
|
6560
|
+
let reportBytes;
|
|
6561
|
+
if (reportInput !== void 0) {
|
|
6562
|
+
if (reportInput.snapshot.digest !== result.manifest.identity.snapshotDigest) {
|
|
6563
|
+
throw new Error("preview report snapshot identity does not match the artifact manifest");
|
|
6564
|
+
}
|
|
6565
|
+
report = createResourcePreviewReport({
|
|
6566
|
+
runId,
|
|
6567
|
+
snapshot: reportInput.snapshot,
|
|
6568
|
+
subject: reportInput.subject,
|
|
6569
|
+
presentation: reportInput.presentation,
|
|
6570
|
+
oracle: reportInput.oracle,
|
|
6571
|
+
artifacts: nonReportArtifacts
|
|
6572
|
+
});
|
|
6573
|
+
reportBytes = new TextEncoder().encode(JSON.stringify(report));
|
|
6574
|
+
}
|
|
6575
|
+
const reportArtifact = reportBytes === void 0 ? void 0 : {
|
|
6576
|
+
owner: "resource-preview",
|
|
6577
|
+
kind: "report",
|
|
6578
|
+
role: "report",
|
|
6579
|
+
uri: projectUri(projectRoot, finalPaths.report),
|
|
6580
|
+
digest: sha256(reportBytes),
|
|
6581
|
+
byteLength: reportBytes.byteLength,
|
|
6582
|
+
mediaType: "application/vnd.forgeax.resource-preview+json",
|
|
6583
|
+
derivedFrom: nonReportArtifacts.map((artifact) => artifact.digest)
|
|
6584
|
+
};
|
|
6585
|
+
const manifest = createPreviewArtifactManifest({
|
|
6586
|
+
...result.manifest,
|
|
6587
|
+
artifacts: reportArtifact === void 0 ? nonReportArtifacts : [reportArtifact, ...nonReportArtifacts]
|
|
5692
6588
|
});
|
|
5693
|
-
|
|
6589
|
+
const validated = validatePreviewArtifactManifest$1(
|
|
6590
|
+
manifest,
|
|
6591
|
+
reportArtifact === void 0 ? requiredRoles : ["report", ...requiredRoles]
|
|
6592
|
+
);
|
|
6593
|
+
if (!validated.ok) throw new Error(JSON.stringify(validated.error.detail));
|
|
6594
|
+
const staging = await mkdtemp(join(runsRoot, ".staging-"));
|
|
6595
|
+
const paths = {
|
|
6596
|
+
tapeJson: join(staging, "rhi-tape.json"),
|
|
6597
|
+
tapeBlob: join(staging, "rhi-tape.bin"),
|
|
6598
|
+
capturePng: join(staging, "capture.png"),
|
|
6599
|
+
freshReplayPng: join(staging, "fresh-replay.png"),
|
|
6600
|
+
profile: join(staging, "profile.json"),
|
|
6601
|
+
manifest: join(staging, "manifest.json"),
|
|
6602
|
+
report: join(staging, "report.json")
|
|
6603
|
+
};
|
|
6604
|
+
try {
|
|
6605
|
+
await Promise.all([
|
|
6606
|
+
writeFile(paths.tapeJson, tapeJson),
|
|
6607
|
+
writeFile(paths.tapeBlob, tapeBlob),
|
|
6608
|
+
writeFile(paths.capturePng, capturePng),
|
|
6609
|
+
writeFile(paths.freshReplayPng, freshReplayPng),
|
|
6610
|
+
writeFile(paths.profile, profile),
|
|
6611
|
+
...reportBytes === void 0 ? [] : [writeFile(paths.report, reportBytes)]
|
|
6612
|
+
]);
|
|
6613
|
+
await writeFile(paths.manifest, `${JSON.stringify(manifest, null, 2)}
|
|
6614
|
+
`);
|
|
6615
|
+
await rename(staging, published);
|
|
6616
|
+
} catch (cause) {
|
|
6617
|
+
await rm(staging, { recursive: true, force: true });
|
|
6618
|
+
throw cause;
|
|
6619
|
+
}
|
|
5694
6620
|
return {
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
}
|
|
6621
|
+
...result,
|
|
6622
|
+
tape: {
|
|
6623
|
+
...result.tape,
|
|
6624
|
+
jsonUri: projectUri(projectRoot, finalPaths.tapeJson),
|
|
6625
|
+
blobUri: projectUri(projectRoot, finalPaths.tapeBlob)
|
|
6626
|
+
},
|
|
6627
|
+
profile: { ...result.profile, uri: projectUri(projectRoot, finalPaths.profile) },
|
|
6628
|
+
capturePng: { ...result.capturePng, uri: projectUri(projectRoot, finalPaths.capturePng) },
|
|
6629
|
+
png: { ...result.png, uri: projectUri(projectRoot, finalPaths.freshReplayPng) },
|
|
6630
|
+
manifest,
|
|
6631
|
+
artifacts: manifest.artifacts.map(artifactRef)
|
|
5702
6632
|
};
|
|
5703
6633
|
}
|
|
5704
|
-
|
|
5705
|
-
// src/tools/cache.ts
|
|
5706
|
-
function createServiceCache() {
|
|
5707
|
-
const entries2 = /* @__PURE__ */ new Map();
|
|
6634
|
+
function browserFailure(phase, cause, pageErrors = []) {
|
|
5708
6635
|
return {
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
6636
|
+
ok: false,
|
|
6637
|
+
error: {
|
|
6638
|
+
code: "tool-preview-browser-host-failed",
|
|
6639
|
+
expected: "a real Chromium page, project bootstrap, WebGPU device, and bounded preview run",
|
|
6640
|
+
hint: "Inspect the Browser Host phase and repair the project or local WebGPU capability.",
|
|
6641
|
+
detail: {
|
|
6642
|
+
phase,
|
|
6643
|
+
cause: transportCause(cause),
|
|
6644
|
+
pageErrors
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
5714
6647
|
};
|
|
5715
6648
|
}
|
|
5716
|
-
function
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
const
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
"a carrier protocol route",
|
|
5743
|
-
"Request a fresh visible offer from the provider."
|
|
5744
|
-
)
|
|
5745
|
-
);
|
|
5746
|
-
return;
|
|
5747
|
-
}
|
|
5748
|
-
if (request.headers.authorization !== `Bearer ${bearerToken}`) {
|
|
5749
|
-
reply(
|
|
5750
|
-
response,
|
|
5751
|
-
401,
|
|
5752
|
-
errorResult(
|
|
5753
|
-
"carrier-token-invalid",
|
|
5754
|
-
"the ephemeral bearer token",
|
|
5755
|
-
"Request a fresh authenticated offer; never guess or persist bearer tokens."
|
|
5756
|
-
)
|
|
5757
|
-
);
|
|
5758
|
-
return;
|
|
5759
|
-
}
|
|
5760
|
-
try {
|
|
5761
|
-
const body = await readBody(request);
|
|
5762
|
-
const route = request.url.slice("/carrier/".length);
|
|
5763
|
-
if (route === "lease") {
|
|
5764
|
-
const result = options.machine.lease({
|
|
5765
|
-
consumerId: String(body.consumerId ?? ""),
|
|
5766
|
-
bearerToken,
|
|
5767
|
-
now: Number(body.now),
|
|
5768
|
-
descriptorDigest: typeof body.descriptorDigest === "string" ? body.descriptorDigest : void 0,
|
|
5769
|
-
recipeDigest: typeof body.recipeDigest === "string" ? body.recipeDigest : void 0
|
|
5770
|
-
});
|
|
5771
|
-
reply(response, result.ok ? 200 : 409, result.ok ? result : result);
|
|
5772
|
-
return;
|
|
5773
|
-
}
|
|
5774
|
-
const leaseId = typeof body.leaseId === "string" ? body.leaseId : "";
|
|
5775
|
-
if (route === "start") {
|
|
5776
|
-
const result = options.machine.started(leaseId);
|
|
5777
|
-
reply(response, result.ok ? 200 : 409, result.ok ? result : result);
|
|
5778
|
-
return;
|
|
6649
|
+
async function runBrowserPreviewHost(projectRoot, recipe, snapshot, runId, signal, bootstrapRoot = "project-bootstrap", resource, options = {}) {
|
|
6650
|
+
const facts = await readProjectFacts(projectRoot);
|
|
6651
|
+
if (!facts.ok)
|
|
6652
|
+
return { ok: false, error: { ...facts.error, detail: facts.error.detail } };
|
|
6653
|
+
const config = await createViteConfig(facts.value, "serve", "/", { bootstrapRoot });
|
|
6654
|
+
const cacheDir = await mkdtemp(join(facts.value.root, ".forgeax", ".browser-host-vite-"));
|
|
6655
|
+
let server;
|
|
6656
|
+
try {
|
|
6657
|
+
const port = await allocateLoopbackPort();
|
|
6658
|
+
server = await createServer$2({
|
|
6659
|
+
...config,
|
|
6660
|
+
cacheDir,
|
|
6661
|
+
logLevel: "silent",
|
|
6662
|
+
optimizeDeps: {
|
|
6663
|
+
...config.optimizeDeps,
|
|
6664
|
+
// Tool runs must observe the current workspace build, even when a
|
|
6665
|
+
// persistent runner retains Vite's optimized-dependency cache.
|
|
6666
|
+
force: true
|
|
6667
|
+
},
|
|
6668
|
+
server: {
|
|
6669
|
+
...config.server,
|
|
6670
|
+
host: "127.0.0.1",
|
|
6671
|
+
port,
|
|
6672
|
+
// Vite's port 0 means its default 5173. Bind the probed port exactly so
|
|
6673
|
+
// another project or runner cannot be mistaken for this Browser Host.
|
|
6674
|
+
strictPort: true
|
|
5779
6675
|
}
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
6676
|
+
});
|
|
6677
|
+
} catch (cause) {
|
|
6678
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
6679
|
+
throw cause;
|
|
6680
|
+
}
|
|
6681
|
+
const hostStartedAtMs = performance.now();
|
|
6682
|
+
let phase = "server-listen";
|
|
6683
|
+
let browser;
|
|
6684
|
+
let page;
|
|
6685
|
+
let pageErrors = [];
|
|
6686
|
+
let responseDiagnostics = [];
|
|
6687
|
+
const launchBrowser = (headless) => chromium.launch({
|
|
6688
|
+
channel: process.env.FORGEAX_CHROME_CHANNEL ?? "chrome",
|
|
6689
|
+
headless,
|
|
6690
|
+
args: [
|
|
6691
|
+
"--enable-unsafe-webgpu",
|
|
6692
|
+
"--enable-features=Vulkan,UseSkiaRenderer,SharedArrayBuffer",
|
|
6693
|
+
"--use-vulkan=swiftshader",
|
|
6694
|
+
"--use-angle=swiftshader",
|
|
6695
|
+
"--disable-vulkan-surface",
|
|
6696
|
+
"--ignore-gpu-blocklist",
|
|
6697
|
+
"--disable-gpu-driver-bug-workarounds",
|
|
6698
|
+
"--disable-dawn-features=disallow_unsafe_apis",
|
|
6699
|
+
"--autoplay-policy=no-user-gesture-required"
|
|
6700
|
+
]
|
|
6701
|
+
});
|
|
6702
|
+
const observePage = (target) => {
|
|
6703
|
+
target.on("pageerror", (error) => pageErrors.push(error.message));
|
|
6704
|
+
target.on("console", (message) => {
|
|
6705
|
+
if (message.type() === "error" || message.type() === "warning") {
|
|
6706
|
+
pageErrors.push(`${message.type()}: ${message.text()}`);
|
|
5784
6707
|
}
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
)
|
|
5796
|
-
);
|
|
5797
|
-
return;
|
|
5798
|
-
}
|
|
5799
|
-
if (body.descriptorDigest !== options.descriptorDigest) {
|
|
5800
|
-
reply(
|
|
5801
|
-
response,
|
|
5802
|
-
409,
|
|
5803
|
-
errorResult(
|
|
5804
|
-
"carrier-descriptor-mismatch",
|
|
5805
|
-
"the offered descriptor digest",
|
|
5806
|
-
"Rebuild the offer from the current descriptor before retrying."
|
|
5807
|
-
)
|
|
5808
|
-
);
|
|
5809
|
-
return;
|
|
5810
|
-
}
|
|
5811
|
-
if (body.recipeDigest !== options.recipeDigest) {
|
|
5812
|
-
reply(
|
|
5813
|
-
response,
|
|
5814
|
-
409,
|
|
5815
|
-
errorResult(
|
|
5816
|
-
"carrier-recipe-mismatch",
|
|
5817
|
-
"the offered recipe digest",
|
|
5818
|
-
"Serialize a fresh snapshot and request a new carrier before retrying."
|
|
5819
|
-
)
|
|
5820
|
-
);
|
|
5821
|
-
return;
|
|
5822
|
-
}
|
|
5823
|
-
const terminal = await options.execute({
|
|
5824
|
-
leaseId,
|
|
5825
|
-
descriptorDigest: options.descriptorDigest,
|
|
5826
|
-
recipeDigest: options.recipeDigest,
|
|
5827
|
-
args: body.args
|
|
5828
|
-
});
|
|
5829
|
-
reply(response, 200, { ok: true, value: terminal, state: "started" });
|
|
5830
|
-
return;
|
|
6708
|
+
});
|
|
6709
|
+
target.on("response", (response) => {
|
|
6710
|
+
if (response.status() >= 400) {
|
|
6711
|
+
responseDiagnostics.push(
|
|
6712
|
+
response.text().then((body) => {
|
|
6713
|
+
pageErrors.push(`HTTP ${response.status()}: ${response.url()} ${body}`);
|
|
6714
|
+
}).catch(() => {
|
|
6715
|
+
pageErrors.push(`HTTP ${response.status()}: ${response.url()}`);
|
|
6716
|
+
})
|
|
6717
|
+
);
|
|
5831
6718
|
}
|
|
5832
|
-
|
|
5833
|
-
response,
|
|
5834
|
-
404,
|
|
5835
|
-
errorResult(
|
|
5836
|
-
"carrier-provider-exit",
|
|
5837
|
-
"a carrier protocol route",
|
|
5838
|
-
"Request a fresh visible offer from the provider."
|
|
5839
|
-
)
|
|
5840
|
-
);
|
|
5841
|
-
} catch (cause) {
|
|
5842
|
-
reply(
|
|
5843
|
-
response,
|
|
5844
|
-
400,
|
|
5845
|
-
errorResult(
|
|
5846
|
-
"carrier-provider-exit",
|
|
5847
|
-
"a valid carrier request",
|
|
5848
|
-
"Serialize POD only and retry from the last safe snapshot.",
|
|
5849
|
-
{ cause: cause instanceof Error ? cause.message : String(cause) }
|
|
5850
|
-
)
|
|
5851
|
-
);
|
|
5852
|
-
}
|
|
5853
|
-
});
|
|
5854
|
-
await new Promise((resolve18, reject) => {
|
|
5855
|
-
const onError = (error) => {
|
|
5856
|
-
server.off("listening", onListening);
|
|
5857
|
-
reject(error);
|
|
5858
|
-
};
|
|
5859
|
-
const onListening = () => {
|
|
5860
|
-
server.off("error", onError);
|
|
5861
|
-
resolve18();
|
|
5862
|
-
};
|
|
5863
|
-
server.once("error", onError);
|
|
5864
|
-
server.once("listening", onListening);
|
|
5865
|
-
server.listen(0, host);
|
|
5866
|
-
});
|
|
5867
|
-
const address = server.address();
|
|
5868
|
-
if (address === null || typeof address === "string")
|
|
5869
|
-
throw new Error("carrier provider did not expose a loopback port");
|
|
5870
|
-
const endpoint = `http://${host}:${address.port}/carrier`;
|
|
5871
|
-
let closed = false;
|
|
5872
|
-
return {
|
|
5873
|
-
endpoint,
|
|
5874
|
-
offer: options.machine.offer,
|
|
5875
|
-
async close() {
|
|
5876
|
-
if (closed) return;
|
|
5877
|
-
closed = true;
|
|
5878
|
-
await new Promise(
|
|
5879
|
-
(resolve18, reject) => server.close((error) => error === void 0 ? resolve18() : reject(error))
|
|
5880
|
-
);
|
|
5881
|
-
}
|
|
6719
|
+
});
|
|
5882
6720
|
};
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
const result = machine.lease(request);
|
|
5889
|
-
if (!result.ok) return result;
|
|
5890
|
-
return { ok: true, value: { leaseId: result.value.leaseId }, state: result.state };
|
|
5891
|
-
},
|
|
5892
|
-
started: machine.started,
|
|
5893
|
-
exit: machine.exit
|
|
6721
|
+
const closeBrowserProcess = async () => {
|
|
6722
|
+
await page?.close().catch(() => void 0);
|
|
6723
|
+
page = void 0;
|
|
6724
|
+
await browser?.close().catch(() => void 0);
|
|
6725
|
+
browser = void 0;
|
|
5894
6726
|
};
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
projectId: options.projectId,
|
|
5899
|
-
consumerId: options.consumerId,
|
|
5900
|
-
endpoint: options.endpoint ?? "http://127.0.0.1:5740/carrier",
|
|
5901
|
-
now: options.now,
|
|
5902
|
-
ttlMs: options.ttlMs ?? 3e4
|
|
5903
|
-
});
|
|
5904
|
-
let lookupCount = 0;
|
|
5905
|
-
const lookup = () => {
|
|
5906
|
-
if (options.presentation !== "visible" || machine.snapshot().state !== "offered") return;
|
|
5907
|
-
lookupCount += 1;
|
|
5908
|
-
return machine.offer;
|
|
6727
|
+
const abort = () => {
|
|
6728
|
+
void closeBrowserProcess();
|
|
6729
|
+
void server.close();
|
|
5909
6730
|
};
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
6731
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
6732
|
+
try {
|
|
6733
|
+
if (signal.aborted) throw new Error("Browser Host aborted before launch");
|
|
6734
|
+
await server.listen();
|
|
6735
|
+
const address = server.httpServer?.address();
|
|
6736
|
+
if (address === null || address === void 0 || typeof address === "string") {
|
|
6737
|
+
throw new Error("Vite Browser Host did not expose a loopback TCP address");
|
|
5915
6738
|
}
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
init_catalog();
|
|
5921
|
-
function decorateResourcePreviewTerminal(terminal) {
|
|
5922
|
-
if (terminal.outcome === "succeeded") return terminal;
|
|
5923
|
-
const recovery = describeResourcePreviewFailure(terminal.failure);
|
|
5924
|
-
if (recovery === void 0) return terminal;
|
|
5925
|
-
if (terminal.failure.code !== "tool-domain-failed") return terminal;
|
|
5926
|
-
return {
|
|
5927
|
-
...terminal,
|
|
5928
|
-
failure: {
|
|
5929
|
-
...terminal.failure,
|
|
5930
|
-
detail: {
|
|
5931
|
-
...terminal.failure.detail,
|
|
5932
|
-
...recovery.suggestedOperation === void 0 ? {} : { suggestedOperation: recovery.suggestedOperation },
|
|
5933
|
-
recovery: recovery.actions
|
|
5934
|
-
}
|
|
6739
|
+
phase = "server-transform";
|
|
6740
|
+
const entryTransform = await server.transformRequest("/main.ts");
|
|
6741
|
+
if (entryTransform === null) {
|
|
6742
|
+
throw new Error("Vite Browser Host entry transform returned no module");
|
|
5935
6743
|
}
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
artifacts: []
|
|
5943
|
-
};
|
|
5944
|
-
}
|
|
5945
|
-
function runNamedTool(runtime, id, args) {
|
|
5946
|
-
const contribution = runtime.get(id);
|
|
5947
|
-
if (contribution === void 0) return Promise.resolve(missingTool(id));
|
|
5948
|
-
return runtime.run(contribution, args).terminal.then(decorateResourcePreviewTerminal);
|
|
5949
|
-
}
|
|
5950
|
-
function runGenericTool(runtime, id, encodedArgs) {
|
|
5951
|
-
let args;
|
|
5952
|
-
try {
|
|
5953
|
-
args = JSON.parse(encodedArgs);
|
|
5954
|
-
} catch {
|
|
5955
|
-
return Promise.resolve({
|
|
5956
|
-
outcome: "failed",
|
|
5957
|
-
failure: {
|
|
5958
|
-
code: "tool-invalid-args",
|
|
5959
|
-
expected: "generic CLI arguments to be valid JSON",
|
|
5960
|
-
hint: "Encode one JSON value that conforms to the descriptor argsSchema.",
|
|
5961
|
-
detail: { message: "Invalid JSON", value: null }
|
|
5962
|
-
},
|
|
5963
|
-
artifacts: []
|
|
6744
|
+
phase = "capture-browser-launch";
|
|
6745
|
+
browser = await launchBrowser(recipe.presentation === "hidden");
|
|
6746
|
+
phase = "capture-page-bootstrap";
|
|
6747
|
+
page = await browser.newPage({
|
|
6748
|
+
viewport: recipe.viewport,
|
|
6749
|
+
deviceScaleFactor: 1
|
|
5964
6750
|
});
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
list: () => listTools(loaded.value),
|
|
6006
|
-
describe: (id) => describeTool(loaded.value, id),
|
|
6007
|
-
async run(id, args, runOptions = {}) {
|
|
6008
|
-
if (id === "preview.run") return retiredPreviewTool();
|
|
6009
|
-
const contribution = runtime.get(id);
|
|
6010
|
-
if (contribution === void 0) return missingTool2(id);
|
|
6011
|
-
const binding = bindingById.get(id);
|
|
6012
|
-
const terminal = binding !== void 0 && runProjectTool2 !== void 0 ? await runProjectTool2(binding, args, runOptions) : realmDispatch === void 0 ? await runtime.run(contribution, args, runOptions).terminal : await realmDispatch.run(id, args, runOptions);
|
|
6013
|
-
return decorateResourcePreviewTerminal(terminal);
|
|
6014
|
-
}
|
|
6015
|
-
};
|
|
6016
|
-
}
|
|
6017
|
-
|
|
6018
|
-
// src/index.ts
|
|
6019
|
-
init_contributions();
|
|
6020
|
-
function runLibraryTool(contribution, args, options) {
|
|
6021
|
-
return createToolRuntime([contribution]).run(contribution, args, options).terminal;
|
|
6022
|
-
}
|
|
6023
|
-
var roster = [
|
|
6024
|
-
{
|
|
6025
|
-
operation: "project.preview",
|
|
6026
|
-
owner: "devkit/preview-host",
|
|
6027
|
-
realm: "host",
|
|
6028
|
-
artifacts: previewArtifacts(),
|
|
6029
|
-
benefit: "reuse the real WebGPU hidden preview recipe",
|
|
6030
|
-
fallback: "private"
|
|
6031
|
-
},
|
|
6032
|
-
{
|
|
6033
|
-
operation: "material.preview",
|
|
6034
|
-
owner: "engine-preview/material",
|
|
6035
|
-
realm: "host",
|
|
6036
|
-
artifacts: previewArtifacts(),
|
|
6037
|
-
benefit: "preview one material subject through the canonical lit rig",
|
|
6038
|
-
fallback: "private"
|
|
6039
|
-
},
|
|
6040
|
-
{
|
|
6041
|
-
operation: "mesh.preview",
|
|
6042
|
-
owner: "engine-preview/mesh",
|
|
6043
|
-
realm: "host",
|
|
6044
|
-
artifacts: previewArtifacts(),
|
|
6045
|
-
benefit: "preview one mesh subject with every submesh and AABB evidence",
|
|
6046
|
-
fallback: "private"
|
|
6047
|
-
},
|
|
6048
|
-
{
|
|
6049
|
-
operation: "vfx.preview",
|
|
6050
|
-
owner: "engine-preview/vfx",
|
|
6051
|
-
realm: "host",
|
|
6052
|
-
artifacts: previewArtifacts(),
|
|
6053
|
-
benefit: "preview one bounded VFX timeline with compute evidence",
|
|
6054
|
-
fallback: "private"
|
|
6055
|
-
},
|
|
6056
|
-
{
|
|
6057
|
-
operation: "texture.preview",
|
|
6058
|
-
owner: "engine-preview/texture",
|
|
6059
|
-
realm: "host",
|
|
6060
|
-
artifacts: previewArtifacts(),
|
|
6061
|
-
benefit: "preview one texture on the aspect-preserving unlit quad",
|
|
6062
|
-
fallback: "private"
|
|
6063
|
-
}
|
|
6064
|
-
];
|
|
6065
|
-
function previewArtifacts() {
|
|
6066
|
-
return [
|
|
6067
|
-
{
|
|
6068
|
-
owner: "preview-tool-proof",
|
|
6069
|
-
source: "M3",
|
|
6070
|
-
ref: {
|
|
6071
|
-
kind: "rhi-tape",
|
|
6072
|
-
digest: "sha256:e62a302dd29e302e1d0928306fe9c90c47ee336aa2e9e35fdc1d75586ae0018d",
|
|
6073
|
-
uri: "repo:apps/preview/__tests__/tool-proof.recipe.integration.test.ts"
|
|
6074
|
-
}
|
|
6075
|
-
},
|
|
6076
|
-
{
|
|
6077
|
-
owner: "preview-tool-proof",
|
|
6078
|
-
source: "M3",
|
|
6079
|
-
ref: {
|
|
6080
|
-
kind: "profile-capture",
|
|
6081
|
-
digest: "sha256:11ec9024e1ea08b38f0901db272847ac402d2dce0c757efe744315608e889c5c",
|
|
6082
|
-
uri: "repo:packages/profiler/src/__tests__/fixtures/profile-capture/model-input.json"
|
|
6751
|
+
observePage(page);
|
|
6752
|
+
const captureUrl = new URL(`http://127.0.0.1:${address.port}/`);
|
|
6753
|
+
captureUrl.searchParams.set("forgeax-tool-recipe", JSON.stringify(recipe));
|
|
6754
|
+
captureUrl.searchParams.set("forgeax-tool-snapshot", JSON.stringify(snapshot));
|
|
6755
|
+
captureUrl.searchParams.set("forgeax-tool-run-id", runId);
|
|
6756
|
+
if (resource !== void 0)
|
|
6757
|
+
captureUrl.searchParams.set("forgeax-resource-preview", JSON.stringify(resource));
|
|
6758
|
+
await page.goto(captureUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
|
|
6759
|
+
await page.waitForFunction(
|
|
6760
|
+
() => globalThis.__forgeaxToolHost?.ready === true,
|
|
6761
|
+
void 0,
|
|
6762
|
+
{ timeout: 45e3 }
|
|
6763
|
+
);
|
|
6764
|
+
phase = "capture-run";
|
|
6765
|
+
const captured = await page.evaluate(async () => {
|
|
6766
|
+
const host = globalThis.__forgeaxToolHost;
|
|
6767
|
+
const value2 = await host.capture();
|
|
6768
|
+
return JSON.parse(
|
|
6769
|
+
JSON.stringify(
|
|
6770
|
+
value2,
|
|
6771
|
+
(_key, nested) => nested instanceof Error ? {
|
|
6772
|
+
...nested,
|
|
6773
|
+
name: nested.name,
|
|
6774
|
+
message: nested.message,
|
|
6775
|
+
stack: nested.stack
|
|
6776
|
+
} : nested
|
|
6777
|
+
)
|
|
6778
|
+
);
|
|
6779
|
+
});
|
|
6780
|
+
await Promise.all(responseDiagnostics);
|
|
6781
|
+
if (!captured.ok) return browserFailure("capture-run", captured.error, pageErrors);
|
|
6782
|
+
if (pageErrors.length > 0)
|
|
6783
|
+
return browserFailure("capture-page-runtime", pageErrors[0], pageErrors);
|
|
6784
|
+
const capturePng = await page.screenshot({ type: "png" });
|
|
6785
|
+
const capturedResult = {
|
|
6786
|
+
...captured.result,
|
|
6787
|
+
capturePng: {
|
|
6788
|
+
uri: `data:image/png;base64,${capturePng.toString("base64")}`,
|
|
6789
|
+
width: recipe.viewport.width,
|
|
6790
|
+
height: recipe.viewport.height
|
|
6083
6791
|
}
|
|
6084
|
-
}
|
|
6085
|
-
];
|
|
6086
|
-
}
|
|
6087
|
-
function validEvidence(entry) {
|
|
6088
|
-
return entry.owner.length > 0 && entry.artifacts.length > 0 && entry.artifacts.every(
|
|
6089
|
-
({ owner, source, ref }) => owner.length > 0 && source === "M3" && /^sha256:[0-9a-f]{64}$/.test(ref.digest) && typeof ref.uri === "string" && ref.uri.startsWith("repo:") && isSupportedEvidenceKind(ref.kind)
|
|
6090
|
-
);
|
|
6091
|
-
}
|
|
6092
|
-
function isSupportedEvidenceKind(kind) {
|
|
6093
|
-
return kind === "rhi-tape" || kind === "profile-capture" || kind === "png";
|
|
6094
|
-
}
|
|
6095
|
-
function createMigrationRoster() {
|
|
6096
|
-
return roster.filter(validEvidence).map((entry) => ({
|
|
6097
|
-
...entry,
|
|
6098
|
-
artifacts: entry.artifacts.map((artifact) => ({ ...artifact, ref: { ...artifact.ref } }))
|
|
6099
|
-
}));
|
|
6100
|
-
}
|
|
6101
|
-
function resolveMigration(entries2, operation, target) {
|
|
6102
|
-
const entry = entries2.find((candidate) => candidate.operation === operation);
|
|
6103
|
-
if (entry === void 0 || !validEvidence(entry)) {
|
|
6104
|
-
return {
|
|
6105
|
-
ok: false,
|
|
6106
|
-
error: capabilityUnavailableError(`migration:${operation}`, target.realm)
|
|
6107
6792
|
};
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6793
|
+
await closeBrowserProcess();
|
|
6794
|
+
pageErrors = [];
|
|
6795
|
+
responseDiagnostics = [];
|
|
6796
|
+
phase = "replay-browser-launch";
|
|
6797
|
+
browser = await launchBrowser(true);
|
|
6798
|
+
phase = "replay-page-bootstrap";
|
|
6799
|
+
page = await browser.newPage({
|
|
6800
|
+
viewport: recipe.viewport,
|
|
6801
|
+
deviceScaleFactor: 1
|
|
6802
|
+
});
|
|
6803
|
+
observePage(page);
|
|
6804
|
+
const replayUrl = new URL(`http://127.0.0.1:${address.port}/`);
|
|
6805
|
+
replayUrl.searchParams.set("forgeax-tool-replay", "1");
|
|
6806
|
+
await page.goto(replayUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
|
|
6807
|
+
await page.waitForFunction(
|
|
6808
|
+
() => globalThis.__forgeaxToolReplayHost?.ready === true,
|
|
6809
|
+
void 0,
|
|
6810
|
+
{ timeout: 45e3 }
|
|
6811
|
+
);
|
|
6812
|
+
phase = "replay-run";
|
|
6813
|
+
const result = await page.evaluate(async (serializedCapture) => {
|
|
6814
|
+
const host = globalThis.__forgeaxToolReplayHost;
|
|
6815
|
+
const value2 = await host.run(JSON.parse(serializedCapture));
|
|
6816
|
+
return JSON.parse(
|
|
6817
|
+
JSON.stringify(
|
|
6818
|
+
value2,
|
|
6819
|
+
(_key, nested) => nested instanceof Error ? {
|
|
6820
|
+
...nested,
|
|
6821
|
+
name: nested.name,
|
|
6822
|
+
message: nested.message,
|
|
6823
|
+
stack: nested.stack
|
|
6824
|
+
} : nested
|
|
6825
|
+
)
|
|
6826
|
+
);
|
|
6827
|
+
}, JSON.stringify(capturedResult));
|
|
6828
|
+
await Promise.all(responseDiagnostics);
|
|
6829
|
+
if (!result.ok) return browserFailure("replay-run", result.error, pageErrors);
|
|
6830
|
+
if (pageErrors.length > 0)
|
|
6831
|
+
return browserFailure("replay-page-runtime", pageErrors[0], pageErrors);
|
|
6832
|
+
const endedAtMs = performance.now();
|
|
6833
|
+
const phases = result.result.operationTiming.phases;
|
|
6834
|
+
const observedDurationMs = phases === void 0 ? 0 : Object.values(phases).reduce(
|
|
6835
|
+
(total, observation) => total + (observation.status === "observed" ? observation.durationMs : 0),
|
|
6836
|
+
0
|
|
6837
|
+
);
|
|
6838
|
+
const durationMs = endedAtMs - hostStartedAtMs;
|
|
6839
|
+
const value = {
|
|
6840
|
+
...result.result,
|
|
6841
|
+
operationTiming: {
|
|
6842
|
+
...result.result.operationTiming,
|
|
6843
|
+
startedAtMs: hostStartedAtMs,
|
|
6844
|
+
endedAtMs,
|
|
6845
|
+
durationMs,
|
|
6846
|
+
...phases === void 0 ? { unattributedMs: durationMs } : {
|
|
6847
|
+
phases: {
|
|
6848
|
+
...phases,
|
|
6849
|
+
transport: {
|
|
6850
|
+
status: "observed",
|
|
6851
|
+
durationMs: Math.max(0, durationMs - observedDurationMs)
|
|
6852
|
+
}
|
|
6853
|
+
},
|
|
6854
|
+
unattributedMs: 0
|
|
6855
|
+
}
|
|
6856
|
+
},
|
|
6857
|
+
actualCarrier: recipe.presentation === "hidden" ? "headless-private" : "headed-private"
|
|
6858
|
+
};
|
|
6859
|
+
phase = "artifact-publish";
|
|
6149
6860
|
return {
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
...cause.stack === void 0 ? {} : { stack: cause.stack }
|
|
6861
|
+
ok: true,
|
|
6862
|
+
value: options.publish === false ? value : await publishPreviewArtifacts(projectRoot, runId, value)
|
|
6153
6863
|
};
|
|
6154
|
-
}
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6864
|
+
} catch (cause) {
|
|
6865
|
+
await Promise.all(responseDiagnostics);
|
|
6866
|
+
return browserFailure(phase, cause, pageErrors);
|
|
6867
|
+
} finally {
|
|
6868
|
+
signal.removeEventListener("abort", abort);
|
|
6869
|
+
await closeBrowserProcess();
|
|
6870
|
+
await server.close().catch(() => void 0);
|
|
6871
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
6160
6872
|
}
|
|
6161
6873
|
}
|
|
6162
|
-
function
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6874
|
+
function runBrowserResourcePreviewHost(projectRoot, recipe, snapshot, runId, signal, resource, options = {}) {
|
|
6875
|
+
return runBrowserPreviewHost(
|
|
6876
|
+
projectRoot,
|
|
6877
|
+
recipe,
|
|
6878
|
+
snapshot,
|
|
6879
|
+
runId,
|
|
6880
|
+
signal,
|
|
6881
|
+
"resource-bootstrap",
|
|
6882
|
+
resource,
|
|
6883
|
+
options
|
|
6884
|
+
);
|
|
6166
6885
|
}
|
|
6167
|
-
|
|
6168
|
-
|
|
6886
|
+
var init_browser_host = __esm({
|
|
6887
|
+
"src/tools/browser-host.ts"() {
|
|
6888
|
+
init_host();
|
|
6889
|
+
init_project();
|
|
6890
|
+
}
|
|
6891
|
+
});
|
|
6892
|
+
|
|
6893
|
+
// src/tools/native-preview.ts
|
|
6894
|
+
var native_preview_exports = {};
|
|
6895
|
+
__export(native_preview_exports, {
|
|
6896
|
+
isNativePreviewTool: () => isNativePreviewTool,
|
|
6897
|
+
runNativePreviewTool: () => runNativePreviewTool
|
|
6898
|
+
});
|
|
6899
|
+
function isNativePreviewTool(id) {
|
|
6900
|
+
return previewToolIds.has(id);
|
|
6169
6901
|
}
|
|
6170
|
-
async function
|
|
6171
|
-
const
|
|
6902
|
+
async function runNativePreviewTool(contribution, args, options, projectRoot) {
|
|
6903
|
+
const kind = contribution.descriptor.id.split(".")[0];
|
|
6904
|
+
if (kind !== "material" && kind !== "mesh" && kind !== "texture" && kind !== "vfx") {
|
|
6905
|
+
throw new Error(`unsupported native preview operation ${contribution.descriptor.id}`);
|
|
6906
|
+
}
|
|
6907
|
+
const parsed = contribution.descriptor.argsSchema.parse(args);
|
|
6908
|
+
if (!parsed.ok) {
|
|
6909
|
+
return {
|
|
6910
|
+
outcome: "failed",
|
|
6911
|
+
failure: {
|
|
6912
|
+
code: "tool-invalid-args",
|
|
6913
|
+
expected: "preview arguments to match the operation argsSchema",
|
|
6914
|
+
hint: "Pass { guid } and an optional power-of-two size from AssetRegistry catalog identity.",
|
|
6915
|
+
detail: { message: parsed.error, value: null }
|
|
6916
|
+
},
|
|
6917
|
+
artifacts: []
|
|
6918
|
+
};
|
|
6919
|
+
}
|
|
6920
|
+
const request = parsed.value;
|
|
6921
|
+
const { guid, size = RESOURCE_PREVIEW_DEFAULT_SIZE } = request;
|
|
6922
|
+
const selectedPreviewPlugin = nativePreviewPlugins.find(
|
|
6923
|
+
([, plugin]) => plugin.tools.some(
|
|
6924
|
+
(tool) => tool.descriptor.id === contribution.descriptor.id
|
|
6925
|
+
)
|
|
6926
|
+
);
|
|
6927
|
+
if (selectedPreviewPlugin === void 0) {
|
|
6928
|
+
throw new Error(`native preview plugin does not export ${contribution.descriptor.id}`);
|
|
6929
|
+
}
|
|
6930
|
+
const selectedPluginName = selectedPreviewPlugin[0];
|
|
6931
|
+
const snapshot = options.snapshot ?? { revision: 0, digest: `sha256:project:${projectRoot}` };
|
|
6932
|
+
const previewRunId = `${contribution.descriptor.id}:${crypto.randomUUID()}`;
|
|
6933
|
+
const browser = await runBrowserResourcePreviewHost(
|
|
6934
|
+
projectRoot,
|
|
6935
|
+
createToolPreviewRecipe({
|
|
6936
|
+
presentation: "hidden",
|
|
6937
|
+
viewport: { width: size, height: size },
|
|
6938
|
+
frames: 32
|
|
6939
|
+
}),
|
|
6940
|
+
snapshot,
|
|
6941
|
+
previewRunId,
|
|
6942
|
+
options.signal ?? new AbortController().signal,
|
|
6943
|
+
{ kind, guid, size },
|
|
6944
|
+
{ publish: false }
|
|
6945
|
+
);
|
|
6946
|
+
if (!browser.ok || browser.value.resource === void 0) {
|
|
6947
|
+
const failure2 = browser.ok ? {
|
|
6948
|
+
code: "tool-preview-bootstrap-failed",
|
|
6949
|
+
expected: "the Browser resource bootstrap to return AssetRegistry owner facts",
|
|
6950
|
+
hint: "Inspect resource-bootstrap and retry after the owner publishes the GUID payload.",
|
|
6951
|
+
detail: { phase: "resource-owner" }
|
|
6952
|
+
} : browser.error;
|
|
6953
|
+
return {
|
|
6954
|
+
outcome: "failed",
|
|
6955
|
+
failure: {
|
|
6956
|
+
code: "tool-domain-failed",
|
|
6957
|
+
expected: failure2.expected ?? "the Browser resource bootstrap to succeed",
|
|
6958
|
+
hint: failure2.hint ?? "Inspect the Browser resource bootstrap failure and retry.",
|
|
6959
|
+
detail: {
|
|
6960
|
+
code: failure2.code,
|
|
6961
|
+
...failure2.detail === void 0 ? {} : { payload: failure2.detail }
|
|
6962
|
+
}
|
|
6963
|
+
},
|
|
6964
|
+
artifacts: []
|
|
6965
|
+
};
|
|
6966
|
+
}
|
|
6967
|
+
const resource = browser.value.resource;
|
|
6968
|
+
const subjectDrawn = kind === "texture" ? browser.value.drawCalls > 0 : browser.value.drawCalls > 2;
|
|
6969
|
+
if (!subjectDrawn) {
|
|
6970
|
+
return {
|
|
6971
|
+
outcome: "failed",
|
|
6972
|
+
failure: {
|
|
6973
|
+
code: "tool-domain-failed",
|
|
6974
|
+
expected: "the captured resource frame to contain a subject draw in addition to canonical presentation passes",
|
|
6975
|
+
hint: "Inspect material readiness and the RHI tape before retrying the same GUID preview.",
|
|
6976
|
+
detail: {
|
|
6977
|
+
code: "tool-preview-subject-not-rendered",
|
|
6978
|
+
payload: { kind, drawCalls: browser.value.drawCalls }
|
|
6979
|
+
}
|
|
6980
|
+
},
|
|
6981
|
+
artifacts: []
|
|
6982
|
+
};
|
|
6983
|
+
}
|
|
6984
|
+
const ctx = new Context();
|
|
6172
6985
|
try {
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6986
|
+
const host = createNativePreviewHost({
|
|
6987
|
+
runId: `${contribution.descriptor.id}:native`,
|
|
6988
|
+
snapshot,
|
|
6989
|
+
projectRoot,
|
|
6990
|
+
backend: "webgpu",
|
|
6991
|
+
signal: options.signal ?? new AbortController().signal,
|
|
6992
|
+
assets: {
|
|
6993
|
+
loadByGuid: async () => ({
|
|
6994
|
+
ok: true,
|
|
6995
|
+
value: resource.asset,
|
|
6996
|
+
...resource.digest === void 0 ? {} : { digest: resource.digest },
|
|
6997
|
+
...resource.ownerFacts === void 0 ? {} : { ownerFacts: resource.ownerFacts }
|
|
6998
|
+
})
|
|
6999
|
+
},
|
|
7000
|
+
renderer: {
|
|
7001
|
+
rendererReady: browser.value.trace.events.includes("renderer-created"),
|
|
7002
|
+
worldReady: browser.value.trace.events.includes("world-updated"),
|
|
7003
|
+
drawCalls: browser.value.drawCalls,
|
|
7004
|
+
nonBlackPixels: browser.value.nonBlackPixels,
|
|
7005
|
+
...resource.observation === void 0 ? {} : { observation: resource.observation },
|
|
7006
|
+
...kind === "vfx" && resource.observation !== void 0 ? {
|
|
7007
|
+
vfx: {
|
|
7008
|
+
dispatches: typeof resource.observation.dispatches === "number" ? resource.observation.dispatches : 0,
|
|
7009
|
+
indirectDraws: typeof resource.observation.indirectDraws === "number" ? resource.observation.indirectDraws : 0,
|
|
7010
|
+
subjectOutputs: typeof resource.observation.subjectOutputs === "number" ? resource.observation.subjectOutputs : 0
|
|
7011
|
+
}
|
|
7012
|
+
} : {},
|
|
7013
|
+
texture: { drawCalls: browser.value.drawCalls }
|
|
7014
|
+
},
|
|
7015
|
+
artifacts: browser.value.artifacts
|
|
6185
7016
|
});
|
|
6186
|
-
const
|
|
6187
|
-
|
|
6188
|
-
|
|
7017
|
+
const { loader } = await installCatalogLoader(
|
|
7018
|
+
ctx,
|
|
7019
|
+
createNativePreviewCatalog(host, selectedPluginName),
|
|
7020
|
+
"host"
|
|
7021
|
+
);
|
|
7022
|
+
await loader.root.update(
|
|
7023
|
+
projectPluginEntries(
|
|
7024
|
+
previewPluginEntries.filter(({ name }) => name === selectedPluginName),
|
|
7025
|
+
"host",
|
|
7026
|
+
"host"
|
|
7027
|
+
)
|
|
7028
|
+
);
|
|
7029
|
+
await loader.await();
|
|
7030
|
+
const nativeContribution = nativePreviewTools.find(
|
|
7031
|
+
(candidate) => candidate.descriptor.id === contribution.descriptor.id
|
|
7032
|
+
);
|
|
7033
|
+
if (nativeContribution === void 0) {
|
|
7034
|
+
throw new Error(`native preview plugin does not export ${contribution.descriptor.id}`);
|
|
6189
7035
|
}
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
7036
|
+
const terminal = await createToolRuntime([nativeContribution]).run(nativeContribution, args, {
|
|
7037
|
+
...options,
|
|
7038
|
+
snapshot,
|
|
7039
|
+
capabilityResolver: createContextCapabilityResolver(ctx)
|
|
7040
|
+
}).terminal;
|
|
7041
|
+
if (terminal.outcome !== "succeeded") return terminal;
|
|
7042
|
+
const domainResult = terminal.result;
|
|
7043
|
+
let published;
|
|
7044
|
+
try {
|
|
7045
|
+
published = await publishPreviewArtifacts(projectRoot, previewRunId, browser.value, {
|
|
7046
|
+
snapshot,
|
|
7047
|
+
subject: domainResult.subject,
|
|
7048
|
+
presentation: domainResult.presentation,
|
|
7049
|
+
oracle: domainResult.oracle
|
|
7050
|
+
});
|
|
7051
|
+
} catch (cause) {
|
|
7052
|
+
return {
|
|
7053
|
+
outcome: "failed",
|
|
7054
|
+
failure: {
|
|
7055
|
+
code: "tool-domain-failed",
|
|
7056
|
+
expected: "the preview report and all capture artifacts to publish atomically",
|
|
7057
|
+
hint: "Inspect the artifact manifest identity or digest failure and retry the same ToolRun.",
|
|
7058
|
+
detail: {
|
|
7059
|
+
code: "tool-artifact-manifest-invalid",
|
|
7060
|
+
payload: cause instanceof Error ? cause.message : String(cause)
|
|
7061
|
+
}
|
|
7062
|
+
},
|
|
7063
|
+
artifacts: []
|
|
7064
|
+
};
|
|
7065
|
+
}
|
|
7066
|
+
const report = published.manifest.artifacts.find((artifact) => artifact.role === "report");
|
|
7067
|
+
if (report === void 0) {
|
|
7068
|
+
throw new Error("resource preview publisher returned no report artifact");
|
|
6194
7069
|
}
|
|
7070
|
+
return {
|
|
7071
|
+
...terminal,
|
|
7072
|
+
result: {
|
|
7073
|
+
...terminal.result,
|
|
7074
|
+
actualCarrier: browser.value.actualCarrier,
|
|
7075
|
+
report: {
|
|
7076
|
+
kind: "tool-result",
|
|
7077
|
+
digest: report.digest,
|
|
7078
|
+
uri: report.uri,
|
|
7079
|
+
mediaType: report.mediaType,
|
|
7080
|
+
sizeBytes: report.byteLength
|
|
7081
|
+
},
|
|
7082
|
+
artifacts: published.artifacts
|
|
7083
|
+
},
|
|
7084
|
+
artifacts: published.artifacts
|
|
7085
|
+
};
|
|
7086
|
+
} finally {
|
|
7087
|
+
await ctx.fiber.dispose();
|
|
6195
7088
|
}
|
|
6196
7089
|
}
|
|
6197
|
-
|
|
6198
|
-
|
|
7090
|
+
var previewPluginEntries, previewToolIds;
|
|
7091
|
+
var init_native_preview = __esm({
|
|
7092
|
+
"src/tools/native-preview.ts"() {
|
|
7093
|
+
init_browser_host();
|
|
7094
|
+
init_preview_catalog();
|
|
7095
|
+
previewPluginEntries = nativePreviewPlugins.map(([name]) => ({
|
|
7096
|
+
id: `forgeax-preview-${name.slice(name.lastIndexOf("/") + 1)}`,
|
|
7097
|
+
name,
|
|
7098
|
+
realm: "host"
|
|
7099
|
+
}));
|
|
7100
|
+
previewToolIds = new Set(nativePreviewTools.map(({ descriptor }) => descriptor.id));
|
|
7101
|
+
}
|
|
7102
|
+
});
|
|
7103
|
+
|
|
7104
|
+
// src/index.ts
|
|
7105
|
+
init_commands();
|
|
7106
|
+
init_dist();
|
|
7107
|
+
init_engine_binding();
|
|
7108
|
+
init_init();
|
|
7109
|
+
init_project();
|
|
7110
|
+
init_operations();
|
|
7111
|
+
init_single_html();
|
|
7112
|
+
init_software_capture();
|
|
7113
|
+
|
|
7114
|
+
// src/tools/benchmark/statistics.ts
|
|
7115
|
+
function calculateSampleStatistics(samples) {
|
|
7116
|
+
if (samples.length === 0) throw new RangeError("benchmark samples must not be empty");
|
|
7117
|
+
const sorted = [...samples].sort((left, right) => left - right);
|
|
7118
|
+
const medianIndex = (sorted.length - 1) / 2;
|
|
7119
|
+
const median = interpolate(sorted, medianIndex);
|
|
7120
|
+
const p95 = interpolate(sorted, Math.ceil(sorted.length * 0.95) - 1);
|
|
7121
|
+
const max = sorted[sorted.length - 1];
|
|
7122
|
+
if (max === void 0) throw new RangeError("benchmark samples must not be empty");
|
|
7123
|
+
return { count: sorted.length, median, p95, max };
|
|
6199
7124
|
}
|
|
6200
|
-
function
|
|
6201
|
-
const
|
|
7125
|
+
function interpolate(values, index) {
|
|
7126
|
+
const lower = Math.floor(index);
|
|
7127
|
+
const upper = Math.ceil(index);
|
|
7128
|
+
const lowerValue = values[lower];
|
|
7129
|
+
const upperValue = values[upper];
|
|
7130
|
+
if (lowerValue === void 0 || upperValue === void 0) {
|
|
7131
|
+
throw new RangeError("benchmark percentile index is out of range");
|
|
7132
|
+
}
|
|
7133
|
+
return lower === upper ? lowerValue : lowerValue + (upperValue - lowerValue) * (index - lower);
|
|
7134
|
+
}
|
|
7135
|
+
|
|
7136
|
+
// src/tools/benchmark/report.ts
|
|
7137
|
+
var DEFAULT_ADMISSION_THRESHOLDS = {
|
|
7138
|
+
sampleCountPerPhase: 30,
|
|
7139
|
+
medianImprovement: 0.2,
|
|
7140
|
+
p95Improvement: 0.1,
|
|
7141
|
+
maxRegression: 0.1,
|
|
7142
|
+
peakRssMultiplier: 1.25
|
|
7143
|
+
};
|
|
7144
|
+
function summarizeBenchmarkSamples(samples) {
|
|
7145
|
+
if (samples.length === 0) {
|
|
7146
|
+
return {
|
|
7147
|
+
samples: 0,
|
|
7148
|
+
durationMs: { count: 0, median: 0, p95: 0, max: 0 },
|
|
7149
|
+
peakRssBytes: { count: 0, median: 0, p95: 0, max: 0 },
|
|
7150
|
+
cleanupPassed: false,
|
|
7151
|
+
evictionPassed: false,
|
|
7152
|
+
exclusivePhasesMs: {}
|
|
7153
|
+
};
|
|
7154
|
+
}
|
|
7155
|
+
const durations = samples.map((sample) => sample.durationMs);
|
|
7156
|
+
const rss = samples.map((sample) => sample.peakRssBytes);
|
|
7157
|
+
const phaseNames = [
|
|
7158
|
+
...new Set(samples.flatMap((sample) => Object.keys(sample.exclusivePhasesMs)))
|
|
7159
|
+
];
|
|
7160
|
+
const exclusivePhasesMs = Object.fromEntries(
|
|
7161
|
+
phaseNames.map((phase) => [
|
|
7162
|
+
phase,
|
|
7163
|
+
calculateSampleStatistics(samples.map((sample) => sample.exclusivePhasesMs[phase] ?? 0))
|
|
7164
|
+
])
|
|
7165
|
+
);
|
|
6202
7166
|
return {
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
7167
|
+
samples: samples.length,
|
|
7168
|
+
durationMs: calculateSampleStatistics(durations),
|
|
7169
|
+
peakRssBytes: calculateSampleStatistics(rss),
|
|
7170
|
+
cleanupPassed: samples.every((sample) => sample.cleanupPassed),
|
|
7171
|
+
evictionPassed: samples.every((sample) => sample.evictionPassed),
|
|
7172
|
+
exclusivePhasesMs
|
|
6208
7173
|
};
|
|
6209
7174
|
}
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
7175
|
+
function createAdmissionReport(recipe, samples, thresholds = DEFAULT_ADMISSION_THRESHOLDS) {
|
|
7176
|
+
const privateSamples = samples.filter((sample) => sample.mode === "private");
|
|
7177
|
+
const serviceSamples = samples.filter((sample) => sample.mode === "service");
|
|
7178
|
+
const order = samples.map((sample) => sample.mode);
|
|
7179
|
+
const privateReport = summarizeBenchmarkSamples(privateSamples);
|
|
7180
|
+
const serviceReport = summarizeBenchmarkSamples(serviceSamples);
|
|
7181
|
+
const reasons = [];
|
|
7182
|
+
const expectedSamples = thresholds.sampleCountPerPhase * 2;
|
|
7183
|
+
const complete = privateSamples.length === expectedSamples && serviceSamples.length === expectedSamples;
|
|
7184
|
+
if (!complete) {
|
|
7185
|
+
reasons.push("sample count is incomplete");
|
|
6215
7186
|
}
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
"
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
7187
|
+
if (samples.some((sample) => sample.recipeDigest !== recipe.digest)) {
|
|
7188
|
+
reasons.push("recipe identity drifted");
|
|
7189
|
+
}
|
|
7190
|
+
if (!privateReport.cleanupPassed || !serviceReport.cleanupPassed) reasons.push("cleanup failed");
|
|
7191
|
+
if (!privateReport.evictionPassed || !serviceReport.evictionPassed)
|
|
7192
|
+
reasons.push("eviction failed");
|
|
7193
|
+
if (order.some((mode, index) => index > 0 && mode === order[index - 1])) {
|
|
7194
|
+
reasons.push("private and service samples were not alternated");
|
|
7195
|
+
}
|
|
7196
|
+
const medianLimit = privateReport.durationMs.median * (1 - thresholds.medianImprovement);
|
|
7197
|
+
const p95Limit = privateReport.durationMs.p95 * (1 - thresholds.p95Improvement);
|
|
7198
|
+
const maxLimit = privateReport.durationMs.max * (1 + thresholds.maxRegression);
|
|
7199
|
+
const rssLimit = privateReport.peakRssBytes.median * thresholds.peakRssMultiplier;
|
|
7200
|
+
if (complete && serviceReport.durationMs.median > medianLimit)
|
|
7201
|
+
reasons.push("median improvement threshold missed");
|
|
7202
|
+
if (complete && serviceReport.durationMs.p95 > p95Limit)
|
|
7203
|
+
reasons.push("p95 improvement threshold missed");
|
|
7204
|
+
if (complete && serviceReport.durationMs.max > maxLimit)
|
|
7205
|
+
reasons.push("max regression threshold exceeded");
|
|
7206
|
+
if (complete && serviceReport.peakRssBytes.median > rssLimit)
|
|
7207
|
+
reasons.push("peak RSS threshold exceeded");
|
|
7208
|
+
return {
|
|
7209
|
+
schema: "forgeax.tool-service-admission.v1",
|
|
7210
|
+
recipe,
|
|
7211
|
+
thresholds,
|
|
7212
|
+
order,
|
|
7213
|
+
private: privateReport,
|
|
7214
|
+
service: serviceReport,
|
|
7215
|
+
valid: complete && !reasons.some(
|
|
7216
|
+
(reason) => [
|
|
7217
|
+
"recipe identity drifted",
|
|
7218
|
+
"cleanup failed",
|
|
7219
|
+
"eviction failed",
|
|
7220
|
+
"private and service samples were not alternated"
|
|
7221
|
+
].includes(reason)
|
|
7222
|
+
),
|
|
7223
|
+
admitted: reasons.length === 0,
|
|
7224
|
+
reasons
|
|
6251
7225
|
};
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
7226
|
+
}
|
|
7227
|
+
|
|
7228
|
+
// src/tools/benchmark/harness.ts
|
|
7229
|
+
async function runBenchmarkAdmission(options) {
|
|
7230
|
+
const sampleCount = options.thresholds?.sampleCountPerPhase ?? 30;
|
|
7231
|
+
const samples = [];
|
|
7232
|
+
const phases = ["cold", "warm"];
|
|
7233
|
+
const modes = ["private", "service"];
|
|
7234
|
+
for (const phase of phases) {
|
|
7235
|
+
for (let index = 0; index < sampleCount; index += 1) {
|
|
7236
|
+
for (const mode of modes) {
|
|
7237
|
+
const measurement = await options.measure(mode, phase, index);
|
|
7238
|
+
samples.push({
|
|
7239
|
+
mode,
|
|
7240
|
+
phase,
|
|
7241
|
+
durationMs: measurement.durationMs,
|
|
7242
|
+
peakRssBytes: measurement.peakRssBytes,
|
|
7243
|
+
exclusivePhasesMs: measurement.exclusivePhasesMs ?? {},
|
|
7244
|
+
rhiDebugOverheadMs: measurement.rhiDebugOverheadMs ?? 0,
|
|
7245
|
+
carrierRendezvousMs: measurement.carrierRendezvousMs ?? 0,
|
|
7246
|
+
cleanupPassed: measurement.cleanupPassed,
|
|
7247
|
+
evictionPassed: measurement.evictionPassed,
|
|
7248
|
+
recipeDigest: options.recipe.digest
|
|
7249
|
+
});
|
|
7250
|
+
}
|
|
6256
7251
|
}
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
7252
|
+
}
|
|
7253
|
+
return createAdmissionReport(options.recipe, samples, options.thresholds);
|
|
7254
|
+
}
|
|
7255
|
+
async function bootstrapRealm(ctx, input) {
|
|
7256
|
+
const cloneSafe = validateRealmBootstrapPayload(input.payload);
|
|
7257
|
+
if (!cloneSafe.ok) return cloneSafe;
|
|
7258
|
+
if (!input.supportedRealms.includes(input.realm)) {
|
|
7259
|
+
return {
|
|
7260
|
+
ok: false,
|
|
7261
|
+
error: {
|
|
7262
|
+
code: "realm-capability-unavailable",
|
|
7263
|
+
realm: input.realm,
|
|
7264
|
+
supportedRealms: input.supportedRealms
|
|
7265
|
+
}
|
|
7266
|
+
};
|
|
7267
|
+
}
|
|
7268
|
+
const entries2 = projectPluginEntries(input.entries, input.realm, input.realm);
|
|
7269
|
+
if (input.realm === "build") {
|
|
7270
|
+
if (input.lifecycle === void 0) {
|
|
7271
|
+
return {
|
|
7272
|
+
ok: false,
|
|
7273
|
+
error: {
|
|
7274
|
+
code: "realm-lifecycle-adapter-missing",
|
|
7275
|
+
realm: input.realm,
|
|
7276
|
+
hint: "Inject a build lifecycle adapter that owns execution and stop cleanup."
|
|
7277
|
+
}
|
|
7278
|
+
};
|
|
6262
7279
|
}
|
|
7280
|
+
const lifecycle = await input.lifecycle.start({
|
|
7281
|
+
realm: input.realm,
|
|
7282
|
+
entries: entries2,
|
|
7283
|
+
catalog: input.catalog,
|
|
7284
|
+
catalogDigest: input.catalogDigest
|
|
7285
|
+
});
|
|
6263
7286
|
return {
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
digest,
|
|
6267
|
-
byteLength: bytes.byteLength,
|
|
6268
|
-
mediaType: mediaTypeByRole[role]
|
|
7287
|
+
ok: true,
|
|
7288
|
+
value: { realm: input.realm, catalogDigest: input.catalogDigest, entries: entries2, lifecycle }
|
|
6269
7289
|
};
|
|
6270
|
-
});
|
|
6271
|
-
if (requiredRoles.some((role) => !nonReportArtifacts.some((artifact) => artifact.role === role))) {
|
|
6272
|
-
throw new Error(
|
|
6273
|
-
"preview publication is missing capture, fresh-replay, tape, or profile artifact"
|
|
6274
|
-
);
|
|
6275
7290
|
}
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
7291
|
+
if (ctx === void 0) {
|
|
7292
|
+
return {
|
|
7293
|
+
ok: false,
|
|
7294
|
+
error: { code: "realm-context-missing", realm: input.realm }
|
|
7295
|
+
};
|
|
7296
|
+
}
|
|
7297
|
+
const loaded = await bootstrapCatalogLoader(ctx, input.catalog, input.realm, {
|
|
7298
|
+
catalogDigest: input.catalogDigest,
|
|
7299
|
+
supportedRealms: input.supportedRealms
|
|
6280
7300
|
});
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
7301
|
+
if (!loaded.ok) return loaded;
|
|
7302
|
+
return {
|
|
7303
|
+
ok: true,
|
|
7304
|
+
value: {
|
|
7305
|
+
realm: input.realm,
|
|
7306
|
+
catalogDigest: input.catalogDigest,
|
|
7307
|
+
entries: entries2,
|
|
7308
|
+
loader: loaded.value
|
|
7309
|
+
}
|
|
7310
|
+
};
|
|
7311
|
+
}
|
|
7312
|
+
|
|
7313
|
+
// src/tools/cache.ts
|
|
7314
|
+
function createServiceCache() {
|
|
7315
|
+
const entries2 = /* @__PURE__ */ new Map();
|
|
7316
|
+
return {
|
|
7317
|
+
get: (key) => entries2.get(key),
|
|
7318
|
+
set: (key, entry) => entries2.set(key, entry),
|
|
7319
|
+
evict: (key) => entries2.delete(key),
|
|
7320
|
+
clear: () => entries2.clear(),
|
|
7321
|
+
size: () => entries2.size
|
|
7322
|
+
};
|
|
7323
|
+
}
|
|
7324
|
+
function errorResult2(code, expected, hint, detail = {}) {
|
|
7325
|
+
return { ok: false, error: { code, expected, hint, detail } };
|
|
7326
|
+
}
|
|
7327
|
+
async function readBody(request) {
|
|
7328
|
+
const chunks = [];
|
|
7329
|
+
for await (const chunk of request) chunks.push(Buffer.from(chunk));
|
|
7330
|
+
const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
7331
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
7332
|
+
throw new Error("carrier request must be an object");
|
|
7333
|
+
return value;
|
|
7334
|
+
}
|
|
7335
|
+
function reply(response, status, payload) {
|
|
7336
|
+
response.statusCode = status;
|
|
7337
|
+
response.setHeader("content-type", "application/json");
|
|
7338
|
+
response.end(JSON.stringify(payload));
|
|
7339
|
+
}
|
|
7340
|
+
async function createCarrierProviderService(options) {
|
|
7341
|
+
const host = options.host ?? "127.0.0.1";
|
|
7342
|
+
const bearerToken = options.machine.offer.bearerToken;
|
|
7343
|
+
const server = createServer(async (request, response) => {
|
|
7344
|
+
if (request.method !== "POST" || !request.url?.startsWith("/carrier/")) {
|
|
7345
|
+
reply(
|
|
7346
|
+
response,
|
|
7347
|
+
404,
|
|
7348
|
+
errorResult2(
|
|
7349
|
+
"carrier-provider-exit",
|
|
7350
|
+
"a carrier protocol route",
|
|
7351
|
+
"Request a fresh visible offer from the provider."
|
|
7352
|
+
)
|
|
7353
|
+
);
|
|
7354
|
+
return;
|
|
7355
|
+
}
|
|
7356
|
+
if (request.headers.authorization !== `Bearer ${bearerToken}`) {
|
|
7357
|
+
reply(
|
|
7358
|
+
response,
|
|
7359
|
+
401,
|
|
7360
|
+
errorResult2(
|
|
7361
|
+
"carrier-token-invalid",
|
|
7362
|
+
"the ephemeral bearer token",
|
|
7363
|
+
"Request a fresh authenticated offer; never guess or persist bearer tokens."
|
|
7364
|
+
)
|
|
7365
|
+
);
|
|
7366
|
+
return;
|
|
7367
|
+
}
|
|
7368
|
+
try {
|
|
7369
|
+
const body = await readBody(request);
|
|
7370
|
+
const route = request.url.slice("/carrier/".length);
|
|
7371
|
+
if (route === "lease") {
|
|
7372
|
+
const result = options.machine.lease({
|
|
7373
|
+
consumerId: String(body.consumerId ?? ""),
|
|
7374
|
+
bearerToken,
|
|
7375
|
+
now: Number(body.now),
|
|
7376
|
+
descriptorDigest: typeof body.descriptorDigest === "string" ? body.descriptorDigest : void 0,
|
|
7377
|
+
recipeDigest: typeof body.recipeDigest === "string" ? body.recipeDigest : void 0
|
|
7378
|
+
});
|
|
7379
|
+
reply(response, result.ok ? 200 : 409, result.ok ? result : result);
|
|
7380
|
+
return;
|
|
7381
|
+
}
|
|
7382
|
+
const leaseId = typeof body.leaseId === "string" ? body.leaseId : "";
|
|
7383
|
+
if (route === "start") {
|
|
7384
|
+
const result = options.machine.started(leaseId);
|
|
7385
|
+
reply(response, result.ok ? 200 : 409, result.ok ? result : result);
|
|
7386
|
+
return;
|
|
7387
|
+
}
|
|
7388
|
+
if (route === "exit") {
|
|
7389
|
+
const result = options.machine.exit(leaseId);
|
|
7390
|
+
reply(response, result.ok ? 200 : 409, result.ok ? result : result);
|
|
7391
|
+
return;
|
|
7392
|
+
}
|
|
7393
|
+
if (route === "execute") {
|
|
7394
|
+
const snapshot = options.machine.snapshot();
|
|
7395
|
+
if (snapshot.leaseId !== leaseId || snapshot.state !== "started") {
|
|
7396
|
+
reply(
|
|
7397
|
+
response,
|
|
7398
|
+
409,
|
|
7399
|
+
errorResult2(
|
|
7400
|
+
snapshot.state === "exited" ? "carrier-exited" : "carrier-lease-required",
|
|
7401
|
+
"a started carrier lease",
|
|
7402
|
+
"Do not fallback after started; report the structured terminal error and offer a new carrier."
|
|
7403
|
+
)
|
|
7404
|
+
);
|
|
7405
|
+
return;
|
|
7406
|
+
}
|
|
7407
|
+
if (body.descriptorDigest !== options.descriptorDigest) {
|
|
7408
|
+
reply(
|
|
7409
|
+
response,
|
|
7410
|
+
409,
|
|
7411
|
+
errorResult2(
|
|
7412
|
+
"carrier-descriptor-mismatch",
|
|
7413
|
+
"the offered descriptor digest",
|
|
7414
|
+
"Rebuild the offer from the current descriptor before retrying."
|
|
7415
|
+
)
|
|
7416
|
+
);
|
|
7417
|
+
return;
|
|
7418
|
+
}
|
|
7419
|
+
if (body.recipeDigest !== options.recipeDigest) {
|
|
7420
|
+
reply(
|
|
7421
|
+
response,
|
|
7422
|
+
409,
|
|
7423
|
+
errorResult2(
|
|
7424
|
+
"carrier-recipe-mismatch",
|
|
7425
|
+
"the offered recipe digest",
|
|
7426
|
+
"Serialize a fresh snapshot and request a new carrier before retrying."
|
|
7427
|
+
)
|
|
7428
|
+
);
|
|
7429
|
+
return;
|
|
7430
|
+
}
|
|
7431
|
+
const terminal = await options.execute({
|
|
7432
|
+
leaseId,
|
|
7433
|
+
descriptorDigest: options.descriptorDigest,
|
|
7434
|
+
recipeDigest: options.recipeDigest,
|
|
7435
|
+
args: body.args
|
|
7436
|
+
});
|
|
7437
|
+
reply(response, 200, { ok: true, value: terminal, state: "started" });
|
|
7438
|
+
return;
|
|
7439
|
+
}
|
|
7440
|
+
reply(
|
|
7441
|
+
response,
|
|
7442
|
+
404,
|
|
7443
|
+
errorResult2(
|
|
7444
|
+
"carrier-provider-exit",
|
|
7445
|
+
"a carrier protocol route",
|
|
7446
|
+
"Request a fresh visible offer from the provider."
|
|
7447
|
+
)
|
|
7448
|
+
);
|
|
7449
|
+
} catch (cause) {
|
|
7450
|
+
reply(
|
|
7451
|
+
response,
|
|
7452
|
+
400,
|
|
7453
|
+
errorResult2(
|
|
7454
|
+
"carrier-provider-exit",
|
|
7455
|
+
"a valid carrier request",
|
|
7456
|
+
"Serialize POD only and retry from the last safe snapshot.",
|
|
7457
|
+
{ cause: cause instanceof Error ? cause.message : String(cause) }
|
|
7458
|
+
)
|
|
7459
|
+
);
|
|
7460
|
+
}
|
|
7461
|
+
});
|
|
7462
|
+
await new Promise((resolve19, reject) => {
|
|
7463
|
+
const onError = (error) => {
|
|
7464
|
+
server.off("listening", onListening);
|
|
7465
|
+
reject(error);
|
|
7466
|
+
};
|
|
7467
|
+
const onListening = () => {
|
|
7468
|
+
server.off("error", onError);
|
|
7469
|
+
resolve19();
|
|
7470
|
+
};
|
|
7471
|
+
server.once("error", onError);
|
|
7472
|
+
server.once("listening", onListening);
|
|
7473
|
+
server.listen(0, host);
|
|
7474
|
+
});
|
|
7475
|
+
const address = server.address();
|
|
7476
|
+
if (address === null || typeof address === "string")
|
|
7477
|
+
throw new Error("carrier provider did not expose a loopback port");
|
|
7478
|
+
const endpoint = `http://${host}:${address.port}/carrier`;
|
|
7479
|
+
let closed = false;
|
|
7480
|
+
return {
|
|
7481
|
+
endpoint,
|
|
7482
|
+
offer: options.machine.offer,
|
|
7483
|
+
async close() {
|
|
7484
|
+
if (closed) return;
|
|
7485
|
+
closed = true;
|
|
7486
|
+
await new Promise(
|
|
7487
|
+
(resolve19, reject) => server.close((error) => error === void 0 ? resolve19() : reject(error))
|
|
7488
|
+
);
|
|
7489
|
+
}
|
|
6295
7490
|
};
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
writeFile(paths.tapeJson, tapeJson),
|
|
6299
|
-
writeFile(paths.tapeBlob, tapeBlob),
|
|
6300
|
-
writeFile(paths.capturePng, capturePng),
|
|
6301
|
-
writeFile(paths.freshReplayPng, freshReplayPng),
|
|
6302
|
-
writeFile(paths.profile, profile),
|
|
6303
|
-
...reportBytes === void 0 ? [] : [writeFile(paths.report, reportBytes)]
|
|
6304
|
-
]);
|
|
6305
|
-
await writeFile(paths.manifest, `${JSON.stringify(manifest, null, 2)}
|
|
6306
|
-
`);
|
|
6307
|
-
await rename(staging, published);
|
|
6308
|
-
} catch (cause) {
|
|
6309
|
-
await rm(staging, { recursive: true, force: true });
|
|
6310
|
-
throw cause;
|
|
6311
|
-
}
|
|
7491
|
+
}
|
|
7492
|
+
function createCarrierProvider(machine) {
|
|
6312
7493
|
return {
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
7494
|
+
offer: machine.offer,
|
|
7495
|
+
accept(request) {
|
|
7496
|
+
const result = machine.lease(request);
|
|
7497
|
+
if (!result.ok) return result;
|
|
7498
|
+
return { ok: true, value: { leaseId: result.value.leaseId }, state: result.state };
|
|
6318
7499
|
},
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
png: { ...result.png, uri: projectUri(projectRoot, finalPaths.freshReplayPng) },
|
|
6322
|
-
manifest,
|
|
6323
|
-
artifacts: manifest.artifacts.map(artifactRef)
|
|
7500
|
+
started: machine.started,
|
|
7501
|
+
exit: machine.exit
|
|
6324
7502
|
};
|
|
6325
7503
|
}
|
|
6326
|
-
function
|
|
7504
|
+
function createCarrierRendezvous(options) {
|
|
7505
|
+
const machine = createCarrierStateMachine({
|
|
7506
|
+
projectId: options.projectId,
|
|
7507
|
+
consumerId: options.consumerId,
|
|
7508
|
+
endpoint: options.endpoint ?? "http://127.0.0.1:5740/carrier",
|
|
7509
|
+
now: options.now,
|
|
7510
|
+
ttlMs: options.ttlMs ?? 3e4
|
|
7511
|
+
});
|
|
7512
|
+
let lookupCount = 0;
|
|
7513
|
+
const lookup = () => {
|
|
7514
|
+
if (options.presentation !== "visible" || machine.snapshot().state !== "offered") return;
|
|
7515
|
+
lookupCount += 1;
|
|
7516
|
+
return machine.offer;
|
|
7517
|
+
};
|
|
6327
7518
|
return {
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
7519
|
+
...machine,
|
|
7520
|
+
lookup,
|
|
7521
|
+
get lookupCount() {
|
|
7522
|
+
return lookupCount;
|
|
7523
|
+
}
|
|
7524
|
+
};
|
|
7525
|
+
}
|
|
7526
|
+
|
|
7527
|
+
// src/index.ts
|
|
7528
|
+
init_catalog();
|
|
7529
|
+
function decorateResourcePreviewTerminal(terminal) {
|
|
7530
|
+
if (terminal.outcome === "succeeded") return terminal;
|
|
7531
|
+
const recovery = describeResourcePreviewFailure(terminal.failure);
|
|
7532
|
+
if (recovery === void 0) return terminal;
|
|
7533
|
+
if (terminal.failure.code !== "tool-domain-failed") return terminal;
|
|
7534
|
+
return {
|
|
7535
|
+
...terminal,
|
|
7536
|
+
failure: {
|
|
7537
|
+
...terminal.failure,
|
|
6333
7538
|
detail: {
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
7539
|
+
...terminal.failure.detail,
|
|
7540
|
+
...recovery.suggestedOperation === void 0 ? {} : { suggestedOperation: recovery.suggestedOperation },
|
|
7541
|
+
recovery: recovery.actions
|
|
6337
7542
|
}
|
|
6338
7543
|
}
|
|
6339
7544
|
};
|
|
6340
7545
|
}
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
7546
|
+
function missingTool(id) {
|
|
7547
|
+
return {
|
|
7548
|
+
outcome: "failed",
|
|
7549
|
+
failure: capabilityUnavailableError(`tool:${id}`, "build"),
|
|
7550
|
+
artifacts: []
|
|
7551
|
+
};
|
|
7552
|
+
}
|
|
7553
|
+
function runNamedTool(runtime, id, args) {
|
|
7554
|
+
const contribution = runtime.get(id);
|
|
7555
|
+
if (contribution === void 0) return Promise.resolve(missingTool(id));
|
|
7556
|
+
return runtime.run(contribution, args).terminal.then(decorateResourcePreviewTerminal);
|
|
7557
|
+
}
|
|
7558
|
+
function runGenericTool(runtime, id, encodedArgs) {
|
|
7559
|
+
let args;
|
|
6348
7560
|
try {
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
force: true
|
|
7561
|
+
args = JSON.parse(encodedArgs);
|
|
7562
|
+
} catch {
|
|
7563
|
+
return Promise.resolve({
|
|
7564
|
+
outcome: "failed",
|
|
7565
|
+
failure: {
|
|
7566
|
+
code: "tool-invalid-args",
|
|
7567
|
+
expected: "generic CLI arguments to be valid JSON",
|
|
7568
|
+
hint: "Encode one JSON value that conforms to the descriptor argsSchema.",
|
|
7569
|
+
detail: { message: "Invalid JSON", value: null }
|
|
6359
7570
|
},
|
|
6360
|
-
|
|
6361
|
-
...config.server,
|
|
6362
|
-
host: "127.0.0.1",
|
|
6363
|
-
port,
|
|
6364
|
-
// Vite's port 0 means its default 5173. Bind the probed port exactly so
|
|
6365
|
-
// another project or runner cannot be mistaken for this Browser Host.
|
|
6366
|
-
strictPort: true
|
|
6367
|
-
}
|
|
7571
|
+
artifacts: []
|
|
6368
7572
|
});
|
|
6369
|
-
} catch (cause) {
|
|
6370
|
-
await rm(cacheDir, { recursive: true, force: true });
|
|
6371
|
-
throw cause;
|
|
6372
7573
|
}
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
"
|
|
6384
|
-
|
|
6385
|
-
"
|
|
6386
|
-
"
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
const
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
7574
|
+
return runNamedTool(runtime, id, args);
|
|
7575
|
+
}
|
|
7576
|
+
|
|
7577
|
+
// src/tools/client.ts
|
|
7578
|
+
init_catalog();
|
|
7579
|
+
init_preview_migration();
|
|
7580
|
+
function missingTool2(id) {
|
|
7581
|
+
return {
|
|
7582
|
+
outcome: "failed",
|
|
7583
|
+
failure: {
|
|
7584
|
+
code: "tool-capability-unavailable",
|
|
7585
|
+
expected: `tool ${id} to exist in the project-derived catalog`,
|
|
7586
|
+
hint: "Run tool list and choose one of the discovered operation ids.",
|
|
7587
|
+
detail: { capability: `tool:${id}`, realm: "build" }
|
|
7588
|
+
},
|
|
7589
|
+
artifacts: []
|
|
7590
|
+
};
|
|
7591
|
+
}
|
|
7592
|
+
async function createToolClient(options) {
|
|
7593
|
+
const projectDiscovery = options.projectDiscovery;
|
|
7594
|
+
const runProjectTool2 = projectDiscovery === void 0 ? (await Promise.resolve().then(() => (init_project_tools(), project_tools_exports))).runProjectTool : void 0;
|
|
7595
|
+
const builtins = options.baseContributions ?? (await Promise.resolve().then(() => (init_contributions(), contributions_exports))).createDefaultContributions(options.projectRoot);
|
|
7596
|
+
const project = projectDiscovery === void 0 ? await (await Promise.resolve().then(() => (init_project_tools(), project_tools_exports))).discoverProjectTools(
|
|
7597
|
+
options.projectRoot,
|
|
7598
|
+
options
|
|
7599
|
+
) : await projectDiscovery(options.projectRoot);
|
|
7600
|
+
const contributions = [...builtins, ...project.map(({ contribution }) => contribution)];
|
|
7601
|
+
const runtime = options.baseContributions === void 0 ? (await Promise.resolve().then(() => (init_runtime(), runtime_exports))).createDevkitToolRuntime(contributions) : createToolRuntime(contributions);
|
|
7602
|
+
const realmDispatch = options.realmOwners === void 0 ? void 0 : createRealmDispatch(contributions, options.realmOwners);
|
|
7603
|
+
const bindingById = new Map(
|
|
7604
|
+
project.map((binding) => [binding.contribution.descriptor.id, binding])
|
|
7605
|
+
);
|
|
7606
|
+
const descriptors = runtime.list();
|
|
7607
|
+
const loaded = await loadToolCatalog(
|
|
7608
|
+
createProjectToolCatalogAuthority(options.projectRoot, descriptors),
|
|
7609
|
+
descriptors
|
|
7610
|
+
);
|
|
7611
|
+
if (!loaded.ok) throw loaded.error;
|
|
7612
|
+
return {
|
|
7613
|
+
list: () => listTools(loaded.value),
|
|
7614
|
+
describe: (id) => describeTool(loaded.value, id),
|
|
7615
|
+
async run(id, args, runOptions = {}) {
|
|
7616
|
+
if (id === "preview.run") return retiredPreviewTool();
|
|
7617
|
+
const contribution = runtime.get(id);
|
|
7618
|
+
if (contribution === void 0) return missingTool2(id);
|
|
7619
|
+
const binding = bindingById.get(id);
|
|
7620
|
+
const nativePreview = binding === void 0 && id.endsWith(".preview") ? await Promise.resolve().then(() => (init_native_preview(), native_preview_exports)) : void 0;
|
|
7621
|
+
const terminal = nativePreview?.isNativePreviewTool(id) === true ? await nativePreview.runNativePreviewTool(
|
|
7622
|
+
contribution,
|
|
7623
|
+
args,
|
|
7624
|
+
runOptions,
|
|
7625
|
+
options.projectRoot
|
|
7626
|
+
) : binding !== void 0 && runProjectTool2 !== void 0 ? await runProjectTool2(binding, args, runOptions) : realmDispatch === void 0 ? await runtime.run(contribution, args, runOptions).terminal : await realmDispatch.run(id, args, runOptions);
|
|
7627
|
+
return decorateResourcePreviewTerminal(terminal);
|
|
7628
|
+
}
|
|
7629
|
+
};
|
|
7630
|
+
}
|
|
7631
|
+
|
|
7632
|
+
// src/index.ts
|
|
7633
|
+
init_contributions();
|
|
7634
|
+
function runLibraryTool(contribution, args, options) {
|
|
7635
|
+
return createToolRuntime([contribution]).run(contribution, args, options).terminal;
|
|
7636
|
+
}
|
|
7637
|
+
var roster = [
|
|
7638
|
+
{
|
|
7639
|
+
operation: "project.preview",
|
|
7640
|
+
owner: "devkit/preview-host",
|
|
7641
|
+
realm: "host",
|
|
7642
|
+
artifacts: previewArtifacts(),
|
|
7643
|
+
benefit: "reuse the real WebGPU hidden preview recipe",
|
|
7644
|
+
fallback: "private"
|
|
7645
|
+
},
|
|
7646
|
+
{
|
|
7647
|
+
operation: "material.preview",
|
|
7648
|
+
owner: "engine-preview/material",
|
|
7649
|
+
realm: "host",
|
|
7650
|
+
artifacts: previewArtifacts(),
|
|
7651
|
+
benefit: "preview one material subject through the canonical lit rig",
|
|
7652
|
+
fallback: "private"
|
|
7653
|
+
},
|
|
7654
|
+
{
|
|
7655
|
+
operation: "mesh.preview",
|
|
7656
|
+
owner: "engine-preview/mesh",
|
|
7657
|
+
realm: "host",
|
|
7658
|
+
artifacts: previewArtifacts(),
|
|
7659
|
+
benefit: "preview one mesh subject with every submesh and AABB evidence",
|
|
7660
|
+
fallback: "private"
|
|
7661
|
+
},
|
|
7662
|
+
{
|
|
7663
|
+
operation: "vfx.preview",
|
|
7664
|
+
owner: "engine-preview/vfx",
|
|
7665
|
+
realm: "host",
|
|
7666
|
+
artifacts: previewArtifacts(),
|
|
7667
|
+
benefit: "preview one bounded VFX timeline with compute evidence",
|
|
7668
|
+
fallback: "private"
|
|
7669
|
+
},
|
|
7670
|
+
{
|
|
7671
|
+
operation: "texture.preview",
|
|
7672
|
+
owner: "engine-preview/texture",
|
|
7673
|
+
realm: "host",
|
|
7674
|
+
artifacts: previewArtifacts(),
|
|
7675
|
+
benefit: "preview one texture on the aspect-preserving unlit quad",
|
|
7676
|
+
fallback: "private"
|
|
7677
|
+
}
|
|
7678
|
+
];
|
|
7679
|
+
function previewArtifacts() {
|
|
7680
|
+
return [
|
|
7681
|
+
{
|
|
7682
|
+
owner: "preview-tool-proof",
|
|
7683
|
+
source: "M3",
|
|
7684
|
+
ref: {
|
|
7685
|
+
kind: "rhi-tape",
|
|
7686
|
+
digest: "sha256:e62a302dd29e302e1d0928306fe9c90c47ee336aa2e9e35fdc1d75586ae0018d",
|
|
7687
|
+
uri: "repo:apps/preview/__tests__/tool-proof.recipe.integration.test.ts"
|
|
6399
7688
|
}
|
|
6400
|
-
}
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
})
|
|
6409
|
-
);
|
|
7689
|
+
},
|
|
7690
|
+
{
|
|
7691
|
+
owner: "preview-tool-proof",
|
|
7692
|
+
source: "M3",
|
|
7693
|
+
ref: {
|
|
7694
|
+
kind: "profile-capture",
|
|
7695
|
+
digest: "sha256:11ec9024e1ea08b38f0901db272847ac402d2dce0c757efe744315608e889c5c",
|
|
7696
|
+
uri: "repo:packages/profiler/src/__tests__/fixtures/profile-capture/model-input.json"
|
|
6410
7697
|
}
|
|
6411
|
-
});
|
|
6412
|
-
};
|
|
6413
|
-
const closeBrowserProcess = async () => {
|
|
6414
|
-
await page?.close().catch(() => void 0);
|
|
6415
|
-
page = void 0;
|
|
6416
|
-
await browser?.close().catch(() => void 0);
|
|
6417
|
-
browser = void 0;
|
|
6418
|
-
};
|
|
6419
|
-
const abort = () => {
|
|
6420
|
-
void closeBrowserProcess();
|
|
6421
|
-
void server.close();
|
|
6422
|
-
};
|
|
6423
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
6424
|
-
try {
|
|
6425
|
-
if (signal.aborted) throw new Error("Browser Host aborted before launch");
|
|
6426
|
-
await server.listen();
|
|
6427
|
-
const address = server.httpServer?.address();
|
|
6428
|
-
if (address === null || address === void 0 || typeof address === "string") {
|
|
6429
|
-
throw new Error("Vite Browser Host did not expose a loopback TCP address");
|
|
6430
7698
|
}
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
}
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
await page.goto(captureUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
|
|
6451
|
-
await page.waitForFunction(
|
|
6452
|
-
() => globalThis.__forgeaxToolHost?.ready === true,
|
|
6453
|
-
void 0,
|
|
6454
|
-
{ timeout: 45e3 }
|
|
6455
|
-
);
|
|
6456
|
-
phase = "capture-run";
|
|
6457
|
-
const captured = await page.evaluate(async () => {
|
|
6458
|
-
const host = globalThis.__forgeaxToolHost;
|
|
6459
|
-
const value2 = await host.capture();
|
|
6460
|
-
return JSON.parse(
|
|
6461
|
-
JSON.stringify(
|
|
6462
|
-
value2,
|
|
6463
|
-
(_key, nested) => nested instanceof Error ? {
|
|
6464
|
-
...nested,
|
|
6465
|
-
name: nested.name,
|
|
6466
|
-
message: nested.message,
|
|
6467
|
-
stack: nested.stack
|
|
6468
|
-
} : nested
|
|
6469
|
-
)
|
|
6470
|
-
);
|
|
6471
|
-
});
|
|
6472
|
-
await Promise.all(responseDiagnostics);
|
|
6473
|
-
if (!captured.ok) return browserFailure("capture-run", captured.error, pageErrors);
|
|
6474
|
-
if (pageErrors.length > 0)
|
|
6475
|
-
return browserFailure("capture-page-runtime", pageErrors[0], pageErrors);
|
|
6476
|
-
const capturePng = await page.screenshot({ type: "png" });
|
|
6477
|
-
const capturedResult = {
|
|
6478
|
-
...captured.result,
|
|
6479
|
-
capturePng: {
|
|
6480
|
-
uri: `data:image/png;base64,${capturePng.toString("base64")}`,
|
|
6481
|
-
width: recipe.viewport.width,
|
|
6482
|
-
height: recipe.viewport.height
|
|
6483
|
-
}
|
|
6484
|
-
};
|
|
6485
|
-
await closeBrowserProcess();
|
|
6486
|
-
pageErrors = [];
|
|
6487
|
-
responseDiagnostics = [];
|
|
6488
|
-
phase = "replay-browser-launch";
|
|
6489
|
-
browser = await launchBrowser(true);
|
|
6490
|
-
phase = "replay-page-bootstrap";
|
|
6491
|
-
page = await browser.newPage({
|
|
6492
|
-
viewport: recipe.viewport,
|
|
6493
|
-
deviceScaleFactor: 1
|
|
6494
|
-
});
|
|
6495
|
-
observePage(page);
|
|
6496
|
-
const replayUrl = new URL(`http://127.0.0.1:${address.port}/`);
|
|
6497
|
-
replayUrl.searchParams.set("forgeax-tool-replay", "1");
|
|
6498
|
-
await page.goto(replayUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
|
|
6499
|
-
await page.waitForFunction(
|
|
6500
|
-
() => globalThis.__forgeaxToolReplayHost?.ready === true,
|
|
6501
|
-
void 0,
|
|
6502
|
-
{ timeout: 45e3 }
|
|
6503
|
-
);
|
|
6504
|
-
phase = "replay-run";
|
|
6505
|
-
const result = await page.evaluate(async (serializedCapture) => {
|
|
6506
|
-
const host = globalThis.__forgeaxToolReplayHost;
|
|
6507
|
-
const value2 = await host.run(JSON.parse(serializedCapture));
|
|
6508
|
-
return JSON.parse(
|
|
6509
|
-
JSON.stringify(
|
|
6510
|
-
value2,
|
|
6511
|
-
(_key, nested) => nested instanceof Error ? {
|
|
6512
|
-
...nested,
|
|
6513
|
-
name: nested.name,
|
|
6514
|
-
message: nested.message,
|
|
6515
|
-
stack: nested.stack
|
|
6516
|
-
} : nested
|
|
6517
|
-
)
|
|
6518
|
-
);
|
|
6519
|
-
}, JSON.stringify(capturedResult));
|
|
6520
|
-
await Promise.all(responseDiagnostics);
|
|
6521
|
-
if (!result.ok) return browserFailure("replay-run", result.error, pageErrors);
|
|
6522
|
-
if (pageErrors.length > 0)
|
|
6523
|
-
return browserFailure("replay-page-runtime", pageErrors[0], pageErrors);
|
|
6524
|
-
const endedAtMs = performance.now();
|
|
6525
|
-
const phases = result.result.operationTiming.phases;
|
|
6526
|
-
const observedDurationMs = phases === void 0 ? 0 : Object.values(phases).reduce(
|
|
6527
|
-
(total, observation) => total + (observation.status === "observed" ? observation.durationMs : 0),
|
|
6528
|
-
0
|
|
6529
|
-
);
|
|
6530
|
-
const durationMs = endedAtMs - hostStartedAtMs;
|
|
6531
|
-
const value = {
|
|
6532
|
-
...result.result,
|
|
6533
|
-
operationTiming: {
|
|
6534
|
-
...result.result.operationTiming,
|
|
6535
|
-
startedAtMs: hostStartedAtMs,
|
|
6536
|
-
endedAtMs,
|
|
6537
|
-
durationMs,
|
|
6538
|
-
...phases === void 0 ? { unattributedMs: durationMs } : {
|
|
6539
|
-
phases: {
|
|
6540
|
-
...phases,
|
|
6541
|
-
transport: {
|
|
6542
|
-
status: "observed",
|
|
6543
|
-
durationMs: Math.max(0, durationMs - observedDurationMs)
|
|
6544
|
-
}
|
|
6545
|
-
},
|
|
6546
|
-
unattributedMs: 0
|
|
6547
|
-
}
|
|
6548
|
-
},
|
|
6549
|
-
actualCarrier: recipe.presentation === "hidden" ? "headless-private" : "headed-private"
|
|
6550
|
-
};
|
|
6551
|
-
phase = "artifact-publish";
|
|
7699
|
+
];
|
|
7700
|
+
}
|
|
7701
|
+
function validEvidence(entry) {
|
|
7702
|
+
return entry.owner.length > 0 && entry.artifacts.length > 0 && entry.artifacts.every(
|
|
7703
|
+
({ owner, source, ref }) => owner.length > 0 && source === "M3" && /^sha256:[0-9a-f]{64}$/.test(ref.digest) && typeof ref.uri === "string" && ref.uri.startsWith("repo:") && isSupportedEvidenceKind(ref.kind)
|
|
7704
|
+
);
|
|
7705
|
+
}
|
|
7706
|
+
function isSupportedEvidenceKind(kind) {
|
|
7707
|
+
return kind === "rhi-tape" || kind === "profile-capture" || kind === "png";
|
|
7708
|
+
}
|
|
7709
|
+
function createMigrationRoster() {
|
|
7710
|
+
return roster.filter(validEvidence).map((entry) => ({
|
|
7711
|
+
...entry,
|
|
7712
|
+
artifacts: entry.artifacts.map((artifact) => ({ ...artifact, ref: { ...artifact.ref } }))
|
|
7713
|
+
}));
|
|
7714
|
+
}
|
|
7715
|
+
function resolveMigration(entries2, operation, target) {
|
|
7716
|
+
const entry = entries2.find((candidate) => candidate.operation === operation);
|
|
7717
|
+
if (entry === void 0 || !validEvidence(entry)) {
|
|
6552
7718
|
return {
|
|
6553
|
-
ok:
|
|
6554
|
-
|
|
7719
|
+
ok: false,
|
|
7720
|
+
error: capabilityUnavailableError(`migration:${operation}`, target.realm)
|
|
6555
7721
|
};
|
|
6556
|
-
} catch (cause) {
|
|
6557
|
-
await Promise.all(responseDiagnostics);
|
|
6558
|
-
return browserFailure(phase, cause, pageErrors);
|
|
6559
|
-
} finally {
|
|
6560
|
-
signal.removeEventListener("abort", abort);
|
|
6561
|
-
await closeBrowserProcess();
|
|
6562
|
-
await server.close().catch(() => void 0);
|
|
6563
|
-
await rm(cacheDir, { recursive: true, force: true });
|
|
6564
7722
|
}
|
|
7723
|
+
if (entry.realm !== target.realm || target.catalogDigest.length === 0) {
|
|
7724
|
+
return { ok: false, error: capabilityUnavailableError(`migration:${operation}`, target.realm) };
|
|
7725
|
+
}
|
|
7726
|
+
if (entry.realm === "host" && target.rhiBackend !== "webgpu") {
|
|
7727
|
+
return { ok: false, error: capabilityUnavailableError(`rhi:${operation}`, target.realm) };
|
|
7728
|
+
}
|
|
7729
|
+
const evidenceKinds = entry.artifacts.map(({ ref }) => ref.kind).filter(isSupportedEvidenceKind);
|
|
7730
|
+
if (!evidenceKinds.every((kind) => target.evidence.includes(kind))) {
|
|
7731
|
+
return { ok: false, error: capabilityUnavailableError(`evidence:${operation}`, target.realm) };
|
|
7732
|
+
}
|
|
7733
|
+
const service = createServiceCapability(void 0, {
|
|
7734
|
+
toolId: operation,
|
|
7735
|
+
descriptorDigest: entry.artifacts[0]?.ref.digest ?? "",
|
|
7736
|
+
recipeDigest: entry.artifacts[0]?.ref.digest ?? "",
|
|
7737
|
+
workloadClass: `migration:${operation}`,
|
|
7738
|
+
codeDigest: target.catalogDigest,
|
|
7739
|
+
browserVersion: "unavailable",
|
|
7740
|
+
backend: "webgpu"
|
|
7741
|
+
});
|
|
7742
|
+
return {
|
|
7743
|
+
ok: true,
|
|
7744
|
+
value: {
|
|
7745
|
+
path: service.available ? "service" : "private",
|
|
7746
|
+
operation,
|
|
7747
|
+
owner: entry.owner,
|
|
7748
|
+
artifacts: entry.artifacts.map((artifact) => ({ ...artifact, ref: { ...artifact.ref } })),
|
|
7749
|
+
service
|
|
7750
|
+
}
|
|
7751
|
+
};
|
|
6565
7752
|
}
|
|
6566
7753
|
|
|
7754
|
+
// src/index.ts
|
|
7755
|
+
init_offline_analysis();
|
|
7756
|
+
init_preview_contributions();
|
|
7757
|
+
|
|
6567
7758
|
// src/tools/preview-host.ts
|
|
7759
|
+
init_browser_host();
|
|
6568
7760
|
function carrierRouteFailure(code, expected, hint, detail) {
|
|
6569
7761
|
return { ok: false, error: { code, expected, hint, detail } };
|
|
6570
7762
|
}
|
|
@@ -6836,14 +8028,14 @@ async function createAuthenticatedLoopbackService(options) {
|
|
|
6836
8028
|
reply2(response, 400, { error: message });
|
|
6837
8029
|
}
|
|
6838
8030
|
});
|
|
6839
|
-
await new Promise((
|
|
8031
|
+
await new Promise((resolve19, reject) => {
|
|
6840
8032
|
const onError = (error) => {
|
|
6841
8033
|
server.off("listening", onListening);
|
|
6842
8034
|
reject(error);
|
|
6843
8035
|
};
|
|
6844
8036
|
const onListening = () => {
|
|
6845
8037
|
server.off("error", onError);
|
|
6846
|
-
|
|
8038
|
+
resolve19();
|
|
6847
8039
|
};
|
|
6848
8040
|
server.once("error", onError);
|
|
6849
8041
|
server.once("listening", onListening);
|
|
@@ -6851,7 +8043,7 @@ async function createAuthenticatedLoopbackService(options) {
|
|
|
6851
8043
|
});
|
|
6852
8044
|
const address = server.address();
|
|
6853
8045
|
if (address === null || typeof address === "string") {
|
|
6854
|
-
await new Promise((
|
|
8046
|
+
await new Promise((resolve19) => server.close(() => resolve19()));
|
|
6855
8047
|
throw new Error("loopback service did not expose a TCP address");
|
|
6856
8048
|
}
|
|
6857
8049
|
const endpoint = `http://${host}:${address.port}/run`;
|
|
@@ -6866,8 +8058,8 @@ async function createAuthenticatedLoopbackService(options) {
|
|
|
6866
8058
|
async close() {
|
|
6867
8059
|
if (closed) return;
|
|
6868
8060
|
closed = true;
|
|
6869
|
-
await new Promise((
|
|
6870
|
-
server.close((error) => error === void 0 ?
|
|
8061
|
+
await new Promise((resolve19, reject) => {
|
|
8062
|
+
server.close((error) => error === void 0 ? resolve19() : reject(error));
|
|
6871
8063
|
});
|
|
6872
8064
|
transport.close();
|
|
6873
8065
|
}
|
|
@@ -6877,6 +8069,6 @@ async function createAuthenticatedLoopbackService(options) {
|
|
|
6877
8069
|
// src/index.ts
|
|
6878
8070
|
init_types();
|
|
6879
8071
|
|
|
6880
|
-
export { DEFAULT_ADMISSION_THRESHOLDS, ENGINE_BINDING_SCHEMA_VERSION, RHI_DEBUG_OPERATION_MANIFEST, analyzePreviewArtifacts, assetAddCommand, assetInspectCommand, assetListCommand, assetVerifyCommand, bootstrapRealm, browserCaptureCommand, buildCommand, createAdmissionReport, createAuthenticatedLoopbackService, createAuthorContribution, createBrowserCapture, createBuildContribution, createCarrierProvider, createCarrierProviderService, createCarrierRendezvous, createCliRhiDebugOperationContext, createDefaultContributions, createDevkitToolRuntime,
|
|
8072
|
+
export { DEFAULT_ADMISSION_THRESHOLDS, ENGINE_BINDING_SCHEMA_VERSION, RHI_DEBUG_OPERATION_MANIFEST, analyzePreviewArtifacts, assetAddCommand, assetInspectCommand, assetListCommand, assetVerifyCommand, bootstrapRealm, browserCaptureCommand, buildCommand, bundleSingleHtmlEntry, createAdmissionReport, createAuthenticatedLoopbackService, createAuthorContribution, createBrowserCapture, createBuildContribution, createCarrierProvider, createCarrierProviderService, createCarrierRendezvous, createCliRhiDebugOperationContext, createDefaultContributions, createDevkitToolRuntime, createInitPlan, createMigrationRoster, createOfflineAnalysisContribution, createPreviewContribution, createPreviewContributions, createPreviewToolRuntime, createResourceProbe, createRhiDebugOperationContext, createServiceCache, createServiceExecutor, createSoftwareBrowser, createToolClient, describeTool, devCommand, discoverRhiDebugOperations, doctorCommand, engineBindingFilePath, engineDoctorCommand, engineStatusCommand, engineUnlinkCommand, engineUseLocalCommand, initCommand, inspectEngineWorkspace, listTools, materializeToolCatalog, newCommand, packageCommand, packageFormatError, packageOutputError, pluginInstallCommand, pluginUninstallCommand, previewCommand, readEngineBinding, readProjectFacts, rebuildToolCatalog, recoverRhiDebugError, renderRhiDebugHelp, resolveMigration, resolveProjectPort, resolveRealmCapability, runBenchmarkAdmission, runCarrierPreviewRoute, runGenericTool, runLibraryTool, runNamedTool, runPreviewHost, runPrivateTool, runRhiDebugCommand, runRhiDebugOperation, sdkInstallCommand, shaderCheckCommand, skillInstallCommand, skillVerifyCommand, softwareCaptureCommand, summarizeBenchmarkSamples, testCommand, verifyDist, writeDistManifest, writeSingleHtml };
|
|
6881
8073
|
//# sourceMappingURL=index.mjs.map
|
|
6882
8074
|
//# sourceMappingURL=index.mjs.map
|