@stage5/lumine 0.2.6 → 0.2.8
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/lib/api.js +23 -0
- package/lib/commands.js +353 -1
- package/lib/constants.js +30 -0
- package/lib/doctor.js +720 -0
- package/lib/util.js +7 -0
- package/lib/workspace.js +56 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +35 -3
package/lib/doctor.js
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
|
|
3
|
+
import { buildApiJson, mintBuildApiToken } from "./api.js";
|
|
4
|
+
import { ensureAuth, assertAuthScope } from "./auth.js";
|
|
5
|
+
import { uploadRuntimeAsset } from "./assets.js";
|
|
6
|
+
import { requestJson } from "./http.js";
|
|
7
|
+
import { resolveSdkBuildId } from "./sdk.js";
|
|
8
|
+
import { formatBytes, trimTrailingSlash } from "./util.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PREVIEW_URL = "https://preview.lumine.app";
|
|
11
|
+
const DOCTOR_SOURCE = "lumine-runtime-assets-doctor";
|
|
12
|
+
const PREVIEW_IFRAME_SANDBOX = "allow-scripts allow-downloads allow-pointer-lock";
|
|
13
|
+
|
|
14
|
+
export async function doctorCommand(options) {
|
|
15
|
+
const topic = String(options.positional[0] || "runtime-assets").trim();
|
|
16
|
+
if (topic !== "runtime-assets") {
|
|
17
|
+
throw new Error("Usage: lumine doctor runtime-assets [--build <id>] [--json] [--keep-assets] [--no-browser]");
|
|
18
|
+
}
|
|
19
|
+
await runtimeAssetsDoctor(options);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runtimeAssetsDoctor(options) {
|
|
23
|
+
const buildId = await resolveSdkBuildId(options);
|
|
24
|
+
const auth = await ensureAuth(options);
|
|
25
|
+
await assertAuthScope({ options, auth, scope: "build:sdk" });
|
|
26
|
+
await assertAuthScope({ options, auth, scope: "build:check" });
|
|
27
|
+
|
|
28
|
+
const { token: buildApiToken } = await mintBuildApiToken({
|
|
29
|
+
options,
|
|
30
|
+
auth,
|
|
31
|
+
buildId,
|
|
32
|
+
scopes: ["files:read", "files:write", "preview:read"],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const result = {
|
|
36
|
+
ok: false,
|
|
37
|
+
buildId,
|
|
38
|
+
checkedAt: new Date().toISOString(),
|
|
39
|
+
uploadedAssets: [],
|
|
40
|
+
cloudfront: [],
|
|
41
|
+
previewSession: null,
|
|
42
|
+
browser: null,
|
|
43
|
+
cleanup: [],
|
|
44
|
+
};
|
|
45
|
+
const uploadedAssets = [];
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const candidates = makeRuntimeAssetProbeCandidates();
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
const asset = await uploadRuntimeAsset({
|
|
51
|
+
options,
|
|
52
|
+
auth,
|
|
53
|
+
buildId,
|
|
54
|
+
buildApiToken,
|
|
55
|
+
candidate,
|
|
56
|
+
});
|
|
57
|
+
uploadedAssets.push(asset);
|
|
58
|
+
result.uploadedAssets.push(formatAssetForReport(asset));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const asset of uploadedAssets) {
|
|
62
|
+
result.cloudfront.push(await probeCloudFrontAsset(asset, options));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (options.noBrowser) {
|
|
66
|
+
result.previewSession = {
|
|
67
|
+
ok: false,
|
|
68
|
+
skipped: true,
|
|
69
|
+
reason: "browser_probe_disabled",
|
|
70
|
+
};
|
|
71
|
+
result.browser = {
|
|
72
|
+
ok: false,
|
|
73
|
+
skipped: true,
|
|
74
|
+
reason: "browser_probe_disabled",
|
|
75
|
+
};
|
|
76
|
+
} else {
|
|
77
|
+
result.previewSession = await createRuntimeAssetsPreviewSession({
|
|
78
|
+
options,
|
|
79
|
+
auth,
|
|
80
|
+
buildId,
|
|
81
|
+
hdrAsset: findAssetByExtension(uploadedAssets, ".hdr"),
|
|
82
|
+
glbAsset: findAssetByExtension(uploadedAssets, ".glb"),
|
|
83
|
+
});
|
|
84
|
+
result.browser = await runBrowserLoaderProbe({
|
|
85
|
+
siteUrl: options.siteUrl,
|
|
86
|
+
previewUrl: options.previewUrl,
|
|
87
|
+
previewEntryUrl: result.previewSession.previewEntryUrl,
|
|
88
|
+
timeoutMs: options.timeoutMs,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
result.headersOk = cloudFrontRequiredProbesPass(result.cloudfront);
|
|
93
|
+
result.complete = Boolean(result.browser?.ok);
|
|
94
|
+
result.ok = result.headersOk && (options.noBrowser || result.complete);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
result.ok = false;
|
|
97
|
+
result.headersOk = cloudFrontRequiredProbesPass(result.cloudfront);
|
|
98
|
+
result.complete = false;
|
|
99
|
+
result.error = {
|
|
100
|
+
message: error?.message || String(error),
|
|
101
|
+
status: error?.status || null,
|
|
102
|
+
data: error?.data || null,
|
|
103
|
+
};
|
|
104
|
+
if (!result.previewSession) {
|
|
105
|
+
result.previewSession = {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: result.error.message,
|
|
108
|
+
status: result.error.status,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
if (!options.keepAssets) {
|
|
113
|
+
for (const asset of uploadedAssets) {
|
|
114
|
+
result.cleanup.push(
|
|
115
|
+
await deleteRuntimeAssetQuietly({
|
|
116
|
+
options,
|
|
117
|
+
auth,
|
|
118
|
+
buildId,
|
|
119
|
+
buildApiToken,
|
|
120
|
+
asset,
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
result.cleanup.push({
|
|
126
|
+
skipped: true,
|
|
127
|
+
reason: "keep_assets",
|
|
128
|
+
assetIds: uploadedAssets.map((asset) => Number(asset.id || 0)).filter(Boolean),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
printRuntimeAssetsDoctorResult(result, options);
|
|
134
|
+
if (!result.ok) {
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function makeRuntimeAssetProbeCandidates() {
|
|
140
|
+
const stamp = new Date().toISOString().replace(/[^0-9A-Za-z]+/g, "-");
|
|
141
|
+
return [
|
|
142
|
+
{
|
|
143
|
+
absolutePath: null,
|
|
144
|
+
fileName: `lumine-doctor-env-${stamp}.hdr`,
|
|
145
|
+
mimeType: "image/vnd.radiance",
|
|
146
|
+
buffer: makeMinimalHdr(),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
absolutePath: null,
|
|
150
|
+
fileName: `lumine-doctor-scene-${stamp}.glb`,
|
|
151
|
+
mimeType: "model/gltf-binary",
|
|
152
|
+
buffer: makeMinimalGlb(),
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function makeMinimalHdr() {
|
|
158
|
+
const header = "#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n-Y 1 +X 1\n";
|
|
159
|
+
return Buffer.concat([
|
|
160
|
+
Buffer.from(header, "ascii"),
|
|
161
|
+
Buffer.from([255, 255, 255, 128]),
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function makeMinimalGlb() {
|
|
166
|
+
const json = JSON.stringify({
|
|
167
|
+
asset: { version: "2.0", generator: "lumine-runtime-assets-doctor" },
|
|
168
|
+
scene: 0,
|
|
169
|
+
scenes: [{ nodes: [] }],
|
|
170
|
+
nodes: [],
|
|
171
|
+
});
|
|
172
|
+
const jsonPadding = (4 - (Buffer.byteLength(json) % 4)) % 4;
|
|
173
|
+
const jsonChunk = Buffer.concat([
|
|
174
|
+
Buffer.from(json, "utf8"),
|
|
175
|
+
Buffer.alloc(jsonPadding, 0x20),
|
|
176
|
+
]);
|
|
177
|
+
const totalLength = 12 + 8 + jsonChunk.length;
|
|
178
|
+
const buffer = Buffer.alloc(totalLength);
|
|
179
|
+
buffer.writeUInt32LE(0x46546c67, 0);
|
|
180
|
+
buffer.writeUInt32LE(2, 4);
|
|
181
|
+
buffer.writeUInt32LE(totalLength, 8);
|
|
182
|
+
buffer.writeUInt32LE(jsonChunk.length, 12);
|
|
183
|
+
buffer.writeUInt32LE(0x4e4f534a, 16);
|
|
184
|
+
jsonChunk.copy(buffer, 20);
|
|
185
|
+
return buffer;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function formatAssetForReport(asset) {
|
|
189
|
+
return {
|
|
190
|
+
id: Number(asset?.id || 0),
|
|
191
|
+
fileName: asset?.fileName || "",
|
|
192
|
+
originalFileName: asset?.originalFileName || asset?.fileName || "",
|
|
193
|
+
mimeType: asset?.mimeType || null,
|
|
194
|
+
sizeBytes: Number(asset?.sizeBytes || 0),
|
|
195
|
+
url: asset?.url || "",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function findAssetByExtension(assets, extension) {
|
|
200
|
+
return assets.find((asset) =>
|
|
201
|
+
String(asset?.fileName || asset?.originalFileName || "")
|
|
202
|
+
.toLowerCase()
|
|
203
|
+
.endsWith(extension),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function probeCloudFrontAsset(asset, options) {
|
|
208
|
+
const probes = buildCloudFrontProbeSpecs(options);
|
|
209
|
+
const requests = [];
|
|
210
|
+
for (const probe of probes) {
|
|
211
|
+
requests.push(await probeCloudFrontGet(asset.url, probe, options.timeoutMs));
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
asset: formatAssetForReport(asset),
|
|
215
|
+
requests,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildCloudFrontProbeSpecs(options) {
|
|
220
|
+
const probes = [
|
|
221
|
+
{
|
|
222
|
+
label: "opaque sandbox",
|
|
223
|
+
origin: "null",
|
|
224
|
+
required: true,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
label: "preview origin",
|
|
228
|
+
origin: normalizeOrigin(options.previewUrl),
|
|
229
|
+
required: false,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
label: "site origin",
|
|
233
|
+
origin: normalizeOrigin(options.siteUrl),
|
|
234
|
+
required: false,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
label: "no Origin",
|
|
238
|
+
origin: null,
|
|
239
|
+
required: false,
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
const merged = new Map();
|
|
243
|
+
for (const probe of probes) {
|
|
244
|
+
if (probe.origin === "") continue;
|
|
245
|
+
const key = probe.origin === null ? "no-origin" : probe.origin;
|
|
246
|
+
const existing = merged.get(key);
|
|
247
|
+
if (!existing) {
|
|
248
|
+
merged.set(key, probe);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
existing.required = existing.required || probe.required;
|
|
252
|
+
existing.label = existing.required ? existing.label : probe.label;
|
|
253
|
+
}
|
|
254
|
+
return Array.from(merged.values());
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeOrigin(value) {
|
|
258
|
+
try {
|
|
259
|
+
return new URL(String(value || "")).origin;
|
|
260
|
+
} catch {
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function cloudFrontRequiredProbesPass(cloudfront) {
|
|
266
|
+
if (!cloudfront.length) return false;
|
|
267
|
+
return cloudfront.every((entry) =>
|
|
268
|
+
entry.requests.filter((request) => request.required).length > 0 &&
|
|
269
|
+
entry.requests
|
|
270
|
+
.filter((request) => request.required)
|
|
271
|
+
.every((request) => request.ok && request.corsAllowed),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function probeCloudFrontGet(url, probe, timeoutMs) {
|
|
276
|
+
const controller = new AbortController();
|
|
277
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
278
|
+
const startedAt = Date.now();
|
|
279
|
+
try {
|
|
280
|
+
const response = await fetch(url, {
|
|
281
|
+
method: "GET",
|
|
282
|
+
headers: probe.origin ? { origin: probe.origin } : {},
|
|
283
|
+
signal: controller.signal,
|
|
284
|
+
});
|
|
285
|
+
const body = await response.arrayBuffer();
|
|
286
|
+
const headers = pickHeaders(response.headers);
|
|
287
|
+
const corsAllowed = accessControlAllowOriginMatches({
|
|
288
|
+
headers,
|
|
289
|
+
origin: probe.origin,
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
label: probe.label,
|
|
293
|
+
origin: probe.origin,
|
|
294
|
+
required: probe.required,
|
|
295
|
+
ok: response.ok,
|
|
296
|
+
status: response.status,
|
|
297
|
+
ms: Date.now() - startedAt,
|
|
298
|
+
bytes: body.byteLength,
|
|
299
|
+
hasAccessControlAllowOrigin: Boolean(headers["access-control-allow-origin"]),
|
|
300
|
+
corsAllowed,
|
|
301
|
+
headers,
|
|
302
|
+
};
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return {
|
|
305
|
+
label: probe.label,
|
|
306
|
+
origin: probe.origin,
|
|
307
|
+
required: probe.required,
|
|
308
|
+
ok: false,
|
|
309
|
+
status: 0,
|
|
310
|
+
ms: Date.now() - startedAt,
|
|
311
|
+
bytes: 0,
|
|
312
|
+
hasAccessControlAllowOrigin: false,
|
|
313
|
+
corsAllowed: false,
|
|
314
|
+
error: error?.message || String(error),
|
|
315
|
+
headers: {},
|
|
316
|
+
};
|
|
317
|
+
} finally {
|
|
318
|
+
clearTimeout(timeout);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function accessControlAllowOriginMatches({ headers, origin }) {
|
|
323
|
+
const acao = String(headers["access-control-allow-origin"] || "").trim();
|
|
324
|
+
if (!acao) return false;
|
|
325
|
+
// No-Origin requests are telemetry only. They are not browser CORS checks, so
|
|
326
|
+
// report ACAO presence but never let this path affect required pass/fail.
|
|
327
|
+
if (origin === null) return true;
|
|
328
|
+
return acao === "*" || acao === origin;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function pickHeaders(headers) {
|
|
332
|
+
const keys = [
|
|
333
|
+
"access-control-allow-origin",
|
|
334
|
+
"access-control-allow-methods",
|
|
335
|
+
"access-control-expose-headers",
|
|
336
|
+
"content-type",
|
|
337
|
+
"content-length",
|
|
338
|
+
"cache-control",
|
|
339
|
+
"etag",
|
|
340
|
+
"x-cache",
|
|
341
|
+
"via",
|
|
342
|
+
"x-amz-cf-pop",
|
|
343
|
+
];
|
|
344
|
+
const picked = {};
|
|
345
|
+
for (const key of keys) {
|
|
346
|
+
const value = headers.get(key);
|
|
347
|
+
if (value !== null) picked[key] = value;
|
|
348
|
+
}
|
|
349
|
+
return picked;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function createRuntimeAssetsPreviewSession({
|
|
353
|
+
options,
|
|
354
|
+
auth,
|
|
355
|
+
buildId,
|
|
356
|
+
hdrAsset,
|
|
357
|
+
glbAsset,
|
|
358
|
+
}) {
|
|
359
|
+
if (!hdrAsset?.url || !glbAsset?.url) {
|
|
360
|
+
throw new Error("Runtime asset probe did not produce both .hdr and .glb URLs.");
|
|
361
|
+
}
|
|
362
|
+
const session = await requestJson({
|
|
363
|
+
method: "POST",
|
|
364
|
+
url: `${options.apiUrl}/build/${buildId}/preview-session`,
|
|
365
|
+
authToken: auth.token,
|
|
366
|
+
body: {
|
|
367
|
+
entryPath: "/index.html",
|
|
368
|
+
files: [
|
|
369
|
+
{
|
|
370
|
+
path: "/index.html",
|
|
371
|
+
content: buildRuntimeAssetsProbeHtml({
|
|
372
|
+
hdrUrl: hdrAsset.url,
|
|
373
|
+
glbUrl: glbAsset.url,
|
|
374
|
+
}),
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
timeoutMs: options.timeoutMs,
|
|
379
|
+
});
|
|
380
|
+
const entryUrl = String(session?.entryUrl || "");
|
|
381
|
+
if (!entryUrl) {
|
|
382
|
+
throw new Error("Twinkle did not return a preview-session entry URL.");
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
ok: true,
|
|
386
|
+
sessionId: session.sessionId || null,
|
|
387
|
+
entryUrl,
|
|
388
|
+
previewEntryUrl: `${options.previewUrl}${entryUrl}`,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function buildRuntimeAssetsProbeHtml({ hdrUrl, glbUrl }) {
|
|
393
|
+
return `<!doctype html>
|
|
394
|
+
<html>
|
|
395
|
+
<head>
|
|
396
|
+
<meta charset="utf-8">
|
|
397
|
+
<title>Lumine Runtime Assets Doctor</title>
|
|
398
|
+
</head>
|
|
399
|
+
<body>
|
|
400
|
+
<script type="module">
|
|
401
|
+
const source = ${JSON.stringify(DOCTOR_SOURCE)};
|
|
402
|
+
const hdrUrl = ${JSON.stringify(hdrUrl)};
|
|
403
|
+
const glbUrl = ${JSON.stringify(glbUrl)};
|
|
404
|
+
|
|
405
|
+
function headersToObject(headers) {
|
|
406
|
+
const result = {};
|
|
407
|
+
for (const [key, value] of headers.entries()) {
|
|
408
|
+
result[key.toLowerCase()] = value;
|
|
409
|
+
}
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function fetchAsset(url) {
|
|
414
|
+
const response = await fetch(url, {
|
|
415
|
+
method: 'GET',
|
|
416
|
+
mode: 'cors',
|
|
417
|
+
cache: 'no-store',
|
|
418
|
+
credentials: 'omit'
|
|
419
|
+
});
|
|
420
|
+
const body = await response.arrayBuffer();
|
|
421
|
+
return {
|
|
422
|
+
ok: response.ok,
|
|
423
|
+
status: response.status,
|
|
424
|
+
type: response.type,
|
|
425
|
+
url: response.url,
|
|
426
|
+
bytes: body.byteLength,
|
|
427
|
+
headers: headersToObject(response.headers)
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function loadHdr(url) {
|
|
432
|
+
const { RGBELoader } = await import('/build/vendor/three/0.184.0/addons/loaders/RGBELoader.js');
|
|
433
|
+
const texture = await new Promise((resolve, reject) => {
|
|
434
|
+
new RGBELoader().load(url, resolve, undefined, reject);
|
|
435
|
+
});
|
|
436
|
+
return {
|
|
437
|
+
ok: true,
|
|
438
|
+
width: texture && texture.image ? texture.image.width || null : null,
|
|
439
|
+
height: texture && texture.image ? texture.image.height || null : null,
|
|
440
|
+
type: texture ? texture.type || null : null
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function loadGlb(url) {
|
|
445
|
+
const { GLTFLoader } = await import('/build/vendor/three/0.184.0/addons/loaders/GLTFLoader.js');
|
|
446
|
+
const gltf = await new Promise((resolve, reject) => {
|
|
447
|
+
new GLTFLoader().load(url, resolve, undefined, reject);
|
|
448
|
+
});
|
|
449
|
+
return {
|
|
450
|
+
ok: true,
|
|
451
|
+
sceneType: gltf && gltf.scene ? gltf.scene.type || null : null,
|
|
452
|
+
sceneChildren: gltf && gltf.scene && gltf.scene.children ? gltf.scene.children.length : null,
|
|
453
|
+
scenes: gltf && gltf.scenes ? gltf.scenes.length : null
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function run() {
|
|
458
|
+
const result = {
|
|
459
|
+
source,
|
|
460
|
+
ok: false,
|
|
461
|
+
href: location.href,
|
|
462
|
+
origin: location.origin,
|
|
463
|
+
crossOriginIsolated: window.crossOriginIsolated === true,
|
|
464
|
+
hdrFetch: null,
|
|
465
|
+
glbFetch: null,
|
|
466
|
+
hdrLoader: null,
|
|
467
|
+
glbLoader: null
|
|
468
|
+
};
|
|
469
|
+
try {
|
|
470
|
+
result.hdrFetch = await fetchAsset(hdrUrl);
|
|
471
|
+
result.glbFetch = await fetchAsset(glbUrl);
|
|
472
|
+
result.hdrLoader = await loadHdr(hdrUrl).catch((error) => ({
|
|
473
|
+
ok: false,
|
|
474
|
+
name: error && error.name ? error.name : null,
|
|
475
|
+
error: error && error.message ? error.message : String(error)
|
|
476
|
+
}));
|
|
477
|
+
result.glbLoader = await loadGlb(glbUrl).catch((error) => ({
|
|
478
|
+
ok: false,
|
|
479
|
+
name: error && error.name ? error.name : null,
|
|
480
|
+
error: error && error.message ? error.message : String(error)
|
|
481
|
+
}));
|
|
482
|
+
result.ok = Boolean(
|
|
483
|
+
result.hdrFetch && result.hdrFetch.ok &&
|
|
484
|
+
result.glbFetch && result.glbFetch.ok &&
|
|
485
|
+
result.hdrLoader && result.hdrLoader.ok &&
|
|
486
|
+
result.glbLoader && result.glbLoader.ok
|
|
487
|
+
);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
result.error = error && error.message ? error.message : String(error);
|
|
490
|
+
}
|
|
491
|
+
window.parent.postMessage(result, '*');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
run();
|
|
495
|
+
</script>
|
|
496
|
+
</body>
|
|
497
|
+
</html>`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function runBrowserLoaderProbe({
|
|
501
|
+
siteUrl,
|
|
502
|
+
previewUrl,
|
|
503
|
+
previewEntryUrl,
|
|
504
|
+
timeoutMs,
|
|
505
|
+
}) {
|
|
506
|
+
const playwright = await loadPlaywright();
|
|
507
|
+
if (!playwright?.chromium) {
|
|
508
|
+
return {
|
|
509
|
+
ok: false,
|
|
510
|
+
skipped: true,
|
|
511
|
+
reason: "playwright_not_available",
|
|
512
|
+
message:
|
|
513
|
+
"Install Playwright or set LUMINE_PLAYWRIGHT_MODULE to enable the sandboxed loader probe.",
|
|
514
|
+
previewEntryUrl,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const browser = await playwright.chromium.launch({ headless: true });
|
|
519
|
+
const page = await browser.newPage({
|
|
520
|
+
viewport: { width: 1280, height: 900 },
|
|
521
|
+
});
|
|
522
|
+
const consoleMessages = [];
|
|
523
|
+
page.on("console", (message) => {
|
|
524
|
+
consoleMessages.push({
|
|
525
|
+
type: message.type(),
|
|
526
|
+
text: message.text().slice(0, 500),
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
const harnessUrl = buildBrowserHarnessUrl(siteUrl);
|
|
532
|
+
await page.route(harnessUrl, async (route) => {
|
|
533
|
+
await route.fulfill({
|
|
534
|
+
status: 200,
|
|
535
|
+
contentType: "text/html; charset=utf-8",
|
|
536
|
+
headers: { "cache-control": "no-store" },
|
|
537
|
+
body: buildOuterSandboxHarness(previewEntryUrl),
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
await page.goto(harnessUrl, {
|
|
541
|
+
waitUntil: "domcontentloaded",
|
|
542
|
+
timeout: timeoutMs,
|
|
543
|
+
});
|
|
544
|
+
const loaderResult = await waitForBrowserProbeResult(page, timeoutMs);
|
|
545
|
+
return {
|
|
546
|
+
ok: Boolean(loaderResult?.ok),
|
|
547
|
+
harnessUrl,
|
|
548
|
+
previewOrigin: previewUrl,
|
|
549
|
+
previewEntryUrl,
|
|
550
|
+
sandbox: PREVIEW_IFRAME_SANDBOX,
|
|
551
|
+
result: loaderResult,
|
|
552
|
+
consoleMessages,
|
|
553
|
+
};
|
|
554
|
+
} finally {
|
|
555
|
+
await browser.close();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function buildBrowserHarnessUrl(siteUrl) {
|
|
560
|
+
const url = new URL("/__lumine-doctor/runtime-assets-harness", siteUrl);
|
|
561
|
+
url.searchParams.set("t", String(Date.now()));
|
|
562
|
+
return url.toString();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function waitForBrowserProbeResult(page, timeoutMs) {
|
|
566
|
+
try {
|
|
567
|
+
await page.waitForFunction(
|
|
568
|
+
() => Boolean(window.__lumineRuntimeAssetsDoctorResult),
|
|
569
|
+
null,
|
|
570
|
+
{ timeout: timeoutMs },
|
|
571
|
+
);
|
|
572
|
+
return await page.evaluate(() => window.__lumineRuntimeAssetsDoctorResult);
|
|
573
|
+
} catch (error) {
|
|
574
|
+
return {
|
|
575
|
+
ok: false,
|
|
576
|
+
error: error?.message || "Timed out waiting for runtime asset probe message.",
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function loadPlaywright() {
|
|
582
|
+
const require = createRequire(import.meta.url);
|
|
583
|
+
const candidates = [
|
|
584
|
+
process.env.LUMINE_PLAYWRIGHT_MODULE,
|
|
585
|
+
"playwright",
|
|
586
|
+
].filter(Boolean);
|
|
587
|
+
for (const candidate of candidates) {
|
|
588
|
+
try {
|
|
589
|
+
return require(candidate);
|
|
590
|
+
} catch {
|
|
591
|
+
// Try the next candidate.
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function buildOuterSandboxHarness(previewEntryUrl) {
|
|
598
|
+
return `<!doctype html>
|
|
599
|
+
<html>
|
|
600
|
+
<head>
|
|
601
|
+
<meta charset="utf-8">
|
|
602
|
+
<title>Lumine Runtime Assets Doctor Harness</title>
|
|
603
|
+
</head>
|
|
604
|
+
<body>
|
|
605
|
+
<script>
|
|
606
|
+
window.__lumineRuntimeAssetsDoctorResult = null;
|
|
607
|
+
window.addEventListener('message', (event) => {
|
|
608
|
+
if (!event || !event.data || event.data.source !== ${JSON.stringify(DOCTOR_SOURCE)}) return;
|
|
609
|
+
window.__lumineRuntimeAssetsDoctorResult = event.data;
|
|
610
|
+
});
|
|
611
|
+
</script>
|
|
612
|
+
<iframe
|
|
613
|
+
title="Lumine runtime assets doctor"
|
|
614
|
+
sandbox="${PREVIEW_IFRAME_SANDBOX}"
|
|
615
|
+
src="${escapeHtmlAttribute(previewEntryUrl)}"
|
|
616
|
+
style="width: 800px; height: 600px; border: 0;"
|
|
617
|
+
></iframe>
|
|
618
|
+
</body>
|
|
619
|
+
</html>`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function escapeHtmlAttribute(value) {
|
|
623
|
+
return String(value || "")
|
|
624
|
+
.replace(/&/g, "&")
|
|
625
|
+
.replace(/"/g, """)
|
|
626
|
+
.replace(/</g, "<")
|
|
627
|
+
.replace(/>/g, ">");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function deleteRuntimeAssetQuietly({
|
|
631
|
+
options,
|
|
632
|
+
auth,
|
|
633
|
+
buildId,
|
|
634
|
+
buildApiToken,
|
|
635
|
+
asset,
|
|
636
|
+
}) {
|
|
637
|
+
const assetId = Number(asset?.id || 0);
|
|
638
|
+
if (!assetId) {
|
|
639
|
+
return { ok: false, assetId, error: "missing_asset_id" };
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
await buildApiJson({
|
|
643
|
+
options,
|
|
644
|
+
auth,
|
|
645
|
+
buildId,
|
|
646
|
+
buildApiToken,
|
|
647
|
+
endpointPath: "api/files/delete",
|
|
648
|
+
body: { assetId },
|
|
649
|
+
});
|
|
650
|
+
return { ok: true, assetId };
|
|
651
|
+
} catch (error) {
|
|
652
|
+
return {
|
|
653
|
+
ok: false,
|
|
654
|
+
assetId,
|
|
655
|
+
status: error?.status || null,
|
|
656
|
+
error: error?.message || String(error),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function printRuntimeAssetsDoctorResult(result, options) {
|
|
662
|
+
if (options.json) {
|
|
663
|
+
console.log(JSON.stringify(result, null, 2));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
console.log(`Runtime asset doctor for Build #${result.buildId}`);
|
|
668
|
+
for (const asset of result.uploadedAssets) {
|
|
669
|
+
console.log(
|
|
670
|
+
`- uploaded #${asset.id} ${asset.originalFileName} ${formatBytes(asset.sizeBytes)}`,
|
|
671
|
+
);
|
|
672
|
+
console.log(` ${asset.url}`);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
for (const entry of result.cloudfront) {
|
|
676
|
+
console.log(`- CloudFront ${entry.asset.originalFileName}`);
|
|
677
|
+
for (const request of entry.requests) {
|
|
678
|
+
const origin = request.origin === null ? "(no Origin)" : request.origin;
|
|
679
|
+
const acao = request.headers["access-control-allow-origin"] || "(missing)";
|
|
680
|
+
const contentType = request.headers["content-type"] || "(missing)";
|
|
681
|
+
const required = request.required ? "required" : "info";
|
|
682
|
+
const cors = request.corsAllowed ? "allowed" : "blocked";
|
|
683
|
+
console.log(
|
|
684
|
+
` GET ${request.label} [${required}] Origin ${origin}: ` +
|
|
685
|
+
`${request.ok ? "ok" : "fail"} ${request.status} ` +
|
|
686
|
+
`ACAO=${acao} CORS=${cors} Content-Type=${contentType} bytes=${request.bytes}`,
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (result.previewSession?.previewEntryUrl) {
|
|
692
|
+
console.log(`- preview session: ${result.previewSession.previewEntryUrl}`);
|
|
693
|
+
}
|
|
694
|
+
if (result.error?.message) {
|
|
695
|
+
const status = result.error.status ? ` (${result.error.status})` : "";
|
|
696
|
+
console.log(`- error: ${result.error.message}${status}`);
|
|
697
|
+
}
|
|
698
|
+
if (result.browser?.skipped) {
|
|
699
|
+
console.log(`- browser loader probe: skipped (${result.browser.reason})`);
|
|
700
|
+
if (result.browser.message) console.log(` ${result.browser.message}`);
|
|
701
|
+
} else if (result.browser) {
|
|
702
|
+
console.log(`- browser loader probe: ${result.browser.ok ? "ok" : "fail"}`);
|
|
703
|
+
const loader = result.browser.result || {};
|
|
704
|
+
console.log(` HDRLoader: ${loader.hdrLoader?.ok ? "ok" : "fail"}`);
|
|
705
|
+
console.log(` GLTFLoader: ${loader.glbLoader?.ok ? "ok" : "fail"}`);
|
|
706
|
+
}
|
|
707
|
+
for (const cleanup of result.cleanup) {
|
|
708
|
+
if (cleanup.skipped) {
|
|
709
|
+
console.log(`- cleanup: skipped (${cleanup.reason})`);
|
|
710
|
+
} else {
|
|
711
|
+
console.log(`- cleanup asset #${cleanup.assetId}: ${cleanup.ok ? "ok" : "fail"}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const suffix = result.ok && !result.complete ? " (browser skipped)" : "";
|
|
715
|
+
console.log(`Result: ${result.ok ? "ok" : "fail"}${suffix}`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export function normalizePreviewUrl(value) {
|
|
719
|
+
return trimTrailingSlash(String(value || DEFAULT_PREVIEW_URL));
|
|
720
|
+
}
|