@auraindustry/aurajs 0.0.1 → 0.0.3
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/package.json +13 -1
- package/src/build-contract.mjs +708 -2
- package/src/cli.mjs +1590 -42
- package/src/conformance.mjs +1058 -87
- package/src/game-state-runtime.mjs +1067 -0
- package/src/headless-test.mjs +525 -10
- package/src/host-binary.mjs +33 -10
- package/src/react/aura-game.mjs +310 -0
- package/src/react/index.mjs +1 -0
- package/src/scaffold.mjs +267 -13
- package/src/web-api.mjs +454 -0
- package/templates/create/2d/aura.config.json +28 -0
- package/templates/create/2d/src/main.js +196 -0
- package/templates/create/3d/aura.config.json +28 -0
- package/templates/create/3d/src/main.js +306 -0
- package/templates/create/blank/aura.config.json +28 -0
- package/templates/create/blank/src/main.js +28 -0
- package/templates/create/shared/src/starter-utils/core.js +114 -0
- package/templates/create/shared/src/starter-utils/enemy-archetypes-2d.js +68 -0
- package/templates/create/shared/src/starter-utils/index.js +6 -0
- package/templates/create/shared/src/starter-utils/platformer-3d.js +101 -0
- package/templates/create/shared/src/starter-utils/wave-director.js +101 -0
- package/templates/skills/aurajs/SKILL.md +40 -0
- package/templates/skills/aurajs/api-contract-3d.md +7 -0
- package/templates/skills/aurajs/api-contract.md +7 -0
- package/templates/starter/src/main.js +48 -0
package/src/build-contract.mjs
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { extname, relative, resolve, sep } from 'node:path';
|
|
3
13
|
|
|
4
14
|
export const BUILD_MANIFEST_SCHEMA = 'aurajs.build-manifest.v1';
|
|
15
|
+
export const WEB_BUILD_MANIFEST_SCHEMA = 'aurajs.web-build-manifest.v1';
|
|
16
|
+
export const WEB_RUNTIME_CONFIG_SCHEMA = 'aurajs.web-runtime-config.v1';
|
|
5
17
|
|
|
6
18
|
export function writeBuildManifest(options = {}) {
|
|
7
19
|
const outRoot = resolve(options.outRoot || process.cwd());
|
|
@@ -45,6 +57,73 @@ export function writeBuildManifest(options = {}) {
|
|
|
45
57
|
return manifestPath;
|
|
46
58
|
}
|
|
47
59
|
|
|
60
|
+
export function writeWebBuildArtifacts(options = {}) {
|
|
61
|
+
const projectRoot = resolve(options.projectRoot || process.cwd());
|
|
62
|
+
const outRoot = resolve(options.outRoot || resolve(projectRoot, 'build', 'web'));
|
|
63
|
+
const bundlePath = resolveRequiredPath(options.bundlePath, 'bundlePath');
|
|
64
|
+
const runtimeApiVersion = Number.isInteger(options.runtimeApiVersion)
|
|
65
|
+
? options.runtimeApiVersion
|
|
66
|
+
: 1;
|
|
67
|
+
if (runtimeApiVersion <= 0) {
|
|
68
|
+
throw new Error('web build contract requires runtimeApiVersion to be a positive integer.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
mkdirSync(resolve(outRoot, 'js'), { recursive: true });
|
|
72
|
+
mkdirSync(resolve(outRoot, 'assets'), { recursive: true });
|
|
73
|
+
|
|
74
|
+
const assets = emitWebAssets({
|
|
75
|
+
projectRoot,
|
|
76
|
+
outRoot,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const runtimeConfig = buildRuntimeConfig({
|
|
80
|
+
windowConfig: options.windowConfig || {},
|
|
81
|
+
modules: options.modules || {},
|
|
82
|
+
});
|
|
83
|
+
const runtimeConfigPath = resolve(outRoot, 'runtime-config.json');
|
|
84
|
+
writeCanonicalJson(runtimeConfigPath, runtimeConfig);
|
|
85
|
+
|
|
86
|
+
const loaderPath = resolve(outRoot, 'js', 'aura-web-loader.js');
|
|
87
|
+
writeFileSync(loaderPath, `${WEB_LOADER_SOURCE}\n`, 'utf8');
|
|
88
|
+
|
|
89
|
+
const indexPath = resolve(outRoot, 'index.html');
|
|
90
|
+
writeFileSync(indexPath, `${WEB_INDEX_HTML}\n`, 'utf8');
|
|
91
|
+
|
|
92
|
+
const manifestBody = {
|
|
93
|
+
schema: WEB_BUILD_MANIFEST_SCHEMA,
|
|
94
|
+
schemaVersion: '1.0.0',
|
|
95
|
+
buildTarget: 'web',
|
|
96
|
+
runtimeApiVersion,
|
|
97
|
+
entrypoints: {
|
|
98
|
+
html: toPosix(relative(outRoot, indexPath)),
|
|
99
|
+
loader: toPosix(relative(outRoot, loaderPath)),
|
|
100
|
+
bundle: toPosix(relative(outRoot, bundlePath)),
|
|
101
|
+
runtimeConfig: toPosix(relative(outRoot, runtimeConfigPath)),
|
|
102
|
+
},
|
|
103
|
+
assets: assets.entries,
|
|
104
|
+
};
|
|
105
|
+
const manifestHash = sha256(Buffer.from(JSON.stringify(manifestBody)));
|
|
106
|
+
const manifest = {
|
|
107
|
+
...manifestBody,
|
|
108
|
+
integrity: {
|
|
109
|
+
manifestSha256: manifestHash,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const manifestPath = resolve(outRoot, 'web-build-manifest.json');
|
|
114
|
+
writeCanonicalJson(manifestPath, manifest);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
outRoot,
|
|
118
|
+
manifestPath,
|
|
119
|
+
runtimeConfigPath,
|
|
120
|
+
loaderPath,
|
|
121
|
+
indexPath,
|
|
122
|
+
assetCount: assets.entries.length,
|
|
123
|
+
assetsRoot: assets.assetsOutRoot,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
48
127
|
function resolveRequiredPath(pathLike, fieldName) {
|
|
49
128
|
if (typeof pathLike !== 'string' || pathLike.length === 0) {
|
|
50
129
|
throw new Error(`build contract requires a non-empty "${fieldName}" path.`);
|
|
@@ -69,3 +148,630 @@ function resolveOptionalRelative(outRoot, pathLike) {
|
|
|
69
148
|
}
|
|
70
149
|
return toPosix(relative(outRoot, resolve(pathLike)));
|
|
71
150
|
}
|
|
151
|
+
|
|
152
|
+
function buildRuntimeConfig(options = {}) {
|
|
153
|
+
const windowConfig = options.windowConfig && typeof options.windowConfig === 'object'
|
|
154
|
+
? options.windowConfig
|
|
155
|
+
: {};
|
|
156
|
+
const modules = options.modules && typeof options.modules === 'object'
|
|
157
|
+
? options.modules
|
|
158
|
+
: {};
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
schema: WEB_RUNTIME_CONFIG_SCHEMA,
|
|
162
|
+
schemaVersion: '1.0.0',
|
|
163
|
+
canvas: {
|
|
164
|
+
id: 'aura-canvas',
|
|
165
|
+
resizeMode: 'fit-container',
|
|
166
|
+
width: normalizePositiveInt(windowConfig.width, 1280),
|
|
167
|
+
height: normalizePositiveInt(windowConfig.height, 720),
|
|
168
|
+
devicePixelRatioMode: 'clamped',
|
|
169
|
+
},
|
|
170
|
+
loop: {
|
|
171
|
+
tickMode: 'raf',
|
|
172
|
+
maxDeltaMs: 50,
|
|
173
|
+
},
|
|
174
|
+
modules: {
|
|
175
|
+
physics: modules.physics === true,
|
|
176
|
+
network: modules.network === true,
|
|
177
|
+
multiplayer: modules.multiplayer === true,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function emitWebAssets({ projectRoot, outRoot }) {
|
|
183
|
+
const sourceRoot = resolve(projectRoot, 'assets');
|
|
184
|
+
const assetsOutRoot = resolve(outRoot, 'assets');
|
|
185
|
+
rmSync(assetsOutRoot, { recursive: true, force: true });
|
|
186
|
+
mkdirSync(assetsOutRoot, { recursive: true });
|
|
187
|
+
|
|
188
|
+
const sourceFiles = collectFiles(sourceRoot);
|
|
189
|
+
const entries = [];
|
|
190
|
+
for (const sourcePath of sourceFiles) {
|
|
191
|
+
const sourceRelative = toPosix(relative(sourceRoot, sourcePath));
|
|
192
|
+
const sourceBytes = readFileSync(sourcePath);
|
|
193
|
+
const contentHash = sha256(sourceBytes);
|
|
194
|
+
const relativeHash = sha256(Buffer.from(sourceRelative, 'utf8')).slice(0, 8);
|
|
195
|
+
const extension = extname(sourceRelative).toLowerCase();
|
|
196
|
+
const stemSource = extension.length > 0
|
|
197
|
+
? sourceRelative.slice(0, sourceRelative.length - extension.length)
|
|
198
|
+
: sourceRelative;
|
|
199
|
+
const stem = sanitizeAssetStem(stemSource);
|
|
200
|
+
const outputName = `${stem}.${relativeHash}.${contentHash.slice(0, 12)}${extension}`;
|
|
201
|
+
const outputPath = resolve(assetsOutRoot, outputName);
|
|
202
|
+
|
|
203
|
+
copyFileSync(sourcePath, outputPath);
|
|
204
|
+
entries.push({
|
|
205
|
+
path: `assets/${outputName}`,
|
|
206
|
+
bytes: sourceBytes.length,
|
|
207
|
+
sha256: contentHash,
|
|
208
|
+
mediaType: mediaTypeForExtension(extension),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
213
|
+
removeUnexpectedFiles(assetsOutRoot, new Set(entries.map((entry) => entry.path.slice('assets/'.length))));
|
|
214
|
+
return { entries, assetsOutRoot };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function collectFiles(rootPath) {
|
|
218
|
+
if (!existsSync(rootPath)) {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
const output = [];
|
|
222
|
+
walkFiles(rootPath, output);
|
|
223
|
+
output.sort((a, b) => toPosix(a).localeCompare(toPosix(b)));
|
|
224
|
+
return output;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function walkFiles(dirPath, output) {
|
|
228
|
+
const names = readdirSync(dirPath).sort((a, b) => a.localeCompare(b));
|
|
229
|
+
for (const name of names) {
|
|
230
|
+
const fullPath = resolve(dirPath, name);
|
|
231
|
+
const stat = statSync(fullPath);
|
|
232
|
+
if (stat.isDirectory()) {
|
|
233
|
+
walkFiles(fullPath, output);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
output.push(fullPath);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function sanitizeAssetStem(value) {
|
|
241
|
+
const normalized = String(value || '')
|
|
242
|
+
.replace(/[\\/]+/g, '-')
|
|
243
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
244
|
+
.replace(/-+/g, '-')
|
|
245
|
+
.replace(/^[-._]+|[-._]+$/g, '');
|
|
246
|
+
return normalized.length > 0 ? normalized : 'asset';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function mediaTypeForExtension(extension) {
|
|
250
|
+
switch (extension) {
|
|
251
|
+
case '.png':
|
|
252
|
+
return 'image/png';
|
|
253
|
+
case '.jpg':
|
|
254
|
+
case '.jpeg':
|
|
255
|
+
return 'image/jpeg';
|
|
256
|
+
case '.gif':
|
|
257
|
+
return 'image/gif';
|
|
258
|
+
case '.webp':
|
|
259
|
+
return 'image/webp';
|
|
260
|
+
case '.wav':
|
|
261
|
+
return 'audio/wav';
|
|
262
|
+
case '.ogg':
|
|
263
|
+
return 'audio/ogg';
|
|
264
|
+
case '.mp3':
|
|
265
|
+
return 'audio/mpeg';
|
|
266
|
+
case '.json':
|
|
267
|
+
return 'application/json';
|
|
268
|
+
case '.txt':
|
|
269
|
+
case '.md':
|
|
270
|
+
case '.csv':
|
|
271
|
+
return 'text/plain; charset=utf-8';
|
|
272
|
+
default:
|
|
273
|
+
return 'application/octet-stream';
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizePositiveInt(value, fallback) {
|
|
278
|
+
if (!Number.isFinite(value)) {
|
|
279
|
+
return fallback;
|
|
280
|
+
}
|
|
281
|
+
const normalized = Math.floor(value);
|
|
282
|
+
return normalized > 0 ? normalized : fallback;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function writeCanonicalJson(path, value) {
|
|
286
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function removeUnexpectedFiles(rootPath, allowedNames) {
|
|
290
|
+
if (!existsSync(rootPath)) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
for (const name of readdirSync(rootPath)) {
|
|
294
|
+
if (allowedNames.has(name)) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
rmSync(resolve(rootPath, name), { recursive: true, force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function sha256(value) {
|
|
302
|
+
return createHash('sha256').update(value).digest('hex');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const WEB_INDEX_HTML = [
|
|
306
|
+
'<!doctype html>',
|
|
307
|
+
'<html lang="en">',
|
|
308
|
+
'<head>',
|
|
309
|
+
' <meta charset="utf-8" />',
|
|
310
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
311
|
+
' <title>AuraJS Web Build</title>',
|
|
312
|
+
'</head>',
|
|
313
|
+
'<body>',
|
|
314
|
+
' <div id="aura-root">',
|
|
315
|
+
' <canvas id="aura-canvas"></canvas>',
|
|
316
|
+
' </div>',
|
|
317
|
+
' <script src="./js/aura-web-loader.js"></script>',
|
|
318
|
+
'</body>',
|
|
319
|
+
'</html>',
|
|
320
|
+
].join('\n');
|
|
321
|
+
|
|
322
|
+
const WEB_LOADER_SOURCE = `
|
|
323
|
+
(function (globalRef) {
|
|
324
|
+
const state = {
|
|
325
|
+
phase: 'idle',
|
|
326
|
+
reasonCode: null,
|
|
327
|
+
lastError: null,
|
|
328
|
+
lifecycle: {
|
|
329
|
+
running: false,
|
|
330
|
+
setupCalls: 0,
|
|
331
|
+
updateCalls: 0,
|
|
332
|
+
drawCalls: 0,
|
|
333
|
+
frameCount: 0,
|
|
334
|
+
lastDeltaSeconds: 0
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
let cachedManifest = null;
|
|
338
|
+
let cachedRuntimeConfig = null;
|
|
339
|
+
let mountedRuntime = null;
|
|
340
|
+
|
|
341
|
+
function normalizeCount(value) {
|
|
342
|
+
const numeric = Number(value);
|
|
343
|
+
if (!Number.isFinite(numeric) || numeric < 0) return 0;
|
|
344
|
+
return Math.floor(numeric);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function normalizeDeltaSeconds(value) {
|
|
348
|
+
const numeric = Number(value);
|
|
349
|
+
if (!Number.isFinite(numeric) || numeric < 0) return 0;
|
|
350
|
+
return Number(numeric.toFixed(6));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function cloneLifecycle(lifecycle) {
|
|
354
|
+
const source = lifecycle && typeof lifecycle === 'object' ? lifecycle : {};
|
|
355
|
+
return {
|
|
356
|
+
running: source.running === true,
|
|
357
|
+
setupCalls: normalizeCount(source.setupCalls),
|
|
358
|
+
updateCalls: normalizeCount(source.updateCalls),
|
|
359
|
+
drawCalls: normalizeCount(source.drawCalls),
|
|
360
|
+
frameCount: normalizeCount(source.frameCount),
|
|
361
|
+
lastDeltaSeconds: normalizeDeltaSeconds(source.lastDeltaSeconds)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function setState(phase, reasonCode, error) {
|
|
366
|
+
state.phase = phase;
|
|
367
|
+
state.reasonCode = reasonCode || null;
|
|
368
|
+
state.lastError = error || null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function createError(reasonCode, message, layer, retryable, details) {
|
|
372
|
+
const error = new Error(message);
|
|
373
|
+
error.ok = false;
|
|
374
|
+
error.reasonCode = reasonCode;
|
|
375
|
+
error.layer = layer;
|
|
376
|
+
error.retryable = Boolean(retryable);
|
|
377
|
+
error.details = details || {};
|
|
378
|
+
return error;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function normalizeError(error, fallbackReasonCode, fallbackMessage, fallbackLayer, fallbackRetryable) {
|
|
382
|
+
if (error && typeof error === 'object' && typeof error.reasonCode === 'string') {
|
|
383
|
+
return error;
|
|
384
|
+
}
|
|
385
|
+
return createError(
|
|
386
|
+
fallbackReasonCode,
|
|
387
|
+
fallbackMessage,
|
|
388
|
+
fallbackLayer,
|
|
389
|
+
fallbackRetryable,
|
|
390
|
+
{ cause: String(error && error.message ? error.message : error) }
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function captureFailure(fallback, operation) {
|
|
395
|
+
try {
|
|
396
|
+
return await operation();
|
|
397
|
+
} catch (error) {
|
|
398
|
+
const normalized = normalizeError(
|
|
399
|
+
error,
|
|
400
|
+
fallback.reasonCode,
|
|
401
|
+
fallback.message,
|
|
402
|
+
fallback.layer,
|
|
403
|
+
fallback.retryable
|
|
404
|
+
);
|
|
405
|
+
setState('error', normalized.reasonCode, {
|
|
406
|
+
reasonCode: normalized.reasonCode,
|
|
407
|
+
layer: normalized.layer || fallback.layer,
|
|
408
|
+
retryable: normalized.retryable === true,
|
|
409
|
+
details: normalized.details || {}
|
|
410
|
+
});
|
|
411
|
+
throw normalized;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function readJson(path, missingCode, parseCode) {
|
|
416
|
+
let response;
|
|
417
|
+
try {
|
|
418
|
+
response = await fetch(path, { cache: 'no-store' });
|
|
419
|
+
} catch (error) {
|
|
420
|
+
throw createError(missingCode, 'Failed to fetch ' + path + '.', 'loader', false, { cause: String(error) });
|
|
421
|
+
}
|
|
422
|
+
if (!response || response.ok !== true) {
|
|
423
|
+
throw createError(missingCode, path + ' not found.', 'loader', false, { status: response ? response.status : null });
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
return await response.json();
|
|
427
|
+
} catch (error) {
|
|
428
|
+
throw createError(parseCode, path + ' is not valid JSON.', 'loader', false, { cause: String(error) });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function normalizePath(path) {
|
|
433
|
+
if (typeof path !== 'string') return '';
|
|
434
|
+
return path.replace(/\\\\/g, '/').replace(/^\\.\\//, '');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function resolveTarget(target) {
|
|
438
|
+
if (typeof target === 'string') {
|
|
439
|
+
return document.querySelector(target);
|
|
440
|
+
}
|
|
441
|
+
if (target && typeof target.appendChild === 'function') {
|
|
442
|
+
return target;
|
|
443
|
+
}
|
|
444
|
+
return document.getElementById('aura-root') || document.body;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function ensureManifestValid(manifest) {
|
|
448
|
+
if (!manifest || manifest.schema !== 'aurajs.web-build-manifest.v1') {
|
|
449
|
+
throw createError(
|
|
450
|
+
'web_manifest_schema_unsupported',
|
|
451
|
+
'Manifest schema major version is unsupported.',
|
|
452
|
+
'loader',
|
|
453
|
+
false,
|
|
454
|
+
{ schema: manifest && manifest.schema }
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (!manifest.entrypoints || typeof manifest.entrypoints.bundle !== 'string' || manifest.entrypoints.bundle.length === 0) {
|
|
458
|
+
throw createError('web_manifest_validation_failed', 'Manifest bundle entrypoint is missing.', 'loader', false, {});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function ensureRuntimeConfigValid(runtimeConfig) {
|
|
463
|
+
if (!runtimeConfig || runtimeConfig.schema !== 'aurajs.web-runtime-config.v1') {
|
|
464
|
+
throw createError(
|
|
465
|
+
'web_runtime_config_validation_failed',
|
|
466
|
+
'Runtime config schema major version is unsupported.',
|
|
467
|
+
'loader',
|
|
468
|
+
false,
|
|
469
|
+
{ schema: runtimeConfig && runtimeConfig.schema }
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function loadScript(path) {
|
|
475
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
476
|
+
const script = document.createElement('script');
|
|
477
|
+
script.src = path;
|
|
478
|
+
script.async = false;
|
|
479
|
+
script.onload = function () { resolvePromise(); };
|
|
480
|
+
script.onerror = function () {
|
|
481
|
+
rejectPromise(createError('web_bundle_load_failed', 'Failed to load ' + path + '.', 'loader', true, { path }));
|
|
482
|
+
};
|
|
483
|
+
document.head.appendChild(script);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function normalizePositiveNumber(value, fallback) {
|
|
488
|
+
const numeric = Number(value);
|
|
489
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return fallback;
|
|
490
|
+
return numeric;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function createDeterministicBootstrap() {
|
|
494
|
+
return async function bootstrap(input) {
|
|
495
|
+
const auraRef = globalRef.aura && typeof globalRef.aura === 'object'
|
|
496
|
+
? globalRef.aura
|
|
497
|
+
: (globalRef.aura = {});
|
|
498
|
+
const runtimeConfig = input && typeof input.runtimeConfig === 'object' && input.runtimeConfig
|
|
499
|
+
? input.runtimeConfig
|
|
500
|
+
: {};
|
|
501
|
+
const loopConfig = runtimeConfig.loop && typeof runtimeConfig.loop === 'object'
|
|
502
|
+
? runtimeConfig.loop
|
|
503
|
+
: {};
|
|
504
|
+
const maxDeltaSeconds = normalizePositiveNumber(loopConfig.maxDeltaMs, 50) / 1000;
|
|
505
|
+
const fixedDeltaSeconds = Math.min(maxDeltaSeconds, 1 / 60);
|
|
506
|
+
const requestFrame = typeof globalRef.requestAnimationFrame === 'function'
|
|
507
|
+
? globalRef.requestAnimationFrame.bind(globalRef)
|
|
508
|
+
: function (callback) {
|
|
509
|
+
return globalRef.setTimeout(function () {
|
|
510
|
+
callback(Date.now());
|
|
511
|
+
}, 16);
|
|
512
|
+
};
|
|
513
|
+
const cancelFrame = typeof globalRef.cancelAnimationFrame === 'function'
|
|
514
|
+
? globalRef.cancelAnimationFrame.bind(globalRef)
|
|
515
|
+
: function (handle) {
|
|
516
|
+
globalRef.clearTimeout(handle);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const runtimeState = {
|
|
520
|
+
disposed: false,
|
|
521
|
+
running: false,
|
|
522
|
+
setupCalls: 0,
|
|
523
|
+
updateCalls: 0,
|
|
524
|
+
drawCalls: 0,
|
|
525
|
+
frameCount: 0,
|
|
526
|
+
lastDeltaSeconds: 0,
|
|
527
|
+
rafHandle: null
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
function getLifecycle() {
|
|
531
|
+
return {
|
|
532
|
+
running: runtimeState.running && runtimeState.disposed === false,
|
|
533
|
+
setupCalls: runtimeState.setupCalls,
|
|
534
|
+
updateCalls: runtimeState.updateCalls,
|
|
535
|
+
drawCalls: runtimeState.drawCalls,
|
|
536
|
+
frameCount: runtimeState.frameCount,
|
|
537
|
+
lastDeltaSeconds: runtimeState.lastDeltaSeconds
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function syncLifecycle() {
|
|
542
|
+
state.lifecycle = cloneLifecycle(getLifecycle());
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
if (typeof auraRef.setup === 'function') {
|
|
547
|
+
const setupResult = auraRef.setup();
|
|
548
|
+
if (setupResult && typeof setupResult.then === 'function') {
|
|
549
|
+
await setupResult;
|
|
550
|
+
}
|
|
551
|
+
runtimeState.setupCalls += 1;
|
|
552
|
+
}
|
|
553
|
+
} catch (error) {
|
|
554
|
+
throw createError(
|
|
555
|
+
'web_runtime_bootstrap_failed',
|
|
556
|
+
'Runtime setup failed.',
|
|
557
|
+
'runtime',
|
|
558
|
+
true,
|
|
559
|
+
{ stage: 'setup', cause: String(error && error.message ? error.message : error) }
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
runtimeState.running = true;
|
|
564
|
+
syncLifecycle();
|
|
565
|
+
|
|
566
|
+
function step() {
|
|
567
|
+
if (runtimeState.running === false || runtimeState.disposed === true) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
runtimeState.frameCount += 1;
|
|
571
|
+
runtimeState.lastDeltaSeconds = fixedDeltaSeconds;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
if (typeof auraRef.update === 'function') {
|
|
575
|
+
auraRef.update(fixedDeltaSeconds);
|
|
576
|
+
runtimeState.updateCalls += 1;
|
|
577
|
+
}
|
|
578
|
+
if (typeof auraRef.draw === 'function') {
|
|
579
|
+
auraRef.draw();
|
|
580
|
+
runtimeState.drawCalls += 1;
|
|
581
|
+
}
|
|
582
|
+
} catch (error) {
|
|
583
|
+
runtimeState.running = false;
|
|
584
|
+
syncLifecycle();
|
|
585
|
+
const normalized = normalizeError(
|
|
586
|
+
error,
|
|
587
|
+
'web_runtime_bootstrap_failed',
|
|
588
|
+
'Runtime lifecycle callback failed.',
|
|
589
|
+
'runtime',
|
|
590
|
+
true
|
|
591
|
+
);
|
|
592
|
+
setState('error', normalized.reasonCode, {
|
|
593
|
+
reasonCode: normalized.reasonCode,
|
|
594
|
+
layer: normalized.layer || 'runtime',
|
|
595
|
+
retryable: normalized.retryable === true,
|
|
596
|
+
details: normalized.details || {}
|
|
597
|
+
});
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
syncLifecycle();
|
|
602
|
+
runtimeState.rafHandle = requestFrame(step);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
runtimeState.rafHandle = requestFrame(step);
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
getLifecycle,
|
|
609
|
+
async unmount() {
|
|
610
|
+
if (runtimeState.disposed === true) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
runtimeState.disposed = true;
|
|
614
|
+
runtimeState.running = false;
|
|
615
|
+
if (runtimeState.rafHandle != null) {
|
|
616
|
+
cancelFrame(runtimeState.rafHandle);
|
|
617
|
+
runtimeState.rafHandle = null;
|
|
618
|
+
}
|
|
619
|
+
syncLifecycle();
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function resolveBootstrap() {
|
|
627
|
+
if (typeof globalRef.__AURA_WEB_BOOTSTRAP__ === 'function') {
|
|
628
|
+
return globalRef.__AURA_WEB_BOOTSTRAP__;
|
|
629
|
+
}
|
|
630
|
+
return createDeterministicBootstrap();
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function getLifecycleSnapshot() {
|
|
634
|
+
if (mountedRuntime && typeof mountedRuntime.getLifecycle === 'function') {
|
|
635
|
+
try {
|
|
636
|
+
return cloneLifecycle(mountedRuntime.getLifecycle());
|
|
637
|
+
} catch (_) {
|
|
638
|
+
return cloneLifecycle(state.lifecycle);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return cloneLifecycle(state.lifecycle);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const loader = {
|
|
645
|
+
async load(options) {
|
|
646
|
+
return captureFailure(
|
|
647
|
+
{
|
|
648
|
+
reasonCode: 'web_loader_load_failed',
|
|
649
|
+
message: 'Loader failed to initialize web runtime assets.',
|
|
650
|
+
layer: 'loader',
|
|
651
|
+
retryable: true
|
|
652
|
+
},
|
|
653
|
+
async function () {
|
|
654
|
+
const opts = options || {};
|
|
655
|
+
const rootUrl = typeof opts.rootUrl === 'string' && opts.rootUrl.length > 0
|
|
656
|
+
? opts.rootUrl.replace(/\\/$/, '')
|
|
657
|
+
: '.';
|
|
658
|
+
setState('loading', null, null);
|
|
659
|
+
|
|
660
|
+
cachedManifest = await readJson(rootUrl + '/web-build-manifest.json', 'web_manifest_missing', 'web_manifest_parse_failed');
|
|
661
|
+
cachedRuntimeConfig = await readJson(rootUrl + '/runtime-config.json', 'web_runtime_config_missing', 'web_runtime_config_parse_failed');
|
|
662
|
+
ensureManifestValid(cachedManifest);
|
|
663
|
+
ensureRuntimeConfigValid(cachedRuntimeConfig);
|
|
664
|
+
|
|
665
|
+
const bundleEntry = normalizePath(cachedManifest.entrypoints.bundle);
|
|
666
|
+
if (bundleEntry.length === 0) {
|
|
667
|
+
throw createError('web_entrypoint_missing', 'Bundle entrypoint is empty.', 'loader', false, {});
|
|
668
|
+
}
|
|
669
|
+
await loadScript(rootUrl + '/' + bundleEntry);
|
|
670
|
+
setState('loaded', null, null);
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
);
|
|
674
|
+
},
|
|
675
|
+
async mount(target, options) {
|
|
676
|
+
return captureFailure(
|
|
677
|
+
{
|
|
678
|
+
reasonCode: 'web_loader_mount_failed',
|
|
679
|
+
message: 'Loader mount sequence failed.',
|
|
680
|
+
layer: 'loader',
|
|
681
|
+
retryable: true
|
|
682
|
+
},
|
|
683
|
+
async function () {
|
|
684
|
+
if (!cachedManifest || !cachedRuntimeConfig) {
|
|
685
|
+
await loader.load(options || {});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (mountedRuntime && typeof mountedRuntime.unmount === 'function') {
|
|
689
|
+
await mountedRuntime.unmount();
|
|
690
|
+
mountedRuntime = null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
setState('mounting', null, null);
|
|
694
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
695
|
+
const targetNode = resolveTarget(target == null ? opts.mount : target);
|
|
696
|
+
if (!targetNode) {
|
|
697
|
+
throw createError('web_loader_mount_target_invalid', 'Mount target was not found.', 'loader', false, {});
|
|
698
|
+
}
|
|
699
|
+
const canvas = targetNode.querySelector && targetNode.querySelector('canvas')
|
|
700
|
+
? targetNode.querySelector('canvas')
|
|
701
|
+
: document.getElementById('aura-canvas');
|
|
702
|
+
if (!canvas) {
|
|
703
|
+
throw createError('web_loader_mount_failed', 'Canvas target was not found.', 'loader', true, {});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const bootstrap = resolveBootstrap();
|
|
707
|
+
if (typeof bootstrap !== 'function') {
|
|
708
|
+
throw createError('web_runtime_bootstrap_missing', 'Bundle did not expose required bootstrap function.', 'runtime', false, {});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
let runtimeResult;
|
|
712
|
+
try {
|
|
713
|
+
runtimeResult = await bootstrap({
|
|
714
|
+
canvas: canvas,
|
|
715
|
+
runtimeConfig: cachedRuntimeConfig || {},
|
|
716
|
+
manifest: cachedManifest || {}
|
|
717
|
+
});
|
|
718
|
+
} catch (error) {
|
|
719
|
+
throw normalizeError(
|
|
720
|
+
error,
|
|
721
|
+
'web_runtime_bootstrap_failed',
|
|
722
|
+
'Runtime bootstrap threw an exception.',
|
|
723
|
+
'runtime',
|
|
724
|
+
true
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (!runtimeResult || typeof runtimeResult !== 'object') {
|
|
729
|
+
throw createError('web_runtime_bootstrap_failed', 'Runtime bootstrap returned invalid payload.', 'runtime', false, {});
|
|
730
|
+
}
|
|
731
|
+
if (typeof runtimeResult.unmount !== 'function') {
|
|
732
|
+
runtimeResult.unmount = async function () {
|
|
733
|
+
return true;
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
mountedRuntime = runtimeResult;
|
|
738
|
+
state.lifecycle = getLifecycleSnapshot();
|
|
739
|
+
setState('mounted', null, null);
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
},
|
|
744
|
+
async unmount() {
|
|
745
|
+
return captureFailure(
|
|
746
|
+
{
|
|
747
|
+
reasonCode: 'web_loader_unmount_failed',
|
|
748
|
+
message: 'Loader unmount sequence failed.',
|
|
749
|
+
layer: 'loader',
|
|
750
|
+
retryable: true
|
|
751
|
+
},
|
|
752
|
+
async function () {
|
|
753
|
+
setState('unmounting', null, null);
|
|
754
|
+
if (mountedRuntime && typeof mountedRuntime.unmount === 'function') {
|
|
755
|
+
await mountedRuntime.unmount();
|
|
756
|
+
}
|
|
757
|
+
mountedRuntime = null;
|
|
758
|
+
state.lifecycle = cloneLifecycle(state.lifecycle);
|
|
759
|
+
state.lifecycle.running = false;
|
|
760
|
+
setState('unmounted', null, null);
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
);
|
|
764
|
+
},
|
|
765
|
+
getState() {
|
|
766
|
+
return {
|
|
767
|
+
phase: state.phase,
|
|
768
|
+
reasonCode: state.reasonCode,
|
|
769
|
+
lastError: state.lastError ? { ...state.lastError } : null,
|
|
770
|
+
lifecycle: getLifecycleSnapshot()
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
globalRef.AuraWebLoader = loader;
|
|
776
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
777
|
+
`.trim();
|