@auraindustry/aurajs 0.0.1
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/benchmarks/perf-thresholds.json +102 -0
- package/package.json +47 -0
- package/src/.gitkeep +0 -0
- package/src/asset-pack.mjs +316 -0
- package/src/build-contract.mjs +71 -0
- package/src/bundler.mjs +698 -0
- package/src/cli.mjs +764 -0
- package/src/config.mjs +460 -0
- package/src/conformance-runner.mjs +322 -0
- package/src/conformance.mjs +5407 -0
- package/src/headless-test.mjs +4314 -0
- package/src/host-binary.mjs +423 -0
- package/src/perf-benchmark-runner.mjs +233 -0
- package/src/perf-benchmark.mjs +549 -0
- package/src/postinstall.mjs +26 -0
- package/src/scaffold.mjs +74 -0
- package/templates/starter/aura.config.json +28 -0
- package/templates/starter/src/main.js +226 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { basename, extname, resolve } from 'node:path';
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
+
import { readFileSync, copyFileSync, chmodSync, rmSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
6
|
+
import {
|
|
7
|
+
bundleProject,
|
|
8
|
+
createIncrementalBundler,
|
|
9
|
+
formatBundleError,
|
|
10
|
+
isBundleError,
|
|
11
|
+
} from './bundler.mjs';
|
|
12
|
+
import { scaffold } from './scaffold.mjs';
|
|
13
|
+
import { packageAssets } from './asset-pack.mjs';
|
|
14
|
+
import { writeBuildManifest } from './build-contract.mjs';
|
|
15
|
+
import { parseTestArgs, runHeadlessTest, HeadlessTestError } from './headless-test.mjs';
|
|
16
|
+
import { loadConfig, ConfigError } from './config.mjs';
|
|
17
|
+
import { resolveAndCacheHostBinary, HostBinaryResolutionError } from './host-binary.mjs';
|
|
18
|
+
import { runConformanceCommand } from './conformance-runner.mjs';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Version
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const PKG_PATH = new URL('../package.json', import.meta.url);
|
|
25
|
+
const VERSION = JSON.parse(readFileSync(PKG_PATH, 'utf8')).version;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Command registry
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
const COMMANDS = {
|
|
32
|
+
init: { description: 'Scaffold a new AuraJS project', usage: 'aura init <name>' },
|
|
33
|
+
dev: { description: 'Run with hot-reload and dev tools', usage: 'aura dev' },
|
|
34
|
+
build: {
|
|
35
|
+
description: 'Build a release binary (current host platform in v1)',
|
|
36
|
+
usage: 'aura build [--target <windows|mac|linux|all>] [--asset-mode <embed|sibling>]',
|
|
37
|
+
details: [
|
|
38
|
+
' Options:',
|
|
39
|
+
' --target <windows|mac|linux|all> Target label; current host only in v1',
|
|
40
|
+
' --asset-mode <embed|sibling> Package assets in pak or sibling assets/',
|
|
41
|
+
'',
|
|
42
|
+
' Notes:',
|
|
43
|
+
' - In v1, `aura build` always emits the current host platform output.',
|
|
44
|
+
' - `--target all` keeps current host output; use CI matrix for all platforms.',
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
run: { description: 'Build and run the game', usage: 'aura run' },
|
|
48
|
+
clean: { description: 'Delete build artifacts', usage: 'aura clean' },
|
|
49
|
+
test: { description: 'Run game logic in headless mode', usage: 'aura test [file] [--width N] [--height N] [--frames N]' },
|
|
50
|
+
conformance: { description: 'Run API conformance suites in headless mode', usage: 'aura conformance [--mode shim|native|both] [--json]' },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const SUPPORTED_BUILD_TARGETS = new Set(['windows', 'mac', 'linux', 'all']);
|
|
54
|
+
|
|
55
|
+
class BuildStageError extends Error {
|
|
56
|
+
constructor(stage, cause) {
|
|
57
|
+
super(`Build stage "${stage}" failed: ${cause?.message || String(cause)}`);
|
|
58
|
+
this.name = 'BuildStageError';
|
|
59
|
+
this.stage = stage;
|
|
60
|
+
this.cause = cause;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Help
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
function printHelp() {
|
|
69
|
+
const lines = [
|
|
70
|
+
'',
|
|
71
|
+
` aurajs v${VERSION}`,
|
|
72
|
+
' Write games in JavaScript, build native binaries.',
|
|
73
|
+
'',
|
|
74
|
+
' Usage: aura <command> [options]',
|
|
75
|
+
'',
|
|
76
|
+
' Commands:',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const maxCmd = Math.max(...Object.keys(COMMANDS).map((c) => c.length));
|
|
80
|
+
for (const [name, cmd] of Object.entries(COMMANDS)) {
|
|
81
|
+
lines.push(` ${name.padEnd(maxCmd + 2)}${cmd.description}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
lines.push('');
|
|
85
|
+
lines.push(' Options:');
|
|
86
|
+
lines.push(' --help, -h Show this help message');
|
|
87
|
+
lines.push(' --version, -v Show version number');
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push(' Run "aura <command> --help" for command-specific help.');
|
|
90
|
+
lines.push('');
|
|
91
|
+
|
|
92
|
+
console.log(lines.join('\n'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function printCommandHelp(name) {
|
|
96
|
+
const cmd = COMMANDS[name];
|
|
97
|
+
if (!cmd) {
|
|
98
|
+
error(`Unknown command: ${name}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const lines = [
|
|
102
|
+
'',
|
|
103
|
+
` ${cmd.usage}`,
|
|
104
|
+
'',
|
|
105
|
+
` ${cmd.description}`,
|
|
106
|
+
'',
|
|
107
|
+
];
|
|
108
|
+
if (Array.isArray(cmd.details) && cmd.details.length > 0) {
|
|
109
|
+
lines.push(...cmd.details);
|
|
110
|
+
lines.push('');
|
|
111
|
+
}
|
|
112
|
+
console.log(lines.join('\n'));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Error handling
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
function error(message, exitCode = 1) {
|
|
120
|
+
console.error(`\n aura error: ${message}\n`);
|
|
121
|
+
process.exit(exitCode);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseTarget(args) {
|
|
125
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
126
|
+
if (args[i] === '--target' && args[i + 1]) {
|
|
127
|
+
const value = args[i + 1].toLowerCase();
|
|
128
|
+
if (value === 'macos' || value === 'darwin') return 'mac';
|
|
129
|
+
if (value === 'win' || value === 'win32') return 'windows';
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function currentPlatformTarget() {
|
|
137
|
+
if (process.platform === 'win32') return 'windows';
|
|
138
|
+
if (process.platform === 'darwin') return 'mac';
|
|
139
|
+
return 'linux';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseAssetMode(args) {
|
|
143
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
144
|
+
if (args[i] === '--asset-mode' && args[i + 1]) {
|
|
145
|
+
return args[i + 1].toLowerCase();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return 'embed';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function encodeOptionalModulesEnv(modules) {
|
|
152
|
+
if (!modules || typeof modules !== 'object') {
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
const enabled = [];
|
|
156
|
+
if (modules.physics === true) enabled.push('physics');
|
|
157
|
+
if (modules.network === true) enabled.push('net');
|
|
158
|
+
if (modules.multiplayer === true) enabled.push('multiplayer');
|
|
159
|
+
if (modules.steam === true) enabled.push('steam');
|
|
160
|
+
return enabled.join(',');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function runBuildStage(stage, fn) {
|
|
164
|
+
try {
|
|
165
|
+
return fn();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
throw new BuildStageError(stage, err);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function sanitizeExecutableBaseName(value) {
|
|
172
|
+
const normalized = String(value || '').trim();
|
|
173
|
+
const compact = normalized
|
|
174
|
+
.replace(/\s+/g, '-')
|
|
175
|
+
.replace(/[^A-Za-z0-9._-]/g, '-')
|
|
176
|
+
.replace(/-+/g, '-')
|
|
177
|
+
.replace(/^[-._]+|[-._]+$/g, '');
|
|
178
|
+
if (compact.length === 0) {
|
|
179
|
+
throw new Error(`Unable to derive a safe executable name from identity.executable="${value}".`);
|
|
180
|
+
}
|
|
181
|
+
return compact;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function resolveBuildIdentity(config) {
|
|
185
|
+
const identity = config?.identity || {};
|
|
186
|
+
const name = typeof identity.name === 'string' && identity.name.trim().length > 0
|
|
187
|
+
? identity.name.trim()
|
|
188
|
+
: 'AuraJS Game';
|
|
189
|
+
const version = typeof identity.version === 'string' && identity.version.trim().length > 0
|
|
190
|
+
? identity.version.trim()
|
|
191
|
+
: '0.1.0';
|
|
192
|
+
const executableBaseName = sanitizeExecutableBaseName(identity.executable || 'game');
|
|
193
|
+
const iconPath = typeof identity.icon === 'string' && identity.icon.trim().length > 0
|
|
194
|
+
? identity.icon.trim()
|
|
195
|
+
: null;
|
|
196
|
+
return {
|
|
197
|
+
name,
|
|
198
|
+
version,
|
|
199
|
+
executableBaseName,
|
|
200
|
+
iconPath,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resolveBuildIcon({ projectRoot, targetOutRoot, iconPath }) {
|
|
205
|
+
if (!iconPath) {
|
|
206
|
+
return {
|
|
207
|
+
configuredPath: null,
|
|
208
|
+
sourcePath: null,
|
|
209
|
+
outputPath: null,
|
|
210
|
+
status: 'not_configured',
|
|
211
|
+
reasonCode: 'icon_not_configured',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const sourcePath = resolve(projectRoot, iconPath);
|
|
216
|
+
if (!existsSync(sourcePath)) {
|
|
217
|
+
throw new Error(`Configured identity.icon was not found: ${iconPath}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const extension = extname(sourcePath).toLowerCase();
|
|
221
|
+
const supported = new Set(['.png', '.jpg', '.jpeg', '.ico', '.icns']);
|
|
222
|
+
if (!supported.has(extension)) {
|
|
223
|
+
throw new Error(`Configured identity.icon must use one of ${[...supported].join(', ')}, got: ${basename(sourcePath)}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const outputDir = resolve(targetOutRoot, 'meta');
|
|
227
|
+
mkdirSync(outputDir, { recursive: true });
|
|
228
|
+
const outputPath = resolve(outputDir, `icon${extension}`);
|
|
229
|
+
copyFileSync(sourcePath, outputPath);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
configuredPath: iconPath,
|
|
233
|
+
sourcePath,
|
|
234
|
+
outputPath,
|
|
235
|
+
status: 'copied',
|
|
236
|
+
reasonCode: 'icon_copied',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function executableNameForPlatform(target, executableBaseName = 'game') {
|
|
241
|
+
if (target === 'windows') {
|
|
242
|
+
return `${executableBaseName}.exe`;
|
|
243
|
+
}
|
|
244
|
+
return executableBaseName;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Commands
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
async function cmdInit(args) {
|
|
252
|
+
const name = args[0];
|
|
253
|
+
if (!name) {
|
|
254
|
+
error('Missing project name. Usage: aura init <name>');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
258
|
+
error('Project name must contain only letters, numbers, hyphens, and underscores.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const dest = resolve(process.cwd(), name);
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const result = scaffold(name, dest);
|
|
265
|
+
console.log(`\n Created "${name}" at ${dest}`);
|
|
266
|
+
console.log('');
|
|
267
|
+
for (const file of result.files) {
|
|
268
|
+
console.log(` ${file}`);
|
|
269
|
+
}
|
|
270
|
+
console.log('');
|
|
271
|
+
console.log(' Get started:');
|
|
272
|
+
console.log(` cd ${name}`);
|
|
273
|
+
console.log(' aura dev');
|
|
274
|
+
console.log('');
|
|
275
|
+
} catch (err) {
|
|
276
|
+
error(err.message);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function cmdDev(_args) {
|
|
281
|
+
const projectRoot = process.cwd();
|
|
282
|
+
const config = await loadConfig({ projectRoot, mode: 'dev' });
|
|
283
|
+
const identity = resolveBuildIdentity(config);
|
|
284
|
+
const hostBinary = resolveAndCacheHostBinary();
|
|
285
|
+
const incremental = createIncrementalBundler({
|
|
286
|
+
projectRoot,
|
|
287
|
+
entryFile: config.build.entry,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
let shuttingDown = false;
|
|
291
|
+
let hostChild = null;
|
|
292
|
+
let restartChain = Promise.resolve();
|
|
293
|
+
|
|
294
|
+
const runBundle = (trigger) => {
|
|
295
|
+
const result = incremental.build({ mode: 'dev' });
|
|
296
|
+
console.log(`\n aura dev: ${trigger}`);
|
|
297
|
+
console.log(` Bundle: ${result.outFile}`);
|
|
298
|
+
console.log(` Modules: ${result.moduleCount}`);
|
|
299
|
+
console.log(` Hash: ${result.hash.slice(0, 8)}`);
|
|
300
|
+
console.log(` Rebuild: ${result.elapsedMs}ms`);
|
|
301
|
+
return result;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const stopHost = async (reason) => {
|
|
305
|
+
const child = hostChild;
|
|
306
|
+
if (!child) return;
|
|
307
|
+
|
|
308
|
+
hostChild = null;
|
|
309
|
+
|
|
310
|
+
// Child is already gone — nothing to signal.
|
|
311
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await new Promise((resolvePromise) => {
|
|
316
|
+
let settled = false;
|
|
317
|
+
let killTimer = null;
|
|
318
|
+
|
|
319
|
+
const settle = () => {
|
|
320
|
+
if (settled) return;
|
|
321
|
+
settled = true;
|
|
322
|
+
if (killTimer !== null) {
|
|
323
|
+
clearTimeout(killTimer);
|
|
324
|
+
}
|
|
325
|
+
resolvePromise();
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const onExit = () => {
|
|
329
|
+
child.removeListener('exit', onExit);
|
|
330
|
+
settle();
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
child.once('exit', onExit);
|
|
334
|
+
|
|
335
|
+
killTimer = setTimeout(() => {
|
|
336
|
+
try {
|
|
337
|
+
child.kill('SIGKILL');
|
|
338
|
+
} catch {}
|
|
339
|
+
}, 1500);
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
child.kill('SIGTERM');
|
|
343
|
+
} catch {
|
|
344
|
+
settle();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if (!shuttingDown) {
|
|
349
|
+
console.log(` aura dev: host stopped (${reason}).`);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const startHost = (entrypoint, reason) => {
|
|
354
|
+
if (shuttingDown) return;
|
|
355
|
+
|
|
356
|
+
const child = spawn(hostBinary.binaryPath, [entrypoint], {
|
|
357
|
+
cwd: projectRoot,
|
|
358
|
+
stdio: 'inherit',
|
|
359
|
+
env: {
|
|
360
|
+
...process.env,
|
|
361
|
+
AURA_MODE: 'dev',
|
|
362
|
+
AURA_OPTIONAL_MODULES: encodeOptionalModulesEnv(config.modules),
|
|
363
|
+
AURA_GAME_NAME: identity.name,
|
|
364
|
+
AURA_GAME_VERSION: identity.version,
|
|
365
|
+
AURA_GAME_EXECUTABLE: identity.executableBaseName,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
hostChild = child;
|
|
370
|
+
console.log(` aura dev: host started (pid=${child.pid ?? 'n/a'}, reason=${reason}).`);
|
|
371
|
+
|
|
372
|
+
child.once('error', (err) => {
|
|
373
|
+
if (!shuttingDown) {
|
|
374
|
+
console.error(`\n aura dev host error: ${err.message}\n`);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
child.once('exit', (code, signal) => {
|
|
379
|
+
if (hostChild === child) {
|
|
380
|
+
hostChild = null;
|
|
381
|
+
}
|
|
382
|
+
if (!shuttingDown) {
|
|
383
|
+
if (signal) {
|
|
384
|
+
console.log(` aura dev: host exited by signal ${signal}.`);
|
|
385
|
+
} else {
|
|
386
|
+
console.log(` aura dev: host exited with code ${code ?? 0}.`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const queueHostRestart = (entrypoint, reason) => {
|
|
393
|
+
restartChain = restartChain
|
|
394
|
+
.then(async () => {
|
|
395
|
+
if (hostChild) {
|
|
396
|
+
console.log(` aura dev: restarting host (${reason}).`);
|
|
397
|
+
await stopHost('reload');
|
|
398
|
+
}
|
|
399
|
+
if (!shuttingDown) {
|
|
400
|
+
startHost(entrypoint, reason);
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
.catch((err) => {
|
|
404
|
+
console.error(`\n aura dev restart error: ${err.message}\n`);
|
|
405
|
+
});
|
|
406
|
+
return restartChain;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const initial = runBundle('initial bundle complete');
|
|
410
|
+
console.log(` Host package: ${hostBinary.packageName}`);
|
|
411
|
+
console.log(` Host binary: ${hostBinary.binaryPath}`);
|
|
412
|
+
console.log(` Host cache: ${hostBinary.cacheStatus} (${hostBinary.cacheDir})`);
|
|
413
|
+
for (const diagnostic of hostBinary.diagnostics || []) {
|
|
414
|
+
console.log(` Host diagnostic: ${diagnostic}`);
|
|
415
|
+
}
|
|
416
|
+
startHost(initial.outFile, 'initial');
|
|
417
|
+
console.log('\n Watching src/ for changes... (Ctrl+C to stop)');
|
|
418
|
+
|
|
419
|
+
let pendingTimer = null;
|
|
420
|
+
|
|
421
|
+
const stopWatch = incremental.watch(
|
|
422
|
+
() => {
|
|
423
|
+
clearTimeout(pendingTimer);
|
|
424
|
+
pendingTimer = setTimeout(() => {
|
|
425
|
+
try {
|
|
426
|
+
const next = runBundle('source change rebuilt');
|
|
427
|
+
void queueHostRestart(next.outFile, 'source change');
|
|
428
|
+
} catch (bundleError) {
|
|
429
|
+
console.error(`\n aura dev rebuild failed:\n${formatBundleError(bundleError, projectRoot)}\n`);
|
|
430
|
+
}
|
|
431
|
+
}, 75);
|
|
432
|
+
},
|
|
433
|
+
(watchError) => {
|
|
434
|
+
console.error(`\n aura dev watch error: ${watchError.message}\n`);
|
|
435
|
+
},
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
await new Promise((resolvePromise) => {
|
|
439
|
+
let closed = false;
|
|
440
|
+
|
|
441
|
+
const shutdown = () => {
|
|
442
|
+
if (closed) return;
|
|
443
|
+
closed = true;
|
|
444
|
+
shuttingDown = true;
|
|
445
|
+
process.off('SIGINT', shutdown);
|
|
446
|
+
process.off('SIGTERM', shutdown);
|
|
447
|
+
clearTimeout(pendingTimer);
|
|
448
|
+
stopWatch();
|
|
449
|
+
incremental.dispose();
|
|
450
|
+
restartChain
|
|
451
|
+
.then(() => stopHost('shutdown'))
|
|
452
|
+
.finally(() => resolvePromise());
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
process.on('SIGINT', shutdown);
|
|
456
|
+
process.on('SIGTERM', shutdown);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function cmdBuild(args) {
|
|
461
|
+
const target = parseTarget(args);
|
|
462
|
+
if (target && !SUPPORTED_BUILD_TARGETS.has(target)) {
|
|
463
|
+
throw new Error(`Unsupported --target "${target}". Supported values: windows, mac, linux, all.`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const requestedTarget = target || currentPlatformTarget();
|
|
467
|
+
const projectRoot = process.cwd();
|
|
468
|
+
const config = await loadConfig({ projectRoot, mode: 'build' });
|
|
469
|
+
const identity = resolveBuildIdentity(config);
|
|
470
|
+
const platformTarget = currentPlatformTarget();
|
|
471
|
+
const effectiveTarget = platformTarget;
|
|
472
|
+
const targetOutRoot = resolve(projectRoot, config.build.outDir, effectiveTarget);
|
|
473
|
+
const executableFileName = executableNameForPlatform(effectiveTarget, identity.executableBaseName);
|
|
474
|
+
|
|
475
|
+
// CLI --asset-mode flag overrides config if provided.
|
|
476
|
+
const cliAssetMode = parseAssetMode(args);
|
|
477
|
+
const assetMode = args.some((a) => a === '--asset-mode') ? cliAssetMode : config.build.assetMode;
|
|
478
|
+
|
|
479
|
+
const result = runBuildStage('bundle', () => bundleProject({
|
|
480
|
+
projectRoot,
|
|
481
|
+
mode: 'build',
|
|
482
|
+
entryFile: config.build.entry,
|
|
483
|
+
outFile: resolve(targetOutRoot, 'js/game.bundle.js'),
|
|
484
|
+
}));
|
|
485
|
+
|
|
486
|
+
const assets = runBuildStage('asset-pack', () => packageAssets({
|
|
487
|
+
projectRoot,
|
|
488
|
+
outRoot: targetOutRoot,
|
|
489
|
+
mode: assetMode,
|
|
490
|
+
}));
|
|
491
|
+
const hostBinary = runBuildStage('host-binary-resolve', () => resolveAndCacheHostBinary());
|
|
492
|
+
const executablePath = runBuildStage('host-binary-copy', () => {
|
|
493
|
+
const outPath = resolve(targetOutRoot, executableFileName);
|
|
494
|
+
copyFileSync(hostBinary.binaryPath, outPath);
|
|
495
|
+
if (effectiveTarget !== 'windows') {
|
|
496
|
+
chmodSync(outPath, 0o755);
|
|
497
|
+
}
|
|
498
|
+
return outPath;
|
|
499
|
+
});
|
|
500
|
+
const icon = runBuildStage('identity-icon', () => resolveBuildIcon({
|
|
501
|
+
projectRoot,
|
|
502
|
+
targetOutRoot,
|
|
503
|
+
iconPath: identity.iconPath,
|
|
504
|
+
}));
|
|
505
|
+
const buildManifestPath = runBuildStage('build-contract', () => writeBuildManifest({
|
|
506
|
+
outRoot: targetOutRoot,
|
|
507
|
+
bundlePath: result.outFile,
|
|
508
|
+
assetsManifestPath: assets.manifestPath,
|
|
509
|
+
executablePath,
|
|
510
|
+
assetMode: assets.mode,
|
|
511
|
+
identity: {
|
|
512
|
+
name: identity.name,
|
|
513
|
+
version: identity.version,
|
|
514
|
+
windowTitle: config.window.title,
|
|
515
|
+
executableBaseName: identity.executableBaseName,
|
|
516
|
+
executableFileName,
|
|
517
|
+
},
|
|
518
|
+
icon,
|
|
519
|
+
}));
|
|
520
|
+
|
|
521
|
+
console.log('\n aura build: JavaScript bundle complete.');
|
|
522
|
+
console.log(` Game: ${identity.name}`);
|
|
523
|
+
console.log(` Version: ${identity.version}`);
|
|
524
|
+
console.log(` Bundle: ${result.outFile}`);
|
|
525
|
+
console.log(` Modules: ${result.moduleCount}`);
|
|
526
|
+
console.log(` Hash: ${result.hash.slice(0, 8)}`);
|
|
527
|
+
console.log(` Time: ${result.elapsedMs}ms`);
|
|
528
|
+
console.log('');
|
|
529
|
+
console.log(` Assets: ${assets.assetCount}`);
|
|
530
|
+
console.log(` Asset mode: ${assets.mode}`);
|
|
531
|
+
console.log(` Asset manifest hash: ${assets.manifestHash}`);
|
|
532
|
+
console.log(
|
|
533
|
+
` Asset incremental: copied=${assets.incremental.copied} skipped=${assets.incremental.skipped} removed=${assets.incremental.removed} reusedPack=${assets.incremental.reusedPack}`,
|
|
534
|
+
);
|
|
535
|
+
if (assets.packPath) {
|
|
536
|
+
console.log(` Asset pack: ${assets.packPath}`);
|
|
537
|
+
}
|
|
538
|
+
console.log(` Asset manifest: ${assets.manifestPath}`);
|
|
539
|
+
console.log(` Build manifest: ${buildManifestPath}`);
|
|
540
|
+
console.log(` Host package: ${hostBinary.packageName}`);
|
|
541
|
+
console.log(` Host binary: ${hostBinary.binaryPath}`);
|
|
542
|
+
console.log(` Host cache: ${hostBinary.cacheStatus} (${hostBinary.cacheDir})`);
|
|
543
|
+
for (const diagnostic of hostBinary.diagnostics || []) {
|
|
544
|
+
console.log(` Host diagnostic: ${diagnostic}`);
|
|
545
|
+
}
|
|
546
|
+
console.log(` Executable: ${executablePath}`);
|
|
547
|
+
console.log(` Executable base name: ${identity.executableBaseName}`);
|
|
548
|
+
if (icon.status === 'copied') {
|
|
549
|
+
console.log(` Icon: ${icon.outputPath}`);
|
|
550
|
+
} else {
|
|
551
|
+
console.log(` Icon: ${icon.status} (${icon.reasonCode})`);
|
|
552
|
+
}
|
|
553
|
+
console.log(` Output target: ${effectiveTarget}`);
|
|
554
|
+
|
|
555
|
+
if (target && target !== platformTarget && target !== 'all') {
|
|
556
|
+
console.log('');
|
|
557
|
+
console.log(` Native target requested: ${requestedTarget}`);
|
|
558
|
+
console.log(` Built target: ${platformTarget} (current host only in v1).`);
|
|
559
|
+
console.log(' Note: cross-platform native compile is handled by CI in v1.');
|
|
560
|
+
} else if (target === 'all') {
|
|
561
|
+
console.log('');
|
|
562
|
+
console.log(' --target all in v1 builds only the current host platform.');
|
|
563
|
+
console.log(' Note: use CI matrix to build and publish all targets.');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
console.log('');
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
projectRoot,
|
|
570
|
+
config,
|
|
571
|
+
requestedTarget,
|
|
572
|
+
platformTarget,
|
|
573
|
+
effectiveTarget,
|
|
574
|
+
targetOutRoot,
|
|
575
|
+
executablePath,
|
|
576
|
+
bundlePath: result.outFile,
|
|
577
|
+
assetsManifestPath: assets.manifestPath,
|
|
578
|
+
buildManifestPath,
|
|
579
|
+
assetMode: assets.mode,
|
|
580
|
+
identity,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function cmdRun(args) {
|
|
585
|
+
const built = await cmdBuild(args);
|
|
586
|
+
const launchContract = built.buildManifestPath;
|
|
587
|
+
|
|
588
|
+
console.log(' aura run: launching native host.');
|
|
589
|
+
console.log(` Executable: ${built.executablePath}`);
|
|
590
|
+
console.log(` Build manifest: ${launchContract}`);
|
|
591
|
+
console.log(` Working dir: ${built.projectRoot}`);
|
|
592
|
+
console.log('');
|
|
593
|
+
|
|
594
|
+
const child = spawnSync(built.executablePath, [launchContract], {
|
|
595
|
+
cwd: built.projectRoot,
|
|
596
|
+
stdio: 'inherit',
|
|
597
|
+
env: {
|
|
598
|
+
...process.env,
|
|
599
|
+
AURA_MODE: 'release',
|
|
600
|
+
AURA_OPTIONAL_MODULES: encodeOptionalModulesEnv(built.config.modules),
|
|
601
|
+
AURA_GAME_NAME: built.identity.name,
|
|
602
|
+
AURA_GAME_VERSION: built.identity.version,
|
|
603
|
+
AURA_GAME_EXECUTABLE: built.identity.executableBaseName,
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
if (child.error) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`Failed to launch built host "${built.executablePath}": ${child.error.message}`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (typeof child.status === 'number' && child.status !== 0) {
|
|
614
|
+
throw new Error(`Native host exited with code ${child.status}.`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (child.signal) {
|
|
618
|
+
throw new Error(`Native host terminated by signal ${child.signal}.`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function cmdClean(_args) {
|
|
623
|
+
const projectRoot = process.cwd();
|
|
624
|
+
const config = await loadConfig({ projectRoot, mode: 'build' });
|
|
625
|
+
|
|
626
|
+
const candidates = [
|
|
627
|
+
resolve(projectRoot, config.build.outDir),
|
|
628
|
+
resolve(projectRoot, '.aura/dev'),
|
|
629
|
+
resolve(projectRoot, '.aura/test'),
|
|
630
|
+
];
|
|
631
|
+
|
|
632
|
+
const removed = [];
|
|
633
|
+
for (const path of candidates) {
|
|
634
|
+
if (!existsSync(path)) continue;
|
|
635
|
+
rmSync(path, { recursive: true, force: true });
|
|
636
|
+
removed.push(path);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const auraRoot = resolve(projectRoot, '.aura');
|
|
640
|
+
if (existsSync(auraRoot) && readdirSync(auraRoot).length === 0) {
|
|
641
|
+
rmSync(auraRoot, { recursive: true, force: true });
|
|
642
|
+
removed.push(auraRoot);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
console.log('\n aura clean: complete.');
|
|
646
|
+
if (removed.length === 0) {
|
|
647
|
+
console.log(' Nothing to remove.');
|
|
648
|
+
} else {
|
|
649
|
+
for (const path of removed) {
|
|
650
|
+
console.log(` Removed: ${path}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
console.log('');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function cmdTest(args) {
|
|
657
|
+
const parsed = parseTestArgs(args);
|
|
658
|
+
const result = await runHeadlessTest({
|
|
659
|
+
projectRoot: process.cwd(),
|
|
660
|
+
file: parsed.file,
|
|
661
|
+
width: parsed.width,
|
|
662
|
+
height: parsed.height,
|
|
663
|
+
frames: parsed.frames,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
console.log('\n aura test: headless run complete.');
|
|
667
|
+
console.log(` Entry: ${result.entryFile}`);
|
|
668
|
+
console.log(` Bundle: ${result.bundle.outFile}`);
|
|
669
|
+
console.log(` Modules: ${result.bundle.moduleCount}`);
|
|
670
|
+
console.log(` Frames: ${result.frames}`);
|
|
671
|
+
console.log(` Window: ${result.width}x${result.height}`);
|
|
672
|
+
console.log(` Assertions passed: ${result.passes}`);
|
|
673
|
+
console.log(` Draw calls captured: ${result.drawCalls}`);
|
|
674
|
+
console.log(` Audio calls captured: ${result.audioCalls}`);
|
|
675
|
+
console.log('');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function cmdConformance(args) {
|
|
679
|
+
const exitCode = await runConformanceCommand(args, console);
|
|
680
|
+
if (exitCode !== 0) {
|
|
681
|
+
process.exit(exitCode);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const HANDLERS = {
|
|
686
|
+
init: cmdInit,
|
|
687
|
+
dev: cmdDev,
|
|
688
|
+
build: cmdBuild,
|
|
689
|
+
run: cmdRun,
|
|
690
|
+
clean: cmdClean,
|
|
691
|
+
test: cmdTest,
|
|
692
|
+
conformance: cmdConformance,
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// ---------------------------------------------------------------------------
|
|
696
|
+
// Main
|
|
697
|
+
// ---------------------------------------------------------------------------
|
|
698
|
+
|
|
699
|
+
async function main() {
|
|
700
|
+
const argv = process.argv.slice(2);
|
|
701
|
+
|
|
702
|
+
if (argv.length === 0) {
|
|
703
|
+
printHelp();
|
|
704
|
+
process.exit(0);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (argv.includes('--version') || argv.includes('-v')) {
|
|
708
|
+
console.log(VERSION);
|
|
709
|
+
process.exit(0);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (argv[0] === '--help' || argv[0] === '-h') {
|
|
713
|
+
printHelp();
|
|
714
|
+
process.exit(0);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const command = argv[0];
|
|
718
|
+
|
|
719
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
720
|
+
printCommandHelp(command);
|
|
721
|
+
process.exit(0);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (!COMMANDS[command]) {
|
|
725
|
+
error(`Unknown command: "${command}". Run "aura --help" for available commands.`);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const commandArgs = argv.slice(1).filter((a) => a !== '--help' && a !== '-h');
|
|
729
|
+
try {
|
|
730
|
+
await HANDLERS[command](commandArgs);
|
|
731
|
+
} catch (err) {
|
|
732
|
+
if (err instanceof ConfigError) {
|
|
733
|
+
error(err.message, 4);
|
|
734
|
+
}
|
|
735
|
+
if (err instanceof HeadlessTestError) {
|
|
736
|
+
if (Array.isArray(err.details?.failures) && err.details.failures.length > 0) {
|
|
737
|
+
console.error('\n aura test failures:');
|
|
738
|
+
for (const failure of err.details.failures) {
|
|
739
|
+
console.error(` - ${failure}`);
|
|
740
|
+
}
|
|
741
|
+
console.error('');
|
|
742
|
+
}
|
|
743
|
+
error(err.message, 3);
|
|
744
|
+
}
|
|
745
|
+
if (isBundleError(err)) {
|
|
746
|
+
error(formatBundleError(err, process.cwd()), 2);
|
|
747
|
+
}
|
|
748
|
+
if (err instanceof BuildStageError) {
|
|
749
|
+
if (isBundleError(err.cause)) {
|
|
750
|
+
error(`Build stage "${err.stage}" failed:\n${formatBundleError(err.cause, process.cwd())}`, 2);
|
|
751
|
+
}
|
|
752
|
+
if (err.cause instanceof HostBinaryResolutionError) {
|
|
753
|
+
error(`Build stage "${err.stage}" failed: ${err.cause.message}`, 5);
|
|
754
|
+
}
|
|
755
|
+
error(err.message);
|
|
756
|
+
}
|
|
757
|
+
if (err instanceof HostBinaryResolutionError) {
|
|
758
|
+
error(err.message, 5);
|
|
759
|
+
}
|
|
760
|
+
error(err.message);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
main();
|