playcademy 0.11.13 → 0.12.0
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/dist/constants.d.ts +72 -1
- package/dist/constants.js +46 -0
- package/dist/db.d.ts +61 -0
- package/dist/db.js +671 -0
- package/dist/edge-play/src/constants.ts +3 -28
- package/dist/{types.d.ts → index.d.ts} +118 -14
- package/dist/index.js +4650 -6635
- package/dist/templates/api/sample-route-with-db.ts.template +141 -0
- package/dist/templates/config/playcademy.config.js.template +4 -0
- package/dist/templates/config/playcademy.config.json.template +3 -0
- package/dist/templates/config/timeback-config.js.template +8 -0
- package/dist/templates/database/db-index.ts.template +21 -0
- package/dist/templates/database/db-schema-index.ts.template +8 -0
- package/dist/templates/database/db-schema-scores.ts.template +43 -0
- package/dist/templates/database/db-schema-users.ts.template +23 -0
- package/dist/templates/database/db-seed.ts.template +52 -0
- package/dist/templates/database/db-types.ts.template +21 -0
- package/dist/templates/database/drizzle-config.ts.template +13 -0
- package/dist/templates/database/package.json.template +20 -0
- package/dist/templates/gitignore.template +17 -0
- package/dist/templates/playcademy-gitignore.template +3 -0
- package/dist/utils.d.ts +31 -14
- package/dist/utils.js +523 -490
- package/package.json +18 -2
- package/dist/templates/backend-config.js.template +0 -6
- package/dist/templates/playcademy.config.js.template +0 -4
- package/dist/templates/playcademy.config.json.template +0 -3
- package/dist/templates/timeback-config.js.template +0 -17
- /package/dist/templates/{sample-route.ts → api/sample-route.ts.template} +0 -0
- /package/dist/templates/{integrations-config.js.template → config/integrations-config.js.template} +0 -0
package/dist/utils.js
CHANGED
|
@@ -34,6 +34,7 @@ var init_package_json = __esm({
|
|
|
34
34
|
var file_loader_exports = {};
|
|
35
35
|
__export(file_loader_exports, {
|
|
36
36
|
findFile: () => findFile,
|
|
37
|
+
findFilesByExtension: () => findFilesByExtension,
|
|
37
38
|
getCurrentDirectoryName: () => getCurrentDirectoryName,
|
|
38
39
|
getFileExtension: () => getFileExtension,
|
|
39
40
|
getPackageField: () => getPackageField,
|
|
@@ -178,8 +179,8 @@ function getCurrentDirectoryName(fallback = "unknown-directory") {
|
|
|
178
179
|
const dirName = cwd.split("/").pop();
|
|
179
180
|
return dirName || fallback;
|
|
180
181
|
}
|
|
181
|
-
function getFileExtension(
|
|
182
|
-
return
|
|
182
|
+
function getFileExtension(path2) {
|
|
183
|
+
return path2.split(".").pop()?.toLowerCase();
|
|
183
184
|
}
|
|
184
185
|
async function loadModule(filename, options = {}) {
|
|
185
186
|
const { cwd = process.cwd(), required = false, searchUp = false, maxLevels = 3 } = options;
|
|
@@ -255,363 +256,32 @@ function scanDirectory(dir, options = {}) {
|
|
|
255
256
|
scan(dir);
|
|
256
257
|
return files;
|
|
257
258
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
init_package_json();
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// ../edge-play/src/constants.ts
|
|
266
|
-
var TIMEBACK_ROUTES, ROUTES;
|
|
267
|
-
var init_constants = __esm({
|
|
268
|
-
"../edge-play/src/constants.ts"() {
|
|
269
|
-
"use strict";
|
|
270
|
-
TIMEBACK_ROUTES = {
|
|
271
|
-
END_ACTIVITY: "/integrations/timeback/end-activity"
|
|
272
|
-
};
|
|
273
|
-
ROUTES = {
|
|
274
|
-
/** Route index (lists available routes) */
|
|
275
|
-
INDEX: "/api",
|
|
276
|
-
/** Health check endpoint */
|
|
277
|
-
HEALTH: "/api/health",
|
|
278
|
-
/** TimeBack integration routes */
|
|
279
|
-
TIMEBACK: {
|
|
280
|
-
END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// ../edge-play/src/routes/root.html
|
|
287
|
-
var root_default;
|
|
288
|
-
var init_root = __esm({
|
|
289
|
-
"../edge-play/src/routes/root.html"() {
|
|
290
|
-
"use strict";
|
|
291
|
-
root_default = `<!doctype html>
|
|
292
|
-
<html lang="en" class="dark">
|
|
293
|
-
<head>
|
|
294
|
-
<meta charset="UTF-8" />
|
|
295
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
296
|
-
<title>{{GAME_NAME}}</title>
|
|
297
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
298
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
299
|
-
<link
|
|
300
|
-
href="https://fonts.googleapis.com/css2?family=Tomorrow:wght@700&family=VT323&display=swap"
|
|
301
|
-
rel="stylesheet"
|
|
302
|
-
/>
|
|
303
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
304
|
-
<script>
|
|
305
|
-
tailwind.config = {
|
|
306
|
-
darkMode: 'class',
|
|
307
|
-
}
|
|
308
|
-
</script>
|
|
309
|
-
</head>
|
|
310
|
-
<body
|
|
311
|
-
class="min-h-screen flex items-center justify-center bg-black dark:bg-white transition-colors relative overflow-hidden"
|
|
312
|
-
>
|
|
313
|
-
<button
|
|
314
|
-
id="themeToggle"
|
|
315
|
-
class="fixed top-5 right-5 w-8 h-8 flex items-center justify-center cursor-pointer opacity-40 hover:opacity-100 transition-opacity"
|
|
316
|
-
>
|
|
317
|
-
<svg
|
|
318
|
-
id="sunIcon"
|
|
319
|
-
class="w-5 h-5 hidden stroke-white dark:stroke-black"
|
|
320
|
-
fill="none"
|
|
321
|
-
viewBox="0 0 24 24"
|
|
322
|
-
stroke-width="2"
|
|
323
|
-
>
|
|
324
|
-
<circle cx="12" cy="12" r="4" />
|
|
325
|
-
<path
|
|
326
|
-
d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41m11.32-11.32l1.41-1.41"
|
|
327
|
-
/>
|
|
328
|
-
</svg>
|
|
329
|
-
<svg
|
|
330
|
-
id="moonIcon"
|
|
331
|
-
class="w-5 h-5 stroke-white dark:stroke-black"
|
|
332
|
-
fill="none"
|
|
333
|
-
viewBox="0 0 24 24"
|
|
334
|
-
stroke-width="2"
|
|
335
|
-
>
|
|
336
|
-
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
337
|
-
</svg>
|
|
338
|
-
</button>
|
|
339
|
-
|
|
340
|
-
<!-- Subtle background enhancements: vignette + grid (theme-aware) -->
|
|
341
|
-
<div class="pointer-events-none absolute inset-0 z-0">
|
|
342
|
-
<!-- Light mode vignette (white on black) -->
|
|
343
|
-
<div
|
|
344
|
-
class="absolute inset-0 dark:hidden"
|
|
345
|
-
style="
|
|
346
|
-
background-image: radial-gradient(
|
|
347
|
-
60% 60% at 50% 30%,
|
|
348
|
-
rgba(255, 255, 255, 0.1) 0%,
|
|
349
|
-
rgba(0, 0, 0, 0) 70%
|
|
350
|
-
);
|
|
351
|
-
"
|
|
352
|
-
></div>
|
|
353
|
-
<!-- Dark mode vignette (black on white) -->
|
|
354
|
-
<div
|
|
355
|
-
class="absolute inset-0 hidden dark:block"
|
|
356
|
-
style="
|
|
357
|
-
background-image: radial-gradient(
|
|
358
|
-
60% 60% at 50% 30%,
|
|
359
|
-
rgba(0, 0, 0, 0.08) 0%,
|
|
360
|
-
rgba(0, 0, 0, 0) 70%
|
|
361
|
-
);
|
|
362
|
-
"
|
|
363
|
-
></div>
|
|
364
|
-
<!-- Light mode grid (white on black) -->
|
|
365
|
-
<div
|
|
366
|
-
class="absolute inset-0 dark:hidden"
|
|
367
|
-
style="
|
|
368
|
-
background-image:
|
|
369
|
-
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
|
370
|
-
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
|
|
371
|
-
background-size: 20px 20px;
|
|
372
|
-
"
|
|
373
|
-
></div>
|
|
374
|
-
<!-- Dark mode grid (black on white) -->
|
|
375
|
-
<div
|
|
376
|
-
class="absolute inset-0 hidden dark:block"
|
|
377
|
-
style="
|
|
378
|
-
background-image:
|
|
379
|
-
linear-gradient(rgba(0, 0, 0, 0.02) 1px, transparent 1px),
|
|
380
|
-
linear-gradient(90deg, rgba(0, 0, 0, 0.02) 1px, transparent 1px);
|
|
381
|
-
background-size: 20px 20px;
|
|
382
|
-
"
|
|
383
|
-
></div>
|
|
384
|
-
</div>
|
|
385
|
-
|
|
386
|
-
<div class="max-w-2xl mx-auto px-6 py-12 relative z-10">
|
|
387
|
-
<div class="text-center space-y-8">
|
|
388
|
-
<div>
|
|
389
|
-
<h1
|
|
390
|
-
class="text-4xl md:text-5xl font-bold text-white dark:text-black mb-3"
|
|
391
|
-
style="font-family: 'Tomorrow', sans-serif; letter-spacing: -0.02em"
|
|
392
|
-
>
|
|
393
|
-
{{GAME_NAME}}
|
|
394
|
-
</h1>
|
|
395
|
-
<p
|
|
396
|
-
class="text-lg text-gray-500 dark:text-gray-500"
|
|
397
|
-
style="font-family: 'VT323', monospace; letter-spacing: 0.1em"
|
|
398
|
-
>
|
|
399
|
-
GAME BACKEND API
|
|
400
|
-
</p>
|
|
401
|
-
</div>
|
|
402
|
-
|
|
403
|
-
<a
|
|
404
|
-
href="/api"
|
|
405
|
-
class="inline-block px-8 py-2.5 bg-white dark:bg-black text-black dark:text-white rounded border-2 border-white dark:border-black hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors"
|
|
406
|
-
style="font-family: 'VT323', monospace; letter-spacing: 0.05em; font-size: 1rem"
|
|
407
|
-
>
|
|
408
|
-
VIEW ROUTES \u2192
|
|
409
|
-
</a>
|
|
410
|
-
|
|
411
|
-
<div
|
|
412
|
-
class="pt-8 text-xs text-gray-600 dark:text-gray-500"
|
|
413
|
-
style="font-family: 'VT323', monospace; letter-spacing: 0.05em"
|
|
414
|
-
>
|
|
415
|
-
POWERED BY
|
|
416
|
-
<a
|
|
417
|
-
href="{{PLAYCADEMY_HUB_URL}}"
|
|
418
|
-
target="_blank"
|
|
419
|
-
rel="noopener noreferrer"
|
|
420
|
-
class="font-bold hover:text-gray-500 dark:hover:text-gray-600 transition-colors underline decoration-dotted underline-offset-2"
|
|
421
|
-
>PLAYCADEMY</a
|
|
422
|
-
>
|
|
423
|
-
</div>
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
|
|
427
|
-
<script>
|
|
428
|
-
const html = document.documentElement
|
|
429
|
-
const toggle = document.getElementById('themeToggle')
|
|
430
|
-
const sun = document.getElementById('sunIcon')
|
|
431
|
-
const moon = document.getElementById('moonIcon')
|
|
432
|
-
|
|
433
|
-
function setTheme(isDark) {
|
|
434
|
-
if (isDark) {
|
|
435
|
-
html.classList.add('dark')
|
|
436
|
-
sun.classList.remove('hidden')
|
|
437
|
-
moon.classList.add('hidden')
|
|
438
|
-
} else {
|
|
439
|
-
html.classList.remove('dark')
|
|
440
|
-
sun.classList.add('hidden')
|
|
441
|
-
moon.classList.remove('hidden')
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Initialize theme from localStorage or system preference
|
|
446
|
-
const savedTheme = localStorage.getItem('theme')
|
|
447
|
-
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
448
|
-
const isDark = savedTheme === 'dark' || (!savedTheme && prefersDark)
|
|
449
|
-
setTheme(isDark)
|
|
450
|
-
|
|
451
|
-
// Toggle on click
|
|
452
|
-
toggle.addEventListener('click', () => {
|
|
453
|
-
const willBeDark = !html.classList.contains('dark')
|
|
454
|
-
setTheme(willBeDark)
|
|
455
|
-
localStorage.setItem('theme', willBeDark ? 'dark' : 'light')
|
|
456
|
-
})
|
|
457
|
-
</script>
|
|
458
|
-
</body>
|
|
459
|
-
</html>
|
|
460
|
-
`;
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
// ../edge-play/src/routes/root.ts
|
|
465
|
-
var root_exports = {};
|
|
466
|
-
__export(root_exports, {
|
|
467
|
-
GET: () => GET
|
|
468
|
-
});
|
|
469
|
-
async function GET(c) {
|
|
470
|
-
const config = c.get("config");
|
|
471
|
-
const html = root_default.toString().replace(/\{\{GAME_NAME\}\}/g, config.name).replace(/\{\{PLAYCADEMY_HUB_URL\}\}/g, c.env.PLAYCADEMY_BASE_URL);
|
|
472
|
-
return c.html(html);
|
|
473
|
-
}
|
|
474
|
-
var init_root2 = __esm({
|
|
475
|
-
"../edge-play/src/routes/root.ts"() {
|
|
476
|
-
"use strict";
|
|
477
|
-
init_root();
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// ../edge-play/src/routes/index.ts
|
|
482
|
-
var routes_exports = {};
|
|
483
|
-
__export(routes_exports, {
|
|
484
|
-
GET: () => GET2
|
|
485
|
-
});
|
|
486
|
-
async function GET2(c) {
|
|
487
|
-
const config = c.get("config");
|
|
488
|
-
const customRoutes = c.get("customRoutes") || [];
|
|
489
|
-
const routes = [`GET ${ROUTES.INDEX}`, `GET ${ROUTES.HEALTH}`];
|
|
490
|
-
if (config.integrations?.timeback) {
|
|
491
|
-
routes.push(`POST ${ROUTES.TIMEBACK.END_ACTIVITY}`);
|
|
492
|
-
}
|
|
493
|
-
for (const route of customRoutes) {
|
|
494
|
-
const methods = route.methods?.join(", ") || "*";
|
|
495
|
-
routes.push(`${methods} ${route.path}`);
|
|
496
|
-
}
|
|
497
|
-
return c.json({
|
|
498
|
-
name: config.name,
|
|
499
|
-
routes
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
var init_routes = __esm({
|
|
503
|
-
"../edge-play/src/routes/index.ts"() {
|
|
504
|
-
"use strict";
|
|
505
|
-
init_constants();
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
// ../edge-play/src/routes/health.ts
|
|
510
|
-
var health_exports = {};
|
|
511
|
-
__export(health_exports, {
|
|
512
|
-
GET: () => GET3
|
|
513
|
-
});
|
|
514
|
-
async function GET3(c) {
|
|
515
|
-
const config = c.get("config");
|
|
516
|
-
const sdk = c.get("sdk");
|
|
517
|
-
return c.json({
|
|
518
|
-
status: "ok",
|
|
519
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
520
|
-
// Environment check
|
|
521
|
-
env: {
|
|
522
|
-
hasApiKey: !!c.env.PLAYCADEMY_API_KEY,
|
|
523
|
-
hasGameId: !!c.env.GAME_ID,
|
|
524
|
-
hasBaseUrl: !!c.env.PLAYCADEMY_BASE_URL
|
|
525
|
-
},
|
|
526
|
-
// Config presence
|
|
527
|
-
config: {
|
|
528
|
-
hasConfig: !!config,
|
|
529
|
-
hasIntegrations: !!config?.integrations
|
|
530
|
-
},
|
|
531
|
-
// TimeBack status
|
|
532
|
-
timeback: {
|
|
533
|
-
enabled: !!config?.integrations?.timeback,
|
|
534
|
-
courseIdFetched: !!sdk?.timeback?.courseId
|
|
535
|
-
},
|
|
536
|
-
// Custom routes info
|
|
537
|
-
customRoutes: c.get("customRoutes")?.map((r) => ({
|
|
538
|
-
path: r.path,
|
|
539
|
-
methods: r.methods
|
|
540
|
-
})) || []
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
var init_health = __esm({
|
|
544
|
-
"../edge-play/src/routes/health.ts"() {
|
|
545
|
-
"use strict";
|
|
259
|
+
function findFilesByExtension(dir, extension) {
|
|
260
|
+
if (!existsSync(dir)) {
|
|
261
|
+
return [];
|
|
546
262
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
// ../edge-play/src/routes/integrations/timeback/end-activity.ts
|
|
550
|
-
var end_activity_exports = {};
|
|
551
|
-
__export(end_activity_exports, {
|
|
552
|
-
POST: () => POST
|
|
553
|
-
});
|
|
554
|
-
import { verifyGameToken } from "@playcademy/sdk/server";
|
|
555
|
-
function getConfig(c) {
|
|
556
|
-
const config = c.get("config");
|
|
557
|
-
const timebackConfig = config?.integrations?.timeback;
|
|
558
|
-
if (!timebackConfig) throw new Error("TimeBack integration not found");
|
|
559
|
-
return config;
|
|
560
|
-
}
|
|
561
|
-
function enrichActivityData(activityData, config, c) {
|
|
562
|
-
const appName = activityData.appName || config?.name;
|
|
563
|
-
const subject = activityData.subject || config?.integrations?.timeback?.course?.defaultSubject || config?.integrations?.timeback?.course?.subjects?.[0];
|
|
564
|
-
const sensorUrl = activityData.sensorUrl || new URL(c.req.url).origin;
|
|
565
|
-
if (!appName) throw new Error("App name is required");
|
|
566
|
-
if (!subject) throw new Error("Subject is required");
|
|
567
|
-
if (!sensorUrl) throw new Error("Sensor URL is required");
|
|
568
|
-
return { ...activityData, appName, subject, sensorUrl };
|
|
569
|
-
}
|
|
570
|
-
async function POST(c) {
|
|
263
|
+
const ext = extension.toLowerCase().replace(/^\./, "");
|
|
264
|
+
const files = [];
|
|
571
265
|
try {
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
return c.json({ error: "activityId is required" }, 400);
|
|
583
|
-
}
|
|
584
|
-
if (typeof scoreData?.correctQuestions !== "number" || typeof scoreData?.totalQuestions !== "number") {
|
|
585
|
-
return c.json({ error: "correctQuestions and totalQuestions are required" }, 400);
|
|
586
|
-
}
|
|
587
|
-
if (typeof timingData?.durationSeconds !== "number") {
|
|
588
|
-
return c.json({ error: "durationSeconds is required" }, 400);
|
|
266
|
+
const entries = readdirSync(dir);
|
|
267
|
+
for (const entry of entries) {
|
|
268
|
+
const fullPath = resolve(dir, entry);
|
|
269
|
+
const stat = statSync(fullPath);
|
|
270
|
+
if (stat.isFile()) {
|
|
271
|
+
const fileExt = getFileExtension(entry);
|
|
272
|
+
if (fileExt === ext) {
|
|
273
|
+
files.push(fullPath);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
589
276
|
}
|
|
590
|
-
|
|
591
|
-
const enrichedActivityData = enrichActivityData(activityData, config, c);
|
|
592
|
-
const sdk = c.get("sdk");
|
|
593
|
-
const result = await sdk.timeback.endActivity(user.timeback_id, {
|
|
594
|
-
activityData: enrichedActivityData,
|
|
595
|
-
scoreData,
|
|
596
|
-
timingData,
|
|
597
|
-
xpEarned
|
|
598
|
-
});
|
|
599
|
-
return c.json(result);
|
|
600
|
-
} catch (error) {
|
|
601
|
-
console.error("[TimeBack End Activity] Error:", error);
|
|
602
|
-
return c.json(
|
|
603
|
-
{
|
|
604
|
-
error: "Failed to end activity",
|
|
605
|
-
message: error instanceof Error ? error.message : String(error),
|
|
606
|
-
stack: error instanceof Error ? error.stack : void 0
|
|
607
|
-
},
|
|
608
|
-
500
|
|
609
|
-
);
|
|
277
|
+
} catch {
|
|
610
278
|
}
|
|
279
|
+
return files;
|
|
611
280
|
}
|
|
612
|
-
var
|
|
613
|
-
"../
|
|
281
|
+
var init_file_loader = __esm({
|
|
282
|
+
"../utils/src/file-loader.ts"() {
|
|
614
283
|
"use strict";
|
|
284
|
+
init_package_json();
|
|
615
285
|
}
|
|
616
286
|
});
|
|
617
287
|
|
|
@@ -619,6 +289,27 @@ var init_end_activity = __esm({
|
|
|
619
289
|
init_file_loader();
|
|
620
290
|
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
621
291
|
|
|
292
|
+
// src/constants/api.ts
|
|
293
|
+
var DEFAULT_API_ROUTES_DIRECTORY = "server/api";
|
|
294
|
+
|
|
295
|
+
// src/constants/paths.ts
|
|
296
|
+
var CLI_DIRECTORIES = {
|
|
297
|
+
/** Root directory for CLI artifacts in workspace */
|
|
298
|
+
WORKSPACE: ".playcademy",
|
|
299
|
+
/** Database directory within workspace */
|
|
300
|
+
DATABASE: ".playcademy/db"
|
|
301
|
+
};
|
|
302
|
+
var CLI_FILES = {
|
|
303
|
+
/** Auth store file in user config directory */
|
|
304
|
+
AUTH_STORE: "auth.json",
|
|
305
|
+
/** Games deployment info store */
|
|
306
|
+
GAMES_STORE: "games.json",
|
|
307
|
+
/** Dev server PID file */
|
|
308
|
+
DEV_SERVER_PID: "dev-server.pid",
|
|
309
|
+
/** Initial database file (before miniflare) */
|
|
310
|
+
INITIAL_DATABASE: "initial.sqlite"
|
|
311
|
+
};
|
|
312
|
+
|
|
622
313
|
// src/constants/timeback.ts
|
|
623
314
|
var CONFIG_FILE_NAMES = [
|
|
624
315
|
"playcademy.config.js",
|
|
@@ -661,6 +352,9 @@ var CORE_GAME_UUIDS = {
|
|
|
661
352
|
PLAYGROUND: "00000000-0000-0000-0000-000000000001"
|
|
662
353
|
};
|
|
663
354
|
|
|
355
|
+
// src/constants/index.ts
|
|
356
|
+
var CLOUDFLARE_COMPATIBILITY_DATE = "2024-01-01";
|
|
357
|
+
|
|
664
358
|
// src/lib/config/loader.ts
|
|
665
359
|
var ConfigError = class extends Error {
|
|
666
360
|
constructor(message, field, suggestion) {
|
|
@@ -725,27 +419,17 @@ async function findConfigPath(configPath) {
|
|
|
725
419
|
}
|
|
726
420
|
return result.path;
|
|
727
421
|
}
|
|
422
|
+
async function loadConfigFile(path2) {
|
|
423
|
+
if (path2.endsWith(".json")) {
|
|
424
|
+
return await loadFile(path2, { required: true, parseJson: true });
|
|
425
|
+
}
|
|
426
|
+
const module = await import(`${path2}?t=${Date.now()}`);
|
|
427
|
+
return module.default || module;
|
|
428
|
+
}
|
|
728
429
|
async function loadConfig(configPath) {
|
|
729
430
|
try {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if (configPath) {
|
|
733
|
-
actualPath = resolve2(configPath);
|
|
734
|
-
if (actualPath.endsWith(".json")) {
|
|
735
|
-
config = await loadFile(actualPath, { required: true, parseJson: true });
|
|
736
|
-
} else {
|
|
737
|
-
const module = await import(`${actualPath}?t=${Date.now()}`);
|
|
738
|
-
config = module.default || module;
|
|
739
|
-
}
|
|
740
|
-
} else {
|
|
741
|
-
actualPath = await findConfigPath();
|
|
742
|
-
if (actualPath.endsWith(".json")) {
|
|
743
|
-
config = await loadFile(actualPath, { required: true, parseJson: true });
|
|
744
|
-
} else {
|
|
745
|
-
const module = await import(`${actualPath}?t=${Date.now()}`);
|
|
746
|
-
config = module.default || module;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
431
|
+
const actualPath = configPath ? resolve2(configPath) : await findConfigPath();
|
|
432
|
+
const config = await loadConfigFile(actualPath);
|
|
749
433
|
if (!config || typeof config !== "object") {
|
|
750
434
|
throw new ConfigError(
|
|
751
435
|
"Config file must export or contain a valid configuration object",
|
|
@@ -879,35 +563,9 @@ function processConfigVariables(config) {
|
|
|
879
563
|
}
|
|
880
564
|
|
|
881
565
|
// src/lib/dev/server.ts
|
|
882
|
-
import {
|
|
883
|
-
import {
|
|
884
|
-
import {
|
|
885
|
-
import { cors } from "hono/cors";
|
|
886
|
-
import { logger as honoLogger } from "hono/logger";
|
|
887
|
-
|
|
888
|
-
// ../edge-play/src/register-routes.ts
|
|
889
|
-
init_constants();
|
|
890
|
-
async function registerBuiltinRoutes(app, integrations) {
|
|
891
|
-
const root = await Promise.resolve().then(() => (init_root2(), root_exports));
|
|
892
|
-
app.get("/", root.GET);
|
|
893
|
-
const routesIndex = await Promise.resolve().then(() => (init_routes(), routes_exports));
|
|
894
|
-
app.get(ROUTES.INDEX, routesIndex.GET);
|
|
895
|
-
const health = await Promise.resolve().then(() => (init_health(), health_exports));
|
|
896
|
-
app.get(ROUTES.HEALTH, health.GET);
|
|
897
|
-
if (integrations?.timeback) {
|
|
898
|
-
const [endActivity] = await Promise.all([
|
|
899
|
-
Promise.resolve().then(() => (init_end_activity(), end_activity_exports))
|
|
900
|
-
// ... other routes
|
|
901
|
-
]);
|
|
902
|
-
app.post(ROUTES.TIMEBACK.END_ACTIVITY, endActivity.POST);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// ../edge-play/src/index.ts
|
|
907
|
-
init_constants();
|
|
908
|
-
|
|
909
|
-
// src/lib/dev/server.ts
|
|
910
|
-
import { PlaycademyClient as PlaycademyClient2 } from "@playcademy/sdk/server";
|
|
566
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
567
|
+
import { join as join4 } from "path";
|
|
568
|
+
import { Miniflare } from "miniflare";
|
|
911
569
|
|
|
912
570
|
// src/lib/core/client.ts
|
|
913
571
|
import { PlaycademyClient } from "@playcademy/sdk";
|
|
@@ -1121,6 +779,16 @@ var logger = {
|
|
|
1121
779
|
}
|
|
1122
780
|
};
|
|
1123
781
|
|
|
782
|
+
// src/lib/core/errors.ts
|
|
783
|
+
function getErrorMessage(error) {
|
|
784
|
+
if (error instanceof Error) return error.message;
|
|
785
|
+
if (typeof error === "string") return error;
|
|
786
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
787
|
+
return String(error.message);
|
|
788
|
+
}
|
|
789
|
+
return "Unknown error";
|
|
790
|
+
}
|
|
791
|
+
|
|
1124
792
|
// ../utils/src/ansi.ts
|
|
1125
793
|
var isInteractive = typeof process !== "undefined" && process.stdout?.isTTY && !process.env.CI && process.env.TERM !== "dumb";
|
|
1126
794
|
|
|
@@ -1148,11 +816,275 @@ import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
|
1148
816
|
import { fileURLToPath } from "url";
|
|
1149
817
|
var currentDir = dirname3(fileURLToPath(import.meta.url));
|
|
1150
818
|
|
|
819
|
+
// src/lib/deploy/bundle.ts
|
|
820
|
+
import { existsSync as existsSync2 } from "fs";
|
|
821
|
+
import { join as join2 } from "path";
|
|
822
|
+
|
|
823
|
+
// ../edge-play/src/entry.ts
|
|
824
|
+
var entry_default = "/**\n * Game Backend Entry Point\n *\n * This file is the main entry point for deployed game backends.\n * It creates a Hono app and registers all enabled integration routes.\n *\n * Bundled with esbuild and deployed to Cloudflare Workers (or AWS Lambda).\n * Config is injected at build time via esbuild's `define` option.\n */\n\nimport { Hono } from 'hono'\nimport { cors } from 'hono/cors'\n\nimport { PlaycademyClient } from '@playcademy/sdk/server'\n\nimport { ENV_VARS } from './constants'\nimport { registerBuiltinRoutes } from './register-routes'\n\nimport type { PlaycademyConfig } from '@playcademy/sdk/server'\nimport type { HonoEnv } from './types'\n\n/**\n * Config injected at build time by esbuild\n *\n * The `declare const` tells TypeScript \"this exists at runtime, trust me.\"\n * During bundling, esbuild's `define` option does literal text replacement:\n *\n * Example bundling:\n * Source: if (PLAYCADEMY_CONFIG.integrations.timeback) { ... }\n * Define: { 'PLAYCADEMY_CONFIG': JSON.stringify({ integrations: { timeback: {...} } }) }\n * Output: if ({\"integrations\":{\"timeback\":{...}}}.integrations.timeback) { ... }\n *\n * This enables tree-shaking: if timeback is not configured, those code paths are removed.\n * The bundled Worker only includes the routes that are actually enabled.\n */\ndeclare const PLAYCADEMY_CONFIG: PlaycademyConfig & {\n customRoutes?: Array<{ path: string; file: string }>\n}\n\n// XXX: Polyfill process global for SDK compatibility\n// SDK code may reference process.env without importing it\n// @ts-expect-error - Adding global for Worker environment\nglobalThis.process = {\n env: {}, // Populated per-request from Worker env bindings\n cwd: () => '/',\n}\n\nconst app = new Hono<HonoEnv>()\n\n// TODO: Harden CORS in production - restrict to trusted origins:\n// - Game's assetBundleBase (for hosted games)\n// - Game's externalUrl (for external games)\n// - Platform frontend domains (hub.playcademy.com, hub.dev.playcademy.net)\n// This would require passing game metadata through env bindings during deployment\napp.use(\n '*',\n cors({\n origin: '*', // Permissive for now\n allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n allowHeaders: ['Content-Type', 'Authorization'],\n }),\n)\n\nlet sdkPromise: Promise<PlaycademyClient> | null = null\n\napp.use('*', async (c, next) => {\n // Populate process.env from Worker bindings for SDK compatibility\n globalThis.process.env = {\n [ENV_VARS.PLAYCADEMY_API_KEY]: c.env.PLAYCADEMY_API_KEY,\n [ENV_VARS.GAME_ID]: c.env.GAME_ID,\n [ENV_VARS.PLAYCADEMY_BASE_URL]: c.env.PLAYCADEMY_BASE_URL,\n }\n\n // Set config for all routes\n c.set('config', PLAYCADEMY_CONFIG)\n c.set('customRoutes', PLAYCADEMY_CONFIG.customRoutes || [])\n\n await next()\n})\n\n// Initialize SDK lazily on first request\napp.use('*', async (c, next) => {\n if (!sdkPromise) {\n sdkPromise = PlaycademyClient.init({\n apiKey: c.env[ENV_VARS.PLAYCADEMY_API_KEY],\n gameId: c.env[ENV_VARS.GAME_ID],\n baseUrl: c.env[ENV_VARS.PLAYCADEMY_BASE_URL],\n config: PLAYCADEMY_CONFIG,\n })\n }\n\n c.set('sdk', await sdkPromise)\n await next()\n})\n\n/**\n * Register built-in integration routes based on enabled integrations\n *\n * This function conditionally imports and registers routes like:\n * - POST /api/integrations/timeback/end-activity (if timeback enabled)\n * - GET /api/health (always included)\n *\n * Uses dynamic imports for tree-shaking: if an integration is not enabled,\n * its route code is completely removed from the bundle.\n */\nawait registerBuiltinRoutes(app, PLAYCADEMY_CONFIG.integrations)\n\nexport default app\n";
|
|
825
|
+
|
|
826
|
+
// ../utils/src/path.ts
|
|
827
|
+
import fs from "node:fs";
|
|
828
|
+
import path from "node:path";
|
|
829
|
+
|
|
830
|
+
// ../logger/src/index.ts
|
|
831
|
+
var isBrowser = () => {
|
|
832
|
+
const g = globalThis;
|
|
833
|
+
return typeof g.window !== "undefined" && typeof g.document !== "undefined";
|
|
834
|
+
};
|
|
835
|
+
var isProduction = () => {
|
|
836
|
+
return typeof process !== "undefined" && process.env.NODE_ENV === "production";
|
|
837
|
+
};
|
|
838
|
+
var isDevelopment = () => {
|
|
839
|
+
return typeof process !== "undefined" && process.env.NODE_ENV === "development";
|
|
840
|
+
};
|
|
841
|
+
var isInteractiveTTY = () => {
|
|
842
|
+
return typeof process !== "undefined" && Boolean(process.stdout && process.stdout.isTTY);
|
|
843
|
+
};
|
|
844
|
+
var detectOutputFormat = () => {
|
|
845
|
+
if (isBrowser()) {
|
|
846
|
+
return "browser";
|
|
847
|
+
}
|
|
848
|
+
if (typeof process !== "undefined" && process.env.LOG_FORMAT === "json") {
|
|
849
|
+
return "json-single-line";
|
|
850
|
+
}
|
|
851
|
+
if (typeof process !== "undefined" && process.env.LOG_PRETTY === "true" && isDevelopment()) {
|
|
852
|
+
return "json-pretty";
|
|
853
|
+
}
|
|
854
|
+
const colorPreference = typeof process !== "undefined" ? (process.env.LOG_COLOR ?? "auto").toLowerCase() : "auto";
|
|
855
|
+
if (colorPreference === "always" && !isProduction()) {
|
|
856
|
+
return "color-tty";
|
|
857
|
+
}
|
|
858
|
+
if (colorPreference === "never") {
|
|
859
|
+
return "json-single-line";
|
|
860
|
+
}
|
|
861
|
+
if (isProduction()) {
|
|
862
|
+
return "json-single-line";
|
|
863
|
+
}
|
|
864
|
+
if (isDevelopment() && isInteractiveTTY()) {
|
|
865
|
+
return "color-tty";
|
|
866
|
+
}
|
|
867
|
+
return "json-single-line";
|
|
868
|
+
};
|
|
869
|
+
var colors2 = {
|
|
870
|
+
reset: "\x1B[0m",
|
|
871
|
+
dim: "\x1B[2m",
|
|
872
|
+
red: "\x1B[31m",
|
|
873
|
+
yellow: "\x1B[33m",
|
|
874
|
+
blue: "\x1B[34m",
|
|
875
|
+
cyan: "\x1B[36m",
|
|
876
|
+
gray: "\x1B[90m"
|
|
877
|
+
};
|
|
878
|
+
var getLevelColor = (level) => {
|
|
879
|
+
switch (level) {
|
|
880
|
+
case "debug":
|
|
881
|
+
return colors2.blue;
|
|
882
|
+
case "info":
|
|
883
|
+
return colors2.cyan;
|
|
884
|
+
case "warn":
|
|
885
|
+
return colors2.yellow;
|
|
886
|
+
case "error":
|
|
887
|
+
return colors2.red;
|
|
888
|
+
default:
|
|
889
|
+
return colors2.reset;
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
var formatBrowserOutput = (level, message, context2) => {
|
|
893
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
894
|
+
const levelUpper = level.toUpperCase();
|
|
895
|
+
const consoleMethod = getConsoleMethod(level);
|
|
896
|
+
if (context2 && Object.keys(context2).length > 0) {
|
|
897
|
+
consoleMethod(`[${timestamp}] ${levelUpper}`, message, context2);
|
|
898
|
+
} else {
|
|
899
|
+
consoleMethod(`[${timestamp}] ${levelUpper}`, message);
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
var formatColorTTY = (level, message, context2) => {
|
|
903
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
904
|
+
const levelColor = getLevelColor(level);
|
|
905
|
+
const levelUpper = level.toUpperCase().padEnd(5);
|
|
906
|
+
const consoleMethod = getConsoleMethod(level);
|
|
907
|
+
const coloredPrefix = `${colors2.dim}[${timestamp}]${colors2.reset} ${levelColor}${levelUpper}${colors2.reset}`;
|
|
908
|
+
if (context2 && Object.keys(context2).length > 0) {
|
|
909
|
+
consoleMethod(`${coloredPrefix} ${message}`, context2);
|
|
910
|
+
} else {
|
|
911
|
+
consoleMethod(`${coloredPrefix} ${message}`);
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
var formatJSONSingleLine = (level, message, context2) => {
|
|
915
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
916
|
+
const logEntry = {
|
|
917
|
+
timestamp,
|
|
918
|
+
level: level.toUpperCase(),
|
|
919
|
+
message,
|
|
920
|
+
...context2 && Object.keys(context2).length > 0 && { context: context2 }
|
|
921
|
+
};
|
|
922
|
+
const consoleMethod = getConsoleMethod(level);
|
|
923
|
+
consoleMethod(JSON.stringify(logEntry));
|
|
924
|
+
};
|
|
925
|
+
var formatJSONPretty = (level, message, context2) => {
|
|
926
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
927
|
+
const logEntry = {
|
|
928
|
+
timestamp,
|
|
929
|
+
level: level.toUpperCase(),
|
|
930
|
+
message,
|
|
931
|
+
...context2 && Object.keys(context2).length > 0 && { context: context2 }
|
|
932
|
+
};
|
|
933
|
+
const consoleMethod = getConsoleMethod(level);
|
|
934
|
+
consoleMethod(JSON.stringify(logEntry, null, 2));
|
|
935
|
+
};
|
|
936
|
+
var getConsoleMethod = (level) => {
|
|
937
|
+
switch (level) {
|
|
938
|
+
case "debug":
|
|
939
|
+
return console.debug;
|
|
940
|
+
case "info":
|
|
941
|
+
return console.info;
|
|
942
|
+
case "warn":
|
|
943
|
+
return console.warn;
|
|
944
|
+
case "error":
|
|
945
|
+
return console.error;
|
|
946
|
+
default:
|
|
947
|
+
return console.log;
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
var levelPriority = {
|
|
951
|
+
debug: 0,
|
|
952
|
+
info: 1,
|
|
953
|
+
warn: 2,
|
|
954
|
+
error: 3
|
|
955
|
+
};
|
|
956
|
+
var getMinimumLogLevel = () => {
|
|
957
|
+
const envLevel = typeof process !== "undefined" ? (process.env.LOG_LEVEL ?? "").toLowerCase() : "";
|
|
958
|
+
if (envLevel && ["debug", "info", "warn", "error"].includes(envLevel)) {
|
|
959
|
+
return envLevel;
|
|
960
|
+
}
|
|
961
|
+
return isProduction() ? "info" : "debug";
|
|
962
|
+
};
|
|
963
|
+
var shouldLog = (level) => {
|
|
964
|
+
const minLevel = getMinimumLogLevel();
|
|
965
|
+
return levelPriority[level] >= levelPriority[minLevel];
|
|
966
|
+
};
|
|
967
|
+
var performLog = (level, message, context2) => {
|
|
968
|
+
if (!shouldLog(level)) return;
|
|
969
|
+
const outputFormat = detectOutputFormat();
|
|
970
|
+
switch (outputFormat) {
|
|
971
|
+
case "browser":
|
|
972
|
+
formatBrowserOutput(level, message, context2);
|
|
973
|
+
break;
|
|
974
|
+
case "color-tty":
|
|
975
|
+
formatColorTTY(level, message, context2);
|
|
976
|
+
break;
|
|
977
|
+
case "json-single-line":
|
|
978
|
+
formatJSONSingleLine(level, message, context2);
|
|
979
|
+
break;
|
|
980
|
+
case "json-pretty":
|
|
981
|
+
formatJSONPretty(level, message, context2);
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
var createLogger = () => {
|
|
986
|
+
return {
|
|
987
|
+
debug: (message, context2) => performLog("debug", message, context2),
|
|
988
|
+
info: (message, context2) => performLog("info", message, context2),
|
|
989
|
+
warn: (message, context2) => performLog("warn", message, context2),
|
|
990
|
+
error: (message, context2) => performLog("error", message, context2),
|
|
991
|
+
log: performLog
|
|
992
|
+
};
|
|
993
|
+
};
|
|
994
|
+
var log = createLogger();
|
|
995
|
+
|
|
996
|
+
// ../utils/src/path.ts
|
|
997
|
+
function findMonorepoRootInternal(startDir) {
|
|
998
|
+
let currentDir2 = startDir;
|
|
999
|
+
while (true) {
|
|
1000
|
+
const packageJsonPath = path.join(currentDir2, "package.json");
|
|
1001
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
1002
|
+
try {
|
|
1003
|
+
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
|
|
1004
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
1005
|
+
if (packageJson && typeof packageJson === "object" && "workspaces" in packageJson) {
|
|
1006
|
+
return currentDir2;
|
|
1007
|
+
}
|
|
1008
|
+
} catch (parseError) {
|
|
1009
|
+
log.warn(`[utils/paths] Error parsing ${packageJsonPath}:`, {
|
|
1010
|
+
error: parseError
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const parentDir = path.dirname(currentDir2);
|
|
1015
|
+
if (parentDir === currentDir2) {
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
"Could not find monorepo root (package.json with workspaces) starting from " + startDir
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
currentDir2 = parentDir;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
var memoizedRoot;
|
|
1024
|
+
function getMonorepoRoot() {
|
|
1025
|
+
if (memoizedRoot) return memoizedRoot;
|
|
1026
|
+
try {
|
|
1027
|
+
memoizedRoot = findMonorepoRootInternal(process.cwd());
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
memoizedRoot = process.cwd();
|
|
1030
|
+
if (typeof Bun !== "undefined") {
|
|
1031
|
+
log.warn(
|
|
1032
|
+
"[utils/paths] Could not locate monorepo root via workspace package.json scan. Falling back to process.cwd() (",
|
|
1033
|
+
{ root: memoizedRoot, error }
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return memoizedRoot;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/lib/build/plugins.ts
|
|
1041
|
+
function textLoaderPlugin() {
|
|
1042
|
+
return {
|
|
1043
|
+
name: "text-loader",
|
|
1044
|
+
setup(build) {
|
|
1045
|
+
build.onLoad({ filter: /edge-play\/src\/entry\.ts$/ }, async (args) => {
|
|
1046
|
+
const fs2 = await import("fs/promises");
|
|
1047
|
+
const text = await fs2.readFile(args.path, "utf8");
|
|
1048
|
+
return {
|
|
1049
|
+
contents: `export default ${JSON.stringify(text)}`,
|
|
1050
|
+
loader: "js"
|
|
1051
|
+
};
|
|
1052
|
+
});
|
|
1053
|
+
build.onLoad({ filter: /edge-play\/src\/routes\/root\.html$/ }, async (args) => {
|
|
1054
|
+
const fs2 = await import("fs/promises");
|
|
1055
|
+
const text = await fs2.readFile(args.path, "utf8");
|
|
1056
|
+
return {
|
|
1057
|
+
contents: `export default ${JSON.stringify(text)}`,
|
|
1058
|
+
loader: "js"
|
|
1059
|
+
};
|
|
1060
|
+
});
|
|
1061
|
+
build.onLoad({ filter: /templates\/sample-route\.ts$/ }, async (args) => {
|
|
1062
|
+
const fs2 = await import("fs/promises");
|
|
1063
|
+
const text = await fs2.readFile(args.path, "utf8");
|
|
1064
|
+
return {
|
|
1065
|
+
contents: `export default ${JSON.stringify(text)}`,
|
|
1066
|
+
loader: "js"
|
|
1067
|
+
};
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1151
1073
|
// src/lib/dev/routes.ts
|
|
1152
1074
|
init_file_loader();
|
|
1153
1075
|
import { mkdir, writeFile } from "fs/promises";
|
|
1154
1076
|
import { tmpdir } from "os";
|
|
1155
1077
|
import { join, relative } from "path";
|
|
1078
|
+
|
|
1079
|
+
// src/lib/deploy/hash.ts
|
|
1080
|
+
import { createHash } from "crypto";
|
|
1081
|
+
init_file_loader();
|
|
1082
|
+
function hashContent(content) {
|
|
1083
|
+
const contentStr = typeof content === "string" ? content : JSON.stringify(content);
|
|
1084
|
+
return createHash("sha256").update(contentStr).digest("hex");
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/lib/dev/routes.ts
|
|
1156
1088
|
async function discoverRoutes(apiDir) {
|
|
1157
1089
|
const files = scanDirectory(apiDir, {
|
|
1158
1090
|
extensions: ["ts", "js"],
|
|
@@ -1202,13 +1134,6 @@ function filePathToRoutePath(filePath) {
|
|
|
1202
1134
|
function isBun() {
|
|
1203
1135
|
return typeof Bun !== "undefined";
|
|
1204
1136
|
}
|
|
1205
|
-
function hashPath(path) {
|
|
1206
|
-
let hash = 5381;
|
|
1207
|
-
for (let i = 0; i < path.length; i++) {
|
|
1208
|
-
hash = hash * 33 ^ path.charCodeAt(i);
|
|
1209
|
-
}
|
|
1210
|
-
return (hash >>> 0).toString(36);
|
|
1211
|
-
}
|
|
1212
1137
|
async function transpileRoute(filePath) {
|
|
1213
1138
|
if (isBun() || !filePath.endsWith(".ts")) {
|
|
1214
1139
|
return filePath;
|
|
@@ -1230,29 +1155,139 @@ async function transpileRoute(filePath) {
|
|
|
1230
1155
|
}
|
|
1231
1156
|
const tempDir = join(tmpdir(), "playcademy-dev");
|
|
1232
1157
|
await mkdir(tempDir, { recursive: true });
|
|
1233
|
-
const hash =
|
|
1158
|
+
const hash = hashContent(filePath).slice(0, 12);
|
|
1234
1159
|
const jsPath = join(tempDir, `${hash}.mjs`);
|
|
1235
1160
|
await writeFile(jsPath, result.outputFiles[0].text);
|
|
1236
1161
|
return jsPath;
|
|
1237
1162
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1163
|
+
|
|
1164
|
+
// src/lib/deploy/bundle.ts
|
|
1165
|
+
var entryTemplate = entry_default;
|
|
1166
|
+
async function bundleBackend(config, options = {}) {
|
|
1167
|
+
const esbuild = await import("esbuild");
|
|
1168
|
+
const workspace = getWorkspace();
|
|
1169
|
+
const customRoutesConfig = config.integrations?.customRoutes;
|
|
1170
|
+
const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
|
|
1171
|
+
const customRoutes = await discoverRoutes(join2(workspace, customRoutesDir));
|
|
1172
|
+
const customRouteData = customRoutes.map((r) => ({
|
|
1173
|
+
path: r.path,
|
|
1174
|
+
file: r.file,
|
|
1175
|
+
// Use relative path (e.g., 'server/api/test.ts'), not absolute
|
|
1176
|
+
methods: r.methods
|
|
1177
|
+
}));
|
|
1178
|
+
const bundleConfig = {
|
|
1179
|
+
...config,
|
|
1180
|
+
customRoutes: customRouteData
|
|
1181
|
+
};
|
|
1182
|
+
const entryCode = generateEntryCode(customRouteData, customRoutesDir);
|
|
1183
|
+
const distDir = new URL(".", import.meta.url).pathname;
|
|
1184
|
+
const embeddedEdgeSrc = join2(distDir, "edge-play", "src");
|
|
1185
|
+
const monorepoRoot = getMonorepoRoot();
|
|
1186
|
+
const monorepoEdgeSrc = join2(monorepoRoot, "packages/edge-play/src");
|
|
1187
|
+
const isBuiltPackage = existsSync2(embeddedEdgeSrc);
|
|
1188
|
+
const edgePlaySrc = isBuiltPackage ? embeddedEdgeSrc : monorepoEdgeSrc;
|
|
1189
|
+
const cliPackageRoot = isBuiltPackage ? join2(distDir, "../..") : join2(monorepoRoot, "packages/cli");
|
|
1190
|
+
const cliNodeModules = isBuiltPackage ? cliPackageRoot : monorepoRoot;
|
|
1191
|
+
const workspaceNodeModules = join2(workspace, "node_modules");
|
|
1192
|
+
const result = await esbuild.build({
|
|
1193
|
+
stdin: {
|
|
1194
|
+
contents: entryCode,
|
|
1195
|
+
resolveDir: edgePlaySrc,
|
|
1196
|
+
// For relative imports like ./register-routes
|
|
1197
|
+
loader: "ts"
|
|
1198
|
+
},
|
|
1199
|
+
bundle: true,
|
|
1200
|
+
format: "esm",
|
|
1201
|
+
platform: "browser",
|
|
1202
|
+
target: "es2022",
|
|
1203
|
+
write: false,
|
|
1204
|
+
sourcemap: options.sourcemap ? "inline" : false,
|
|
1205
|
+
minify: options.minify || false,
|
|
1206
|
+
logLevel: "error",
|
|
1207
|
+
nodePaths: [workspaceNodeModules, cliNodeModules],
|
|
1208
|
+
// Check workspace first, then CLI
|
|
1209
|
+
define: {
|
|
1210
|
+
PLAYCADEMY_CONFIG: JSON.stringify(bundleConfig)
|
|
1211
|
+
},
|
|
1212
|
+
alias: {
|
|
1213
|
+
/**
|
|
1214
|
+
* @game-api alias maps to the user's custom routes directory
|
|
1215
|
+
*
|
|
1216
|
+
* This allows custom route imports in the entry code:
|
|
1217
|
+
* import * as customRoute0 from '@game-api/hello.ts'
|
|
1218
|
+
*
|
|
1219
|
+
* The alias resolves to the absolute path of the custom routes directory in the
|
|
1220
|
+
* user's game project (configured via integrations.customRoutes.directory), enabling esbuild
|
|
1221
|
+
* to bundle custom routes into the worker.
|
|
1222
|
+
*/
|
|
1223
|
+
"@game-api": join2(workspace, customRoutesDir),
|
|
1224
|
+
/**
|
|
1225
|
+
* Node.js module polyfills for Cloudflare Workers environment
|
|
1226
|
+
*
|
|
1227
|
+
* Cloudflare Workers don't have Node.js APIs (fs, path, os, etc.).
|
|
1228
|
+
* These aliases redirect Node.js imports to a polyfill that throws helpful errors.
|
|
1229
|
+
*
|
|
1230
|
+
* This prevents bundling errors and provides clear runtime messages if
|
|
1231
|
+
* user code accidentally imports Node.js modules that won't work in Workers.
|
|
1232
|
+
*/
|
|
1233
|
+
fs: join2(edgePlaySrc, "polyfills.js"),
|
|
1234
|
+
"fs/promises": join2(edgePlaySrc, "polyfills.js"),
|
|
1235
|
+
path: join2(edgePlaySrc, "polyfills.js"),
|
|
1236
|
+
os: join2(edgePlaySrc, "polyfills.js"),
|
|
1237
|
+
process: join2(edgePlaySrc, "polyfills.js")
|
|
1238
|
+
},
|
|
1239
|
+
plugins: [textLoaderPlugin()],
|
|
1240
|
+
external: []
|
|
1241
|
+
});
|
|
1242
|
+
if (!result.outputFiles?.[0]) {
|
|
1243
|
+
throw new Error("Backend bundling failed: no output");
|
|
1244
|
+
}
|
|
1245
|
+
const code = result.outputFiles[0].text;
|
|
1246
|
+
return {
|
|
1247
|
+
code,
|
|
1248
|
+
config,
|
|
1249
|
+
customRoutes: customRouteData
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function generateEntryCode(customRoutes, customRoutesDir) {
|
|
1253
|
+
if (customRoutes.length === 0) {
|
|
1254
|
+
return entryTemplate;
|
|
1255
1255
|
}
|
|
1256
|
+
const customRoutesPrefix = customRoutesDir.endsWith("/") ? customRoutesDir : `${customRoutesDir}/`;
|
|
1257
|
+
const importStatements = customRoutes.map((route, i) => {
|
|
1258
|
+
const importPath = route.file.startsWith(customRoutesPrefix) ? route.file.slice(customRoutesPrefix.length) : route.file;
|
|
1259
|
+
return `import * as customRoute${i} from '@game-api/${importPath}'`;
|
|
1260
|
+
}).join("\n");
|
|
1261
|
+
const lastImportIdx = entryTemplate.lastIndexOf("import type");
|
|
1262
|
+
const afterLastImport = entryTemplate.indexOf("\n", lastImportIdx) + 1;
|
|
1263
|
+
const withImports = entryTemplate.slice(0, afterLastImport) + "\n" + importStatements + "\n" + entryTemplate.slice(afterLastImport);
|
|
1264
|
+
const registrationStatements = customRoutes.flatMap((route, i) => {
|
|
1265
|
+
const methods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
1266
|
+
return methods.map((method) => {
|
|
1267
|
+
const methodLower = method.toLowerCase();
|
|
1268
|
+
return `if (customRoute${i}.${method}) app.${methodLower}('${route.path}', customRoute${i}.${method})`;
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
const registrationCode = ["// Custom routes", ...registrationStatements, ""].join("\n");
|
|
1272
|
+
const exportIdx = withImports.indexOf("export default app");
|
|
1273
|
+
const result = withImports.slice(0, exportIdx) + registrationCode + withImports.slice(exportIdx);
|
|
1274
|
+
return result;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/lib/dev/pid.ts
|
|
1278
|
+
import { mkdir as mkdir2, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
1279
|
+
import { join as join3 } from "path";
|
|
1280
|
+
function getDevServerPidPath() {
|
|
1281
|
+
return join3(getWorkspace(), CLI_DIRECTORIES.WORKSPACE, CLI_FILES.DEV_SERVER_PID);
|
|
1282
|
+
}
|
|
1283
|
+
async function createDevServerPidFile() {
|
|
1284
|
+
const pidPath = getDevServerPidFile();
|
|
1285
|
+
const pidDir = join3(getWorkspace(), CLI_DIRECTORIES.WORKSPACE);
|
|
1286
|
+
await mkdir2(pidDir, { recursive: true });
|
|
1287
|
+
await writeFile2(pidPath, process.pid.toString());
|
|
1288
|
+
}
|
|
1289
|
+
function getDevServerPidFile() {
|
|
1290
|
+
return getDevServerPidPath();
|
|
1256
1291
|
}
|
|
1257
1292
|
|
|
1258
1293
|
// src/lib/dev/server.ts
|
|
@@ -1260,77 +1295,65 @@ async function startDevServer(options) {
|
|
|
1260
1295
|
const {
|
|
1261
1296
|
port,
|
|
1262
1297
|
config,
|
|
1263
|
-
logger: enableLogger = true,
|
|
1264
|
-
quiet = false,
|
|
1265
1298
|
platformUrl = process.env.PLAYCADEMY_BASE_URL || "http://localhost:5174"
|
|
1266
1299
|
} = options;
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1300
|
+
const hasSandboxTimebackCreds = !!process.env.TIMEBACK_API_CLIENT_ID;
|
|
1301
|
+
const devConfig = config.integrations?.timeback && !hasSandboxTimebackCreds ? { ...config, integrations: { ...config.integrations, timeback: void 0 } } : config;
|
|
1302
|
+
const bundle = await bundleBackend(devConfig, {
|
|
1303
|
+
sourcemap: false,
|
|
1304
|
+
minify: false
|
|
1305
|
+
});
|
|
1306
|
+
const dbDir = join4(getWorkspace(), CLI_DIRECTORIES.DATABASE);
|
|
1307
|
+
try {
|
|
1308
|
+
await mkdir3(dbDir, { recursive: true });
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
throw new Error(`Failed to create database directory: ${getErrorMessage(error)}`);
|
|
1270
1311
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1312
|
+
const mf = new Miniflare({
|
|
1313
|
+
port,
|
|
1314
|
+
modules: [
|
|
1315
|
+
{
|
|
1316
|
+
type: "ESModule",
|
|
1317
|
+
path: "index.mjs",
|
|
1318
|
+
contents: bundle.code
|
|
1319
|
+
}
|
|
1320
|
+
],
|
|
1321
|
+
bindings: {
|
|
1281
1322
|
PLAYCADEMY_API_KEY: process.env.PLAYCADEMY_API_KEY || "dev-api-key",
|
|
1282
1323
|
GAME_ID: CORE_GAME_UUIDS.PLAYGROUND,
|
|
1283
|
-
// Use playground game seeded in sandbox
|
|
1284
1324
|
PLAYCADEMY_BASE_URL: platformUrl
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
});
|
|
1290
|
-
let sdkPromise = null;
|
|
1291
|
-
app.use("*", async (c, next) => {
|
|
1292
|
-
if (!sdkPromise) {
|
|
1293
|
-
const initConfig = {
|
|
1294
|
-
apiKey: c.env.PLAYCADEMY_API_KEY,
|
|
1295
|
-
gameId: c.env.GAME_ID,
|
|
1296
|
-
baseUrl: c.env.PLAYCADEMY_BASE_URL,
|
|
1297
|
-
config
|
|
1298
|
-
};
|
|
1299
|
-
sdkPromise = PlaycademyClient2.init(initConfig);
|
|
1300
|
-
}
|
|
1301
|
-
const sdk = await sdkPromise;
|
|
1302
|
-
c.set("sdk", sdk);
|
|
1303
|
-
await next();
|
|
1304
|
-
});
|
|
1305
|
-
const hasSandboxTimebackCreds = !!process.env.TIMEBACK_API_CLIENT_ID;
|
|
1306
|
-
const devIntegrations = config.integrations?.timeback && !hasSandboxTimebackCreds ? { ...config.integrations, timeback: void 0 } : config.integrations;
|
|
1307
|
-
await registerBuiltinRoutes(app, devIntegrations);
|
|
1308
|
-
if (config.integrations?.timeback && !hasSandboxTimebackCreds) {
|
|
1309
|
-
const timebackWarningHandler = async (c) => {
|
|
1310
|
-
return c.json({
|
|
1311
|
-
status: "ok",
|
|
1312
|
-
__playcademyDevWarning: "timeback-not-configured",
|
|
1313
|
-
__playcademyDevMessage: "TimeBack is configured in playcademy.config.js but the sandbox does not have TimeBack credentials.\n\nTo test TimeBack locally:\n \u2022 Set TIMEBACK_ONEROSTER_API_URL, TIMEBACK_CALIPER_API_URL, and TIMEBACK_API_CLIENT_ID/SECRET\n \u2022 Or deploy your game: playcademy deploy\n \u2022 Or wait for @superbuilders/timeback-local (coming soon)"
|
|
1314
|
-
});
|
|
1315
|
-
};
|
|
1316
|
-
app.post("/api/integrations/timeback/end-activity", timebackWarningHandler);
|
|
1317
|
-
}
|
|
1318
|
-
await registerCustomRoutes(app, customRoutesList);
|
|
1319
|
-
return serve({
|
|
1320
|
-
fetch: app.fetch,
|
|
1321
|
-
port
|
|
1325
|
+
},
|
|
1326
|
+
d1Databases: ["DB"],
|
|
1327
|
+
d1Persist: dbDir,
|
|
1328
|
+
compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
|
|
1322
1329
|
});
|
|
1330
|
+
const d1 = await mf.getD1Database("DB");
|
|
1331
|
+
await d1.exec("SELECT 1");
|
|
1332
|
+
await createDevServerPidFile();
|
|
1333
|
+
return mf;
|
|
1323
1334
|
}
|
|
1324
1335
|
|
|
1325
1336
|
// src/lib/dev/reload.ts
|
|
1326
|
-
import { join as
|
|
1337
|
+
import { join as join5, relative as relative2 } from "path";
|
|
1327
1338
|
import chokidar from "chokidar";
|
|
1339
|
+
import { bold as bold3, cyan as cyan2, dim as dim3, green as green2 } from "colorette";
|
|
1340
|
+
function formatTime() {
|
|
1341
|
+
const now = /* @__PURE__ */ new Date();
|
|
1342
|
+
let hours = now.getHours();
|
|
1343
|
+
const minutes = now.getMinutes().toString().padStart(2, "0");
|
|
1344
|
+
const seconds = now.getSeconds().toString().padStart(2, "0");
|
|
1345
|
+
const ampm = hours >= 12 ? "PM" : "AM";
|
|
1346
|
+
hours = hours % 12 || 12;
|
|
1347
|
+
return `${hours}:${minutes}:${seconds} ${ampm}`;
|
|
1348
|
+
}
|
|
1328
1349
|
function startHotReload(onReload, options = {}) {
|
|
1329
1350
|
const workspace = getWorkspace();
|
|
1351
|
+
const customRoutesConfig = options.config?.integrations?.customRoutes;
|
|
1352
|
+
const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
|
|
1330
1353
|
const watchPaths = [
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1354
|
+
join5(workspace, customRoutesDir),
|
|
1355
|
+
join5(workspace, "playcademy.config.js"),
|
|
1356
|
+
join5(workspace, "playcademy.config.json")
|
|
1334
1357
|
];
|
|
1335
1358
|
const watcher = chokidar.watch(watchPaths, {
|
|
1336
1359
|
persistent: true,
|
|
@@ -1340,22 +1363,32 @@ function startHotReload(onReload, options = {}) {
|
|
|
1340
1363
|
pollInterval: 100
|
|
1341
1364
|
}
|
|
1342
1365
|
});
|
|
1343
|
-
const logSuccess = options.onSuccess || (() =>
|
|
1366
|
+
const logSuccess = options.onSuccess || ((changedPath, eventType) => {
|
|
1367
|
+
if (changedPath) {
|
|
1368
|
+
const relativePath = relative2(workspace, changedPath);
|
|
1369
|
+
const timestamp = dim3(formatTime());
|
|
1370
|
+
const brand = bold3(cyan2("[playcademy]"));
|
|
1371
|
+
const event = eventType === "changed" ? green2("reload") : green2(eventType || "reload");
|
|
1372
|
+
console.log(`${timestamp} ${brand} ${event} ${dim3(relativePath)}`);
|
|
1373
|
+
} else {
|
|
1374
|
+
logger.success("Reloaded");
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1344
1377
|
const logError = options.onError || ((error) => {
|
|
1345
1378
|
logger.newLine();
|
|
1346
|
-
logger.error(`Reload failed: ${
|
|
1379
|
+
logger.error(`Reload failed: ${getErrorMessage(error)}`);
|
|
1347
1380
|
});
|
|
1348
|
-
const
|
|
1381
|
+
const createReloadHandler = (eventType) => async (path2) => {
|
|
1349
1382
|
try {
|
|
1350
1383
|
await onReload();
|
|
1351
|
-
logSuccess(
|
|
1384
|
+
logSuccess(path2, eventType);
|
|
1352
1385
|
} catch (error) {
|
|
1353
1386
|
logError(error);
|
|
1354
1387
|
}
|
|
1355
1388
|
};
|
|
1356
|
-
watcher.on("change",
|
|
1357
|
-
watcher.on("add",
|
|
1358
|
-
watcher.on("unlink",
|
|
1389
|
+
watcher.on("change", createReloadHandler("changed"));
|
|
1390
|
+
watcher.on("add", createReloadHandler("added"));
|
|
1391
|
+
watcher.on("unlink", createReloadHandler("removed"));
|
|
1359
1392
|
return watcher;
|
|
1360
1393
|
}
|
|
1361
1394
|
export {
|