@auraindustry/aurajs 0.0.2 → 0.0.4

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.
@@ -0,0 +1,310 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+
3
+ import { createAuraGame } from '../web-api.mjs';
4
+
5
+ function isPlainObject(value) {
6
+ return value != null && typeof value === 'object' && Object.prototype.toString.call(value) === '[object Object]';
7
+ }
8
+
9
+ function stableSerialize(value) {
10
+ if (value == null) return 'null';
11
+ if (typeof value === 'boolean' || typeof value === 'number') return JSON.stringify(value);
12
+ if (typeof value === 'string') return JSON.stringify(value);
13
+ if (Array.isArray(value)) {
14
+ return '[' + value.map((entry) => stableSerialize(entry)).join(',') + ']';
15
+ }
16
+ if (isPlainObject(value)) {
17
+ const keys = Object.keys(value).sort();
18
+ const fields = [];
19
+ for (const key of keys) {
20
+ fields.push(JSON.stringify(key) + ':' + stableSerialize(value[key]));
21
+ }
22
+ return '{' + fields.join(',') + '}';
23
+ }
24
+ return JSON.stringify(String(value));
25
+ }
26
+
27
+ function createReactError(reasonCode, message, retryable, details) {
28
+ const error = new Error(message);
29
+ error.ok = false;
30
+ error.reasonCode = reasonCode;
31
+ error.layer = 'react';
32
+ error.retryable = retryable === true;
33
+ error.details = isPlainObject(details) ? { ...details } : {};
34
+ return error;
35
+ }
36
+
37
+ function normalizeReasonCodedError(error, fallbackReasonCode, fallbackMessage, fallbackRetryable) {
38
+ if (error && typeof error === 'object' && typeof error.reasonCode === 'string') {
39
+ return error;
40
+ }
41
+ return createReactError(
42
+ fallbackReasonCode,
43
+ fallbackMessage,
44
+ fallbackRetryable,
45
+ { cause: String(error && error.message ? error.message : error) },
46
+ );
47
+ }
48
+
49
+ function stableErrorKey(error) {
50
+ if (!error || typeof error !== 'object') return null;
51
+ return stableSerialize({
52
+ reasonCode: typeof error.reasonCode === 'string' ? error.reasonCode : null,
53
+ layer: typeof error.layer === 'string' ? error.layer : null,
54
+ retryable: error.retryable === true,
55
+ details: isPlainObject(error.details) ? error.details : {},
56
+ });
57
+ }
58
+
59
+ function normalizeSource(source) {
60
+ if (source == null) return '.';
61
+ if (typeof source !== 'string' || source.trim().length === 0) {
62
+ throw createReactError(
63
+ 'react_auragame_invalid_source',
64
+ 'AuraGame prop source must be a non-empty string.',
65
+ false,
66
+ {},
67
+ );
68
+ }
69
+ return source.trim();
70
+ }
71
+
72
+ function normalizeConfig(config) {
73
+ if (config == null) return {};
74
+ if (!isPlainObject(config)) {
75
+ throw createReactError(
76
+ 'react_auragame_invalid_config',
77
+ 'AuraGame prop config must be a plain object.',
78
+ false,
79
+ {},
80
+ );
81
+ }
82
+ return { ...config };
83
+ }
84
+
85
+ async function safeUnmount(game) {
86
+ if (!game || typeof game.unmount !== 'function') return;
87
+ try {
88
+ await game.unmount();
89
+ } catch (_) {
90
+ // Cleanup should not throw through React unmount paths.
91
+ }
92
+ }
93
+
94
+ export function AuraGame(props = {}) {
95
+ const {
96
+ source = '.',
97
+ config = null,
98
+ restartKey = null,
99
+ loader = null,
100
+ className,
101
+ style,
102
+ mountClassName,
103
+ mountStyle,
104
+ onReady,
105
+ onPhaseChange,
106
+ onLifecycle,
107
+ onLoadingChange,
108
+ onError,
109
+ renderLoading,
110
+ renderError,
111
+ } = props;
112
+
113
+ const mountNodeRef = useRef(null);
114
+ const gameRef = useRef(null);
115
+ const callbacksRef = useRef({
116
+ onReady: null,
117
+ onPhaseChange: null,
118
+ onLifecycle: null,
119
+ onLoadingChange: null,
120
+ onError: null,
121
+ });
122
+ callbacksRef.current = {
123
+ onReady: typeof onReady === 'function' ? onReady : null,
124
+ onPhaseChange: typeof onPhaseChange === 'function' ? onPhaseChange : null,
125
+ onLifecycle: typeof onLifecycle === 'function' ? onLifecycle : null,
126
+ onLoadingChange: typeof onLoadingChange === 'function' ? onLoadingChange : null,
127
+ onError: typeof onError === 'function' ? onError : null,
128
+ };
129
+
130
+ const [phase, setPhase] = useState('idle');
131
+ const [loading, setLoading] = useState(false);
132
+ const [error, setError] = useState(null);
133
+ const loadingRef = useRef(false);
134
+ const lastErrorKeyRef = useRef(null);
135
+
136
+ const configSignature = useMemo(() => stableSerialize(config), [config]);
137
+ const restartSignature = useMemo(
138
+ () => stableSerialize({ source, config: configSignature, restartKey }),
139
+ [source, configSignature, restartKey],
140
+ );
141
+
142
+ function setLoadingState(nextLoading) {
143
+ const next = nextLoading === true;
144
+ if (loadingRef.current === next) return;
145
+ loadingRef.current = next;
146
+ setLoading(next);
147
+ if (callbacksRef.current.onLoadingChange) {
148
+ callbacksRef.current.onLoadingChange(next);
149
+ }
150
+ }
151
+
152
+ function emitError(nextError, stateSnapshot) {
153
+ const normalized = normalizeReasonCodedError(
154
+ nextError,
155
+ 'react_auragame_mount_failed',
156
+ 'Failed to mount AuraGame.',
157
+ true,
158
+ );
159
+ const key = stableErrorKey(normalized);
160
+ if (key === lastErrorKeyRef.current) {
161
+ return normalized;
162
+ }
163
+ lastErrorKeyRef.current = key;
164
+ setError(normalized);
165
+ if (callbacksRef.current.onError) {
166
+ callbacksRef.current.onError(normalized, stateSnapshot || null);
167
+ }
168
+ return normalized;
169
+ }
170
+
171
+ useEffect(() => {
172
+ let cancelled = false;
173
+ const mountNode = mountNodeRef.current;
174
+ if (!mountNode) {
175
+ const mountError = createReactError(
176
+ 'react_auragame_mount_node_missing',
177
+ 'AuraGame mount node is unavailable.',
178
+ false,
179
+ {},
180
+ );
181
+ setPhase('error');
182
+ setLoadingState(false);
183
+ emitError(mountError, { phase: 'error', reasonCode: mountError.reasonCode });
184
+ return () => {};
185
+ }
186
+
187
+ let normalizedSource = '.';
188
+ let normalizedConfig = {};
189
+ try {
190
+ normalizedSource = normalizeSource(source);
191
+ normalizedConfig = normalizeConfig(config);
192
+ } catch (validationError) {
193
+ setPhase('error');
194
+ setLoadingState(false);
195
+ emitError(validationError, { phase: 'error', reasonCode: validationError.reasonCode });
196
+ return () => {};
197
+ }
198
+
199
+ setPhase('idle');
200
+ setError(null);
201
+ lastErrorKeyRef.current = null;
202
+ setLoadingState(true);
203
+
204
+ const game = createAuraGame({
205
+ loader,
206
+ onPhaseChange: (nextPhase, stateSnapshot) => {
207
+ if (cancelled) return;
208
+ setPhase(typeof nextPhase === 'string' ? nextPhase : 'idle');
209
+ if (callbacksRef.current.onPhaseChange) {
210
+ callbacksRef.current.onPhaseChange(nextPhase, stateSnapshot);
211
+ }
212
+ },
213
+ onLifecycle: (lifecycle, stateSnapshot) => {
214
+ if (cancelled) return;
215
+ if (callbacksRef.current.onLifecycle) {
216
+ callbacksRef.current.onLifecycle(lifecycle, stateSnapshot);
217
+ }
218
+ },
219
+ onError: (nextError, stateSnapshot) => {
220
+ if (cancelled) return;
221
+ setPhase('error');
222
+ emitError(nextError, stateSnapshot);
223
+ },
224
+ });
225
+ gameRef.current = game;
226
+
227
+ async function start() {
228
+ try {
229
+ game.setConfig({
230
+ ...normalizedConfig,
231
+ rootUrl: normalizedSource,
232
+ mount: mountNode,
233
+ });
234
+ const mountedState = await game.mount(mountNode, {
235
+ ...normalizedConfig,
236
+ rootUrl: normalizedSource,
237
+ mount: mountNode,
238
+ });
239
+ if (cancelled) {
240
+ await safeUnmount(game);
241
+ return;
242
+ }
243
+ setPhase(typeof mountedState.phase === 'string' ? mountedState.phase : 'mounted');
244
+ setError(null);
245
+ setLoadingState(false);
246
+ if (callbacksRef.current.onReady) {
247
+ callbacksRef.current.onReady(mountedState);
248
+ }
249
+ } catch (mountError) {
250
+ if (cancelled) return;
251
+ const normalized = normalizeReasonCodedError(
252
+ mountError,
253
+ 'react_auragame_mount_failed',
254
+ 'Failed to mount AuraGame.',
255
+ true,
256
+ );
257
+ setPhase('error');
258
+ setLoadingState(false);
259
+ emitError(normalized, { phase: 'error', reasonCode: normalized.reasonCode });
260
+ }
261
+ }
262
+
263
+ void start();
264
+
265
+ return () => {
266
+ cancelled = true;
267
+ if (gameRef.current === game) {
268
+ gameRef.current = null;
269
+ }
270
+ void safeUnmount(game);
271
+ };
272
+ }, [loader, restartSignature]);
273
+
274
+ const children = [
275
+ React.createElement('div', {
276
+ key: 'mount',
277
+ ref: mountNodeRef,
278
+ className: mountClassName,
279
+ style: mountStyle,
280
+ 'data-aura-game-mount': 'true',
281
+ }),
282
+ ];
283
+
284
+ if (loading && typeof renderLoading === 'function') {
285
+ children.push(React.createElement(React.Fragment, { key: 'loading' }, renderLoading({
286
+ phase,
287
+ loading,
288
+ error,
289
+ })));
290
+ }
291
+
292
+ if (error && typeof renderError === 'function') {
293
+ children.push(React.createElement(React.Fragment, { key: 'error' }, renderError(error, {
294
+ phase,
295
+ loading,
296
+ error,
297
+ })));
298
+ }
299
+
300
+ return React.createElement('div', {
301
+ className,
302
+ style,
303
+ 'data-aura-game': 'true',
304
+ 'data-aura-phase': phase,
305
+ 'data-aura-loading': loading ? 'true' : 'false',
306
+ 'data-aura-error-code': error && typeof error.reasonCode === 'string' ? error.reasonCode : '',
307
+ }, children);
308
+ }
309
+
310
+ export default AuraGame;
@@ -0,0 +1 @@
1
+ export { AuraGame, default } from './aura-game.mjs';
package/src/scaffold.mjs CHANGED
@@ -2,36 +2,290 @@ import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, statSy
2
2
  import { resolve, join, dirname } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
- const TEMPLATES_DIR = resolve(
6
- dirname(fileURLToPath(import.meta.url)),
7
- '../templates/starter'
8
- );
5
+ const CLI_SRC_DIR = dirname(fileURLToPath(import.meta.url));
6
+
7
+ const LEGACY_STARTER_TEMPLATE_DIR = resolve(CLI_SRC_DIR, '../templates/starter');
8
+
9
+ const CREATE_TEMPLATE_DIRS = {
10
+ '2d-shooter': resolve(CLI_SRC_DIR, '../templates/create/2d'),
11
+ '3d-platformer': resolve(CLI_SRC_DIR, '../templates/create/3d'),
12
+ blank: resolve(CLI_SRC_DIR, '../templates/create/blank'),
13
+ };
14
+ const CREATE_SHARED_TEMPLATE_DIR = resolve(CLI_SRC_DIR, '../templates/create/shared');
15
+
16
+ const CREATE_TEMPLATE_ALIASES = {
17
+ '2d': '2d-shooter',
18
+ shooter: '2d-shooter',
19
+ '2d shooter': '2d-shooter',
20
+ '2d-shooter': '2d-shooter',
21
+ '3d': '3d-platformer',
22
+ platformer: '3d-platformer',
23
+ platformers: '3d-platformer',
24
+ '3d platformer': '3d-platformer',
25
+ '3d platformers': '3d-platformer',
26
+ '3d-platformer': '3d-platformer',
27
+ blank: 'blank',
28
+ };
29
+
30
+ const ROOT_SKILLS_DIR = resolve(CLI_SRC_DIR, '../../../skills');
31
+ const PACKAGED_SKILLS_DIR = resolve(CLI_SRC_DIR, '../templates/skills');
32
+
33
+ const GITIGNORE_TEMPLATE = `# Dependencies\nnode_modules/\n\n# Build output\nbuild/\n.aura/\n\n# Runtime logs\n.logs/\n*.log\nnpm-debug.log*\n\n# Local env\n.env\n.env.*\n`;
34
+
35
+ const PLAY_BIN_TEMPLATE = `#!/usr/bin/env node
36
+
37
+ import { spawn } from 'node:child_process';
38
+ import { existsSync, mkdirSync, createWriteStream } from 'node:fs';
39
+ import { resolve, dirname, join } from 'node:path';
40
+ import { fileURLToPath } from 'node:url';
41
+
42
+ const __dirname = dirname(fileURLToPath(import.meta.url));
43
+ const projectRoot = resolve(__dirname, '..');
44
+ const localAuraCli = resolve(projectRoot, 'node_modules', '@auraindustry', 'aurajs', 'src', 'cli.mjs');
45
+ const runArgs = ['run', '--asset-mode', 'sibling'];
46
+
47
+ const logDir = resolve(projectRoot, '.logs');
48
+ mkdirSync(logDir, { recursive: true });
49
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
50
+ const logPath = join(logDir, \`play-\${stamp}.log\`);
51
+ const logStream = createWriteStream(logPath, { flags: 'a' });
52
+
53
+ const command = existsSync(localAuraCli)
54
+ ? {
55
+ cmd: process.execPath,
56
+ args: [localAuraCli, ...runArgs],
57
+ }
58
+ : {
59
+ cmd: 'npm',
60
+ args: ['exec', '--yes', '--package', '@auraindustry/aurajs', '--', 'aura', ...runArgs],
61
+ };
62
+
63
+ logStream.write(\`[play] \${new Date().toISOString()} command=\${command.cmd} \${command.args.join(' ')}\\n\`);
64
+
65
+ const child = spawn(command.cmd, command.args, {
66
+ cwd: projectRoot,
67
+ stdio: ['inherit', 'pipe', 'pipe'],
68
+ env: process.env,
69
+ });
70
+
71
+ function pipeToConsoleAndLog(stream, target) {
72
+ if (!stream) return;
73
+ stream.on('data', (chunk) => {
74
+ target.write(chunk);
75
+ logStream.write(chunk);
76
+ });
77
+ }
78
+
79
+ pipeToConsoleAndLog(child.stdout, process.stdout);
80
+ pipeToConsoleAndLog(child.stderr, process.stderr);
81
+
82
+ child.on('error', (err) => {
83
+ const message = \`[play:error] \${err.message}\\n\`;
84
+ process.stderr.write(message);
85
+ logStream.write(message);
86
+ logStream.end();
87
+ process.exit(1);
88
+ });
89
+
90
+ child.on('close', (code) => {
91
+ logStream.write(\`\\n[play:exit] code=\${code ?? 1}\\n\`);
92
+ logStream.end(() => {
93
+ process.exit(code ?? 1);
94
+ });
95
+ });
96
+ `;
97
+
98
+ export function listCreateTemplates() {
99
+ return Object.keys(CREATE_TEMPLATE_DIRS);
100
+ }
101
+
102
+ export function normalizeCreateTemplate(template) {
103
+ const normalized = String(template || '').trim().toLowerCase();
104
+ return CREATE_TEMPLATE_ALIASES[normalized] || null;
105
+ }
9
106
 
10
107
  /**
11
- * Scaffold a new AuraJS project.
108
+ * Scaffold the legacy starter template (`aura init`).
12
109
  * @param {string} name - Project name (used as directory name and in config).
13
110
  * @param {string} dest - Absolute path to create the project at.
14
111
  */
15
112
  export function scaffold(name, dest) {
16
- if (existsSync(dest)) {
17
- const entries = readdirSync(dest);
18
- if (entries.length > 0) {
19
- throw new Error(`Directory "${name}" already exists and is not empty.`);
20
- }
113
+ assertDestinationIsEmpty(name, dest);
114
+
115
+ copyTree(LEGACY_STARTER_TEMPLATE_DIR, dest, { PROJECT_NAME: name });
116
+ mkdirSync(join(dest, 'assets'), { recursive: true });
117
+
118
+ return {
119
+ dest,
120
+ files: listFiles(dest),
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Scaffold a full npm-ready AuraJS game project (`aura create`).
126
+ * @param {object} options - Scaffolding options.
127
+ * @param {string} options.name - Project directory name (npm package defaults to @aurajs/<name>).
128
+ * @param {string} options.dest - Absolute destination directory.
129
+ * @param {string} [options.template='2d-shooter'] - Template key: 2d-shooter | 3d-platformer | blank.
130
+ * @param {string} [options.version='0.1.0'] - Initial package version.
131
+ * @param {string} [options.license='MIT'] - Initial package license.
132
+ */
133
+ export function scaffoldGame(options) {
134
+ const {
135
+ name,
136
+ dest,
137
+ template = '2d-shooter',
138
+ version = '0.1.0',
139
+ license = 'MIT',
140
+ } = options;
141
+
142
+ const normalizedTemplate = normalizeCreateTemplate(template);
143
+ const templateDir = CREATE_TEMPLATE_DIRS[normalizedTemplate];
144
+ if (!templateDir || !existsSync(templateDir)) {
145
+ throw new Error(
146
+ `Unknown template "${template}". Expected one of: ${listCreateTemplates().join(', ')} (aliases: 2d, 3d, shooter, platformer, blank).`,
147
+ );
21
148
  }
22
149
 
23
- // Copy template tree, replacing {{PROJECT_NAME}} placeholders.
24
- copyTree(TEMPLATES_DIR, dest, { PROJECT_NAME: name });
150
+ assertDestinationIsEmpty(name, dest);
151
+
152
+ const binName = sanitizeBinName(name);
153
+ const projectTitle = toDisplayTitle(name);
154
+ const replacements = {
155
+ PROJECT_NAME: name,
156
+ PROJECT_TITLE: projectTitle,
157
+ PROJECT_BIN_NAME: binName,
158
+ PROJECT_VERSION: version,
159
+ PROJECT_LICENSE: license,
160
+ };
161
+
162
+ copyTree(templateDir, dest, replacements);
163
+ if (normalizedTemplate !== 'blank' && existsSync(CREATE_SHARED_TEMPLATE_DIR)) {
164
+ copyTree(CREATE_SHARED_TEMPLATE_DIR, dest, replacements);
165
+ }
25
166
 
26
- // Ensure assets/ directory exists (even if empty in template).
27
167
  mkdirSync(join(dest, 'assets'), { recursive: true });
168
+ mkdirSync(join(dest, 'bin'), { recursive: true });
169
+ mkdirSync(join(dest, '.logs'), { recursive: true });
170
+ writeFileSync(join(dest, '.logs', '.gitkeep'), '', 'utf8');
171
+
172
+ const pkg = buildPackageJson({
173
+ name,
174
+ version,
175
+ license,
176
+ binName,
177
+ template: normalizedTemplate,
178
+ });
179
+
180
+ writeFileSync(join(dest, 'package.json'), JSON.stringify(pkg, null, 2) + '\n', 'utf8');
181
+ writeFileSync(join(dest, '.gitignore'), GITIGNORE_TEMPLATE, 'utf8');
182
+ writeFileSync(join(dest, 'bin', 'play.js'), PLAY_BIN_TEMPLATE, { encoding: 'utf8', mode: 0o755 });
183
+
184
+ const skillsSource = resolveSkillsSourceDir();
185
+ if (skillsSource) {
186
+ copyTree(skillsSource, join(dest, 'skills'), replacements);
187
+ } else {
188
+ mkdirSync(join(dest, 'skills'), { recursive: true });
189
+ }
28
190
 
29
191
  return {
30
192
  dest,
193
+ template: normalizedTemplate,
31
194
  files: listFiles(dest),
32
195
  };
33
196
  }
34
197
 
198
+ function sanitizeBinName(name) {
199
+ const normalized = String(name || '').trim();
200
+ const compact = normalized
201
+ .replace(/^@[^/]+\//, '')
202
+ .replace(/\s+/g, '-')
203
+ .replace(/[^a-zA-Z0-9._-]/g, '-')
204
+ .replace(/-+/g, '-')
205
+ .replace(/^[-._]+|[-._]+$/g, '')
206
+ .toLowerCase();
207
+
208
+ if (!compact) {
209
+ throw new Error(`Unable to derive executable name from project name "${name}".`);
210
+ }
211
+
212
+ return compact;
213
+ }
214
+
215
+ function sanitizePackageSlug(name) {
216
+ const normalized = String(name || '').trim();
217
+ const compact = normalized
218
+ .replace(/^@[^/]+\//, '')
219
+ .replace(/\s+/g, '-')
220
+ .replace(/[^a-zA-Z0-9._-]/g, '-')
221
+ .replace(/-+/g, '-')
222
+ .replace(/^[-._]+|[-._]+$/g, '')
223
+ .toLowerCase();
224
+
225
+ if (!compact) {
226
+ throw new Error(`Unable to derive package slug from project name "${name}".`);
227
+ }
228
+
229
+ return compact;
230
+ }
231
+
232
+ function toDisplayTitle(name) {
233
+ const cleaned = String(name || '').replace(/^@[^/]+\//, '');
234
+ const words = cleaned.split(/[-_\s]+/).filter(Boolean);
235
+ if (words.length === 0) return String(name || '').trim() || 'AuraJS Game';
236
+ return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
237
+ }
238
+
239
+ function resolveSkillsSourceDir() {
240
+ if (existsSync(ROOT_SKILLS_DIR)) return ROOT_SKILLS_DIR;
241
+ if (existsSync(PACKAGED_SKILLS_DIR)) return PACKAGED_SKILLS_DIR;
242
+ return null;
243
+ }
244
+
245
+ function buildPackageJson({ name, version, license, binName, template }) {
246
+ const packageSlug = sanitizePackageSlug(name);
247
+ const packageName = `@aurajs/${packageSlug}`;
248
+ const keywords = ['aurajs', 'game'];
249
+ if (template.includes('3d')) {
250
+ keywords.push('3d');
251
+ keywords.push('platformer');
252
+ } else if (template.includes('2d')) {
253
+ keywords.push('2d');
254
+ keywords.push('shooter');
255
+ }
256
+
257
+ return {
258
+ name: packageName,
259
+ version,
260
+ description: `${toDisplayTitle(name)} — built with AuraJS`,
261
+ type: 'module',
262
+ bin: {
263
+ [binName]: './bin/play.js',
264
+ },
265
+ files: ['bin/', 'src/', 'assets/', 'skills/', 'aura.config.json'],
266
+ scripts: {
267
+ dev: 'aura dev',
268
+ build: 'aura build',
269
+ play: 'node ./bin/play.js',
270
+ publish: 'npx auramaxx publish',
271
+ },
272
+ dependencies: {
273
+ '@auraindustry/aurajs': '*',
274
+ },
275
+ keywords,
276
+ license,
277
+ };
278
+ }
279
+
280
+ function assertDestinationIsEmpty(name, dest) {
281
+ if (existsSync(dest)) {
282
+ const entries = readdirSync(dest);
283
+ if (entries.length > 0) {
284
+ throw new Error(`Directory "${name}" already exists and is not empty.`);
285
+ }
286
+ }
287
+ }
288
+
35
289
  /**
36
290
  * Recursively copy a directory tree, applying placeholder replacements
37
291
  * to text files.