playcademy 0.18.0 → 0.18.2

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.
Files changed (43) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/index.d.ts +24 -12
  3. package/dist/index.js +465 -255
  4. package/dist/utils.js +558 -351
  5. package/dist/version.js +1 -1
  6. package/package.json +1 -1
  7. package/dist/constants/src/achievements.ts +0 -107
  8. package/dist/constants/src/auth.ts +0 -13
  9. package/dist/constants/src/character.ts +0 -16
  10. package/dist/constants/src/domains.ts +0 -50
  11. package/dist/constants/src/env-vars.ts +0 -20
  12. package/dist/constants/src/index.ts +0 -18
  13. package/dist/constants/src/overworld.ts +0 -330
  14. package/dist/constants/src/system.ts +0 -10
  15. package/dist/constants/src/timeback.ts +0 -118
  16. package/dist/constants/src/typescript.ts +0 -21
  17. package/dist/constants/src/workers.ts +0 -36
  18. package/dist/edge-play/src/constants.ts +0 -27
  19. package/dist/edge-play/src/entry/middleware.ts +0 -247
  20. package/dist/edge-play/src/entry/queue.test.ts +0 -279
  21. package/dist/edge-play/src/entry/queue.ts +0 -107
  22. package/dist/edge-play/src/entry/session.ts +0 -45
  23. package/dist/edge-play/src/entry/setup.ts +0 -78
  24. package/dist/edge-play/src/entry/types.ts +0 -30
  25. package/dist/edge-play/src/entry.ts +0 -94
  26. package/dist/edge-play/src/html.d.ts +0 -5
  27. package/dist/edge-play/src/index.ts +0 -4
  28. package/dist/edge-play/src/lib/errors.ts +0 -51
  29. package/dist/edge-play/src/lib/index.ts +0 -3
  30. package/dist/edge-play/src/lib/self-dispatch.test.ts +0 -244
  31. package/dist/edge-play/src/lib/self-dispatch.ts +0 -41
  32. package/dist/edge-play/src/lib/validation.test.ts +0 -190
  33. package/dist/edge-play/src/lib/validation.ts +0 -64
  34. package/dist/edge-play/src/polyfills.js +0 -54
  35. package/dist/edge-play/src/register-routes.ts +0 -59
  36. package/dist/edge-play/src/routes/health.ts +0 -104
  37. package/dist/edge-play/src/routes/index.ts +0 -66
  38. package/dist/edge-play/src/routes/integrations/timeback/end-activity.ts +0 -181
  39. package/dist/edge-play/src/routes/integrations/timeback/get-xp.ts +0 -159
  40. package/dist/edge-play/src/routes/root.html +0 -253
  41. package/dist/edge-play/src/routes/root.ts +0 -22
  42. package/dist/edge-play/src/stub-entry.ts +0 -161
  43. package/dist/edge-play/src/types.ts +0 -124
package/dist/utils.js CHANGED
@@ -1529,13 +1529,10 @@ function buildQueueImportStatements(queueHandlers) {
1529
1529
  if (queueHandlers.length === 0) {
1530
1530
  return "// No queue handlers";
1531
1531
  }
1532
- return [
1533
- "import { createQueueRouter, registerQueueIngressRoute } from './entry/queue'",
1534
- ...queueHandlers.map((handler, index) => {
1535
- const normalizedFile = handler.file.replace(/^server\//, "").replace(/\\/g, "/");
1536
- return `import queueHandler${index} from '@game-server/${normalizedFile}'`;
1537
- })
1538
- ].join("\n");
1532
+ return queueHandlers.map((handler, index) => {
1533
+ const normalizedFile = handler.file.replace(/^server\//, "").replace(/\\/g, "/");
1534
+ return `import queueHandler${index} from '@game-server/${normalizedFile}'`;
1535
+ }).join("\n");
1539
1536
  }
1540
1537
  function buildQueueHandlerCode(queueHandlers) {
1541
1538
  if (queueHandlers.length === 0) {
@@ -1950,8 +1947,8 @@ function processConfigVariables(config) {
1950
1947
  }
1951
1948
 
1952
1949
  // src/lib/dev/server.ts
1953
- import { mkdir as mkdir2 } from "fs/promises";
1954
- import { join as join19 } from "path";
1950
+ import { mkdir as mkdir3 } from "fs/promises";
1951
+ import { join as join20 } from "path";
1955
1952
  import { Log, LogLevel, Miniflare } from "miniflare";
1956
1953
 
1957
1954
  // ../edge-play/src/constants.ts
@@ -2027,14 +2024,14 @@ function readServerInfo(type, projectRoot) {
2027
2024
  return servers[0] || null;
2028
2025
  }
2029
2026
  async function isPortInUse(port) {
2030
- return new Promise((resolve5) => {
2027
+ return new Promise((resolve6) => {
2031
2028
  const server = createServer();
2032
2029
  server.once("error", () => {
2033
- resolve5(true);
2030
+ resolve6(true);
2034
2031
  });
2035
2032
  server.once("listening", () => {
2036
2033
  server.close();
2037
- resolve5(false);
2034
+ resolve6(false);
2038
2035
  });
2039
2036
  server.listen(port);
2040
2037
  });
@@ -2048,12 +2045,150 @@ async function waitForPort(port, timeoutMs = 5e3) {
2048
2045
  Stop the other server or specify a different port with --port <number>.`
2049
2046
  );
2050
2047
  }
2051
- await new Promise((resolve5) => setTimeout(resolve5, 100));
2048
+ await new Promise((resolve6) => setTimeout(resolve6, 100));
2052
2049
  }
2053
2050
  }
2054
2051
 
2055
2052
  // src/lib/deploy/bundle.ts
2056
- import { existsSync as existsSync8, readFileSync as readFileSync3 } from "node:fs";
2053
+ import { join as join14, relative as relative3 } from "node:path";
2054
+
2055
+ // src/lib/dev/routes.ts
2056
+ init_file_loader();
2057
+ import { mkdir, writeFile } from "fs/promises";
2058
+ import { tmpdir as tmpdir2 } from "os";
2059
+ import { join as join11, relative as relative2 } from "path";
2060
+ import { pathToFileURL as pathToFileURL2 } from "url";
2061
+
2062
+ // src/lib/deploy/hash.ts
2063
+ init_file_loader();
2064
+ import { createHash } from "crypto";
2065
+ import { createReadStream } from "fs";
2066
+ import { resolve as resolve4 } from "path";
2067
+ async function hashFile(filePath) {
2068
+ return new Promise((resolveHash, reject) => {
2069
+ const hash = createHash("sha256");
2070
+ const stream = createReadStream(filePath);
2071
+ stream.on("data", (chunk) => hash.update(chunk));
2072
+ stream.on("end", () => resolveHash(hash.digest("hex")));
2073
+ stream.on("error", reject);
2074
+ });
2075
+ }
2076
+ async function hashDirectory(dirPath, extensions) {
2077
+ try {
2078
+ const files = scanDirectory(dirPath, { extensions, allowMissing: true });
2079
+ if (files.length === 0) {
2080
+ return null;
2081
+ }
2082
+ files.sort();
2083
+ const combinedHash = createHash("sha256");
2084
+ for (const file of files) {
2085
+ const filePath = resolve4(dirPath, file);
2086
+ const fileHash = await hashFile(filePath);
2087
+ combinedHash.update(`${file}:${fileHash}`);
2088
+ }
2089
+ return combinedHash.digest("hex");
2090
+ } catch {
2091
+ return null;
2092
+ }
2093
+ }
2094
+ function hashContent(content) {
2095
+ const contentStr = typeof content === "string" ? content : JSON.stringify(content);
2096
+ return createHash("sha256").update(contentStr).digest("hex");
2097
+ }
2098
+
2099
+ // src/lib/dev/routes.ts
2100
+ async function discoverRoutes(apiDir) {
2101
+ const files = scanDirectory(apiDir, {
2102
+ extensions: ["ts", "js"],
2103
+ allowMissing: true
2104
+ });
2105
+ const routes = await Promise.all(
2106
+ files.map(async (file) => {
2107
+ const routePath = filePathToRoutePath(file);
2108
+ const absolutePath = join11(apiDir, file);
2109
+ const relativePath = relative2(getWorkspace(), absolutePath).replace(/\\/g, "/");
2110
+ const methods = await detectExportedMethods(absolutePath);
2111
+ return {
2112
+ path: routePath,
2113
+ file: relativePath,
2114
+ absolutePath,
2115
+ methods
2116
+ };
2117
+ })
2118
+ );
2119
+ return routes;
2120
+ }
2121
+ async function detectExportedMethods(filePath) {
2122
+ try {
2123
+ const importPath = await transpileRoute(filePath);
2124
+ const specifier = pathToFileURL2(importPath).href;
2125
+ const module = await import(specifier);
2126
+ const methods = [];
2127
+ if (module.GET) {
2128
+ methods.push("GET");
2129
+ }
2130
+ if (module.POST) {
2131
+ methods.push("POST");
2132
+ }
2133
+ if (module.PUT) {
2134
+ methods.push("PUT");
2135
+ }
2136
+ if (module.PATCH) {
2137
+ methods.push("PATCH");
2138
+ }
2139
+ if (module.DELETE) {
2140
+ methods.push("DELETE");
2141
+ }
2142
+ return methods;
2143
+ } catch {
2144
+ return [];
2145
+ }
2146
+ }
2147
+ function filePathToRoutePath(filePath) {
2148
+ let routePath = filePath.replace(/\.(ts|js)$/, "");
2149
+ if (routePath === "index" || routePath.endsWith("/index")) {
2150
+ routePath = routePath.replace(/\/?index$/, "");
2151
+ }
2152
+ let urlPath = `/${routePath.replace(/\\/g, "/")}`;
2153
+ urlPath = urlPath.replace(/\[\.\.\.([^\]]+)\]/g, ":$1{.*}");
2154
+ urlPath = urlPath.replace(/\[([^\]]+)\]/g, ":$1");
2155
+ urlPath = urlPath === "/" ? "/api" : `/api${urlPath}`;
2156
+ return urlPath;
2157
+ }
2158
+ function isBun() {
2159
+ return typeof Bun !== "undefined";
2160
+ }
2161
+ async function transpileRoute(filePath) {
2162
+ if (isBun() || !filePath.endsWith(".ts")) {
2163
+ return filePath;
2164
+ }
2165
+ const esbuild3 = await import("esbuild");
2166
+ const result = await esbuild3.build({
2167
+ entryPoints: [filePath],
2168
+ write: false,
2169
+ format: "esm",
2170
+ platform: "node",
2171
+ bundle: true,
2172
+ // Bundle dependencies so route is self-contained
2173
+ target: "node18",
2174
+ external: ["hono"]
2175
+ // Don't bundle Hono (provided by dev server)
2176
+ });
2177
+ if (!result.outputFiles?.[0]) {
2178
+ throw new Error("Transpilation failed: no output");
2179
+ }
2180
+ const tempDir = join11(tmpdir2(), "playcademy-dev");
2181
+ await mkdir(tempDir, { recursive: true });
2182
+ const hash = hashContent(filePath).slice(0, 12);
2183
+ const jsPath = join11(tempDir, `${hash}.mjs`);
2184
+ await writeFile(jsPath, result.outputFiles[0].text);
2185
+ return jsPath;
2186
+ }
2187
+
2188
+ // src/lib/deploy/backend-runtime.ts
2189
+ import { existsSync as existsSync8 } from "node:fs";
2190
+ import { copyFile, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "node:fs/promises";
2191
+ import { tmpdir as tmpdir3 } from "node:os";
2057
2192
  import { join as join13 } from "node:path";
2058
2193
 
2059
2194
  // ../utils/src/path.ts
@@ -2309,193 +2444,266 @@ function getMonorepoRoot() {
2309
2444
  var isCompiledBinary = typeof IS_COMPILED_BINARY !== "undefined" && IS_COMPILED_BINARY;
2310
2445
 
2311
2446
  // src/lib/build/plugins.ts
2447
+ var TEXT_LOADER_PATTERNS = [
2448
+ /edge-play\/src\/stub-entry\.ts$/,
2449
+ /edge-play\/src\/routes\/root\.html$/,
2450
+ /templates\/sample-route\.ts$/
2451
+ ];
2312
2452
  function textLoaderPlugin() {
2313
2453
  return {
2314
2454
  name: "text-loader",
2315
2455
  setup(build2) {
2316
- build2.onLoad({ filter: /edge-play\/src\/entry\.ts$/ }, async (args) => {
2317
- const fs2 = await import("fs/promises");
2318
- const text = await fs2.readFile(args.path, "utf8");
2319
- return {
2320
- contents: `export default ${JSON.stringify(text)}`,
2321
- loader: "js"
2322
- };
2323
- });
2324
- build2.onLoad({ filter: /edge-play\/src\/stub-entry\.ts$/ }, async (args) => {
2325
- const fs2 = await import("fs/promises");
2326
- const text = await fs2.readFile(args.path, "utf8");
2327
- return {
2328
- contents: `export default ${JSON.stringify(text)}`,
2329
- loader: "js"
2330
- };
2331
- });
2332
- build2.onLoad({ filter: /edge-play\/src\/routes\/root\.html$/ }, async (args) => {
2333
- const fs2 = await import("fs/promises");
2334
- const text = await fs2.readFile(args.path, "utf8");
2335
- return {
2336
- contents: `export default ${JSON.stringify(text)}`,
2337
- loader: "js"
2338
- };
2339
- });
2340
- build2.onLoad({ filter: /templates\/sample-route\.ts$/ }, async (args) => {
2341
- const fs2 = await import("fs/promises");
2342
- const text = await fs2.readFile(args.path, "utf8");
2343
- return {
2344
- contents: `export default ${JSON.stringify(text)}`,
2345
- loader: "js"
2346
- };
2347
- });
2456
+ for (const filter of TEXT_LOADER_PATTERNS) {
2457
+ build2.onLoad({ filter }, async (args) => {
2458
+ const fs2 = await import("fs/promises");
2459
+ const text = await fs2.readFile(args.path, "utf8");
2460
+ return {
2461
+ contents: `export default ${JSON.stringify(text)}`,
2462
+ loader: "js"
2463
+ };
2464
+ });
2465
+ }
2348
2466
  }
2349
2467
  };
2350
2468
  }
2351
2469
 
2352
- // src/lib/dev/routes.ts
2353
- init_file_loader();
2354
- import { mkdir, writeFile } from "fs/promises";
2355
- import { tmpdir as tmpdir2 } from "os";
2356
- import { join as join11, relative as relative2 } from "path";
2357
- import { pathToFileURL as pathToFileURL2 } from "url";
2470
+ // src/lib/build/embed.ts
2471
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
2472
+ import { dirname as dirname4, join as join12 } from "node:path";
2358
2473
 
2359
- // src/lib/deploy/hash.ts
2360
- import { createHash } from "crypto";
2361
- init_file_loader();
2362
- function hashContent(content) {
2363
- const contentStr = typeof content === "string" ? content : JSON.stringify(content);
2364
- return createHash("sha256").update(contentStr).digest("hex");
2365
- }
2474
+ // src/version.ts
2475
+ var cliVersion = false ? "0.0.0-dev" : "0.18.2";
2366
2476
 
2367
- // src/lib/dev/routes.ts
2368
- async function discoverRoutes(apiDir) {
2369
- const files = scanDirectory(apiDir, {
2370
- extensions: ["ts", "js"],
2371
- allowMissing: true
2372
- });
2373
- const routes = await Promise.all(
2374
- files.map(async (file) => {
2375
- const routePath = filePathToRoutePath(file);
2376
- const absolutePath = join11(apiDir, file);
2377
- const relativePath = relative2(getWorkspace(), absolutePath).replace(/\\/g, "/");
2378
- const methods = await detectExportedMethods(absolutePath);
2379
- return {
2380
- path: routePath,
2381
- file: relativePath,
2382
- absolutePath,
2383
- methods
2384
- };
2385
- })
2386
- );
2387
- return routes;
2388
- }
2389
- async function detectExportedMethods(filePath) {
2390
- try {
2391
- const importPath = await transpileRoute(filePath);
2392
- const specifier = pathToFileURL2(importPath).href;
2393
- const module = await import(specifier);
2394
- const methods = [];
2395
- if (module.GET) {
2396
- methods.push("GET");
2397
- }
2398
- if (module.POST) {
2399
- methods.push("POST");
2477
+ // src/lib/build/embed.ts
2478
+ function writeFileTree(baseDir, files) {
2479
+ for (const [relativePath, content] of Object.entries(files)) {
2480
+ const fullPath = join12(baseDir, relativePath);
2481
+ const dir = dirname4(fullPath);
2482
+ if (!existsSync7(dir)) {
2483
+ mkdirSync2(dir, { recursive: true });
2400
2484
  }
2401
- if (module.PUT) {
2402
- methods.push("PUT");
2485
+ writeFileSync3(fullPath, content);
2486
+ }
2487
+ }
2488
+ function createBundleExtractor(options) {
2489
+ const { name, cacheSubdir, getBundle } = options;
2490
+ const marker = `.${name}.ok`;
2491
+ let cachedDir = null;
2492
+ return () => {
2493
+ if (cachedDir) {
2494
+ return cachedDir;
2403
2495
  }
2404
- if (module.PATCH) {
2405
- methods.push("PATCH");
2496
+ const bundle = getBundle();
2497
+ if (!bundle) {
2498
+ throw new Error(`${name}: embedded bundle is null (not available in dev mode)`);
2406
2499
  }
2407
- if (module.DELETE) {
2408
- methods.push("DELETE");
2500
+ const dir = join12(cacheVersionDir(cliVersion), cacheSubdir);
2501
+ const markerPath = join12(dir, marker);
2502
+ if (!existsSync7(markerPath)) {
2503
+ mkdirSync2(dir, { recursive: true });
2504
+ writeFileTree(dir, bundle);
2505
+ writeFileSync3(markerPath, `${cliVersion}
2506
+ `, "utf8");
2409
2507
  }
2410
- return methods;
2508
+ cachedDir = dir;
2509
+ return dir;
2510
+ };
2511
+ }
2512
+
2513
+ // src/lib/deploy/embedded-runtime.ts
2514
+ var EMBEDDED_BACKEND_RUNTIME = null;
2515
+
2516
+ // src/lib/deploy/extract-runtime.ts
2517
+ var extractEmbeddedBackendRuntime = createBundleExtractor({
2518
+ name: "embedded-backend-runtime",
2519
+ cacheSubdir: "backend-runtime",
2520
+ getBundle: () => EMBEDDED_BACKEND_RUNTIME
2521
+ });
2522
+
2523
+ // src/lib/deploy/backend-runtime.ts
2524
+ var BACKEND_RUNTIME_ENTRY = "index.js";
2525
+ var BACKEND_RUNTIME_POLYFILLS = "polyfills.js";
2526
+ var BACKEND_RUNTIME_MANIFEST = "manifest.json";
2527
+ var DEV_RUNTIME_CACHE_DIR = "backend-runtime-dev";
2528
+ function createNodePolyfillAliases(polyfillsPath) {
2529
+ return {
2530
+ fs: polyfillsPath,
2531
+ "fs/promises": polyfillsPath,
2532
+ path: polyfillsPath,
2533
+ os: polyfillsPath,
2534
+ process: polyfillsPath,
2535
+ "node:fs": polyfillsPath,
2536
+ "node:fs/promises": polyfillsPath,
2537
+ "node:path": polyfillsPath,
2538
+ "node:os": polyfillsPath,
2539
+ "node:process": polyfillsPath
2540
+ };
2541
+ }
2542
+ var WORKSPACE_SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
2543
+ var RUNTIME_SOURCE_DIRS = [
2544
+ "packages/edge-play/src",
2545
+ "packages/sdk/src",
2546
+ "packages/constants/src",
2547
+ "packages/timeback/src",
2548
+ "packages/types/src",
2549
+ "packages/utils/src",
2550
+ "packages/cloudflare/src",
2551
+ "packages/logger/src"
2552
+ ];
2553
+ async function getPackageVersion2(monorepoRoot, packageName) {
2554
+ try {
2555
+ const packageJson = JSON.parse(
2556
+ await readFile2(join13(monorepoRoot, "packages", packageName, "package.json"), "utf8")
2557
+ );
2558
+ return packageJson.version ?? "0.0.0-dev";
2411
2559
  } catch {
2412
- return [];
2560
+ return "0.0.0-dev";
2413
2561
  }
2414
2562
  }
2415
- function filePathToRoutePath(filePath) {
2416
- let routePath = filePath.replace(/\.(ts|js)$/, "");
2417
- if (routePath === "index" || routePath.endsWith("/index")) {
2418
- routePath = routePath.replace(/\/?index$/, "");
2419
- }
2420
- let urlPath = `/${routePath.replace(/\\/g, "/")}`;
2421
- urlPath = urlPath.replace(/\[\.\.\.([^\]]+)\]/g, ":$1{.*}");
2422
- urlPath = urlPath.replace(/\[([^\]]+)\]/g, ":$1");
2423
- urlPath = urlPath === "/" ? "/api" : `/api${urlPath}`;
2424
- return urlPath;
2563
+ async function computeRuntimeInputFingerprint(monorepoRoot, sdkVersion) {
2564
+ const entries = await Promise.all(
2565
+ RUNTIME_SOURCE_DIRS.map(async (dir) => [
2566
+ dir,
2567
+ await hashDirectory(join13(monorepoRoot, dir), ["ts", "js", "json", "html"])
2568
+ ])
2569
+ );
2570
+ return hashContent({ ...Object.fromEntries(entries), sdkVersion });
2425
2571
  }
2426
- function isBun() {
2427
- return typeof Bun !== "undefined";
2572
+ async function getRuntimeBuildMetadata(monorepoRoot) {
2573
+ const [cliVersion2, sdkVersion] = await Promise.all([
2574
+ getPackageVersion2(monorepoRoot, "cli"),
2575
+ getPackageVersion2(monorepoRoot, "sdk")
2576
+ ]);
2577
+ const inputFingerprint = await computeRuntimeInputFingerprint(monorepoRoot, sdkVersion);
2578
+ return {
2579
+ cliVersion: cliVersion2,
2580
+ sdkVersion,
2581
+ runtimeBuildId: inputFingerprint.slice(0, 12),
2582
+ inputFingerprint,
2583
+ entry: BACKEND_RUNTIME_ENTRY,
2584
+ polyfills: BACKEND_RUNTIME_POLYFILLS
2585
+ };
2428
2586
  }
2429
- async function transpileRoute(filePath) {
2430
- if (isBun() || !filePath.endsWith(".ts")) {
2431
- return filePath;
2587
+ function resolveWorkspaceSourceImport(monorepoRoot, specifier) {
2588
+ if (!specifier.startsWith("@playcademy/")) {
2589
+ return null;
2590
+ }
2591
+ const pathParts = specifier.slice("@playcademy/".length).split("/");
2592
+ const packageName = pathParts.shift();
2593
+ if (!packageName) {
2594
+ return null;
2595
+ }
2596
+ const basePath = join13(monorepoRoot, "packages", packageName, "src");
2597
+ const targetPath = pathParts.length > 0 ? join13(basePath, ...pathParts) : join13(basePath, "index");
2598
+ for (const extension of WORKSPACE_SOURCE_EXTENSIONS) {
2599
+ const withExtension = `${targetPath}${extension}`;
2600
+ if (existsSync8(withExtension)) {
2601
+ return withExtension;
2602
+ }
2603
+ }
2604
+ for (const extension of WORKSPACE_SOURCE_EXTENSIONS) {
2605
+ const indexPath = join13(targetPath, `index${extension}`);
2606
+ if (existsSync8(indexPath)) {
2607
+ return indexPath;
2608
+ }
2432
2609
  }
2610
+ return null;
2611
+ }
2612
+ function createWorkspaceSourceResolverPlugin(monorepoRoot) {
2613
+ return {
2614
+ name: "playcademy-workspace-source-resolver",
2615
+ setup(build2) {
2616
+ build2.onResolve({ filter: /^@playcademy\// }, (args) => {
2617
+ const resolved = resolveWorkspaceSourceImport(monorepoRoot, args.path);
2618
+ return resolved ? { path: resolved } : null;
2619
+ });
2620
+ }
2621
+ };
2622
+ }
2623
+ async function writeRuntimeManifest(outdir, manifest) {
2624
+ await writeFile2(
2625
+ join13(outdir, BACKEND_RUNTIME_MANIFEST),
2626
+ `${JSON.stringify(manifest, null, 2)}
2627
+ `,
2628
+ "utf8"
2629
+ );
2630
+ }
2631
+ async function buildBackendRuntime(outdir) {
2632
+ const monorepoRoot = getMonorepoRoot();
2633
+ const manifest = await getRuntimeBuildMetadata(monorepoRoot);
2634
+ const runtimeEntry = join13(monorepoRoot, "packages", "edge-play", "src", "entry", "index.ts");
2635
+ const polyfillsEntry = join13(monorepoRoot, "packages", "edge-play", "src", "polyfills.js");
2433
2636
  const esbuild3 = await import("esbuild");
2434
- const result = await esbuild3.build({
2435
- entryPoints: [filePath],
2436
- write: false,
2437
- format: "esm",
2438
- platform: "node",
2637
+ await rm(outdir, { recursive: true, force: true });
2638
+ await mkdir2(outdir, { recursive: true });
2639
+ await esbuild3.build({
2640
+ entryPoints: [runtimeEntry],
2641
+ outfile: join13(outdir, BACKEND_RUNTIME_ENTRY),
2439
2642
  bundle: true,
2440
- // Bundle dependencies so route is self-contained
2441
- target: "node18",
2442
- external: ["hono"]
2443
- // Don't bundle Hono (provided by dev server)
2643
+ format: "esm",
2644
+ platform: "browser",
2645
+ target: "es2022",
2646
+ write: true,
2647
+ sourcemap: false,
2648
+ minify: false,
2649
+ logLevel: "error",
2650
+ alias: createNodePolyfillAliases(polyfillsEntry),
2651
+ plugins: [textLoaderPlugin(), createWorkspaceSourceResolverPlugin(monorepoRoot)],
2652
+ define: {
2653
+ PLAYCADEMY_RUNTIME_CLI_VERSION: JSON.stringify(manifest.cliVersion),
2654
+ PLAYCADEMY_RUNTIME_SDK_VERSION: JSON.stringify(manifest.sdkVersion),
2655
+ PLAYCADEMY_RUNTIME_BUILD_ID: JSON.stringify(manifest.runtimeBuildId)
2656
+ }
2444
2657
  });
2445
- if (!result.outputFiles?.[0]) {
2446
- throw new Error("Transpilation failed: no output");
2447
- }
2448
- const tempDir = join11(tmpdir2(), "playcademy-dev");
2449
- await mkdir(tempDir, { recursive: true });
2450
- const hash = hashContent(filePath).slice(0, 12);
2451
- const jsPath = join11(tempDir, `${hash}.mjs`);
2452
- await writeFile(jsPath, result.outputFiles[0].text);
2453
- return jsPath;
2658
+ await copyFile(polyfillsEntry, join13(outdir, BACKEND_RUNTIME_POLYFILLS));
2659
+ await writeRuntimeManifest(outdir, manifest);
2660
+ return manifest;
2454
2661
  }
2455
-
2456
- // src/lib/deploy/extract-sources.ts
2457
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
2458
- import { dirname as dirname4, join as join12 } from "node:path";
2459
-
2460
- // src/version.ts
2461
- var cliVersion = false ? "0.0.0-dev" : "0.18.0";
2462
-
2463
- // src/lib/deploy/embedded-sources.ts
2464
- var EMBEDDED_SOURCES = null;
2465
-
2466
- // src/lib/deploy/extract-sources.ts
2467
- var EXTRACTION_MARKER = ".embedded-sources.ok";
2468
- function writeTree(baseDir, files) {
2469
- for (const [relativePath, content] of Object.entries(files)) {
2470
- const fullPath = join12(baseDir, relativePath);
2471
- const dir = dirname4(fullPath);
2472
- if (!existsSync7(dir)) {
2473
- mkdirSync2(dir, { recursive: true });
2474
- }
2475
- writeFileSync3(fullPath, content);
2662
+ async function ensureDevBackendRuntime() {
2663
+ const monorepoRoot = getMonorepoRoot();
2664
+ const manifest = await getRuntimeBuildMetadata(monorepoRoot);
2665
+ const runtimeDir = join13(
2666
+ tmpdir3(),
2667
+ DEV_RUNTIME_CACHE_DIR,
2668
+ manifest.cliVersion,
2669
+ manifest.inputFingerprint
2670
+ );
2671
+ if (!existsSync8(join13(runtimeDir, BACKEND_RUNTIME_MANIFEST))) {
2672
+ await buildBackendRuntime(runtimeDir);
2476
2673
  }
2674
+ return runtimeDir;
2477
2675
  }
2478
- var cachedPaths = null;
2479
- function extractEmbeddedSources() {
2480
- if (cachedPaths) {
2481
- return cachedPaths;
2482
- }
2483
- if (!EMBEDDED_SOURCES) {
2484
- throw new Error("extractEmbeddedSources called but EMBEDDED_SOURCES is null (dev mode)");
2485
- }
2486
- const cacheRoot = cacheVersionDir(cliVersion);
2487
- const edgePlaySrc = join12(cacheRoot, "edge-play", "src");
2488
- const constantsSrc = join12(cacheRoot, "constants", "src");
2489
- const markerPath = join12(cacheRoot, EXTRACTION_MARKER);
2490
- if (!existsSync7(markerPath)) {
2491
- mkdirSync2(cacheRoot, { recursive: true });
2492
- writeTree(edgePlaySrc, EMBEDDED_SOURCES.edgePlay);
2493
- writeTree(constantsSrc, EMBEDDED_SOURCES.constants);
2494
- writeFileSync3(markerPath, `${cliVersion}
2495
- `, "utf8");
2676
+ async function readRuntimeManifest(runtimeDir) {
2677
+ const content = await readFile2(join13(runtimeDir, BACKEND_RUNTIME_MANIFEST), "utf8");
2678
+ return JSON.parse(content);
2679
+ }
2680
+ async function resolveBackendRuntimePaths() {
2681
+ const workspace = getWorkspace();
2682
+ const workspaceNodeModules = join13(workspace, "node_modules");
2683
+ if (isCompiledBinary) {
2684
+ const runtimeDir2 = extractEmbeddedBackendRuntime();
2685
+ const manifest2 = await readRuntimeManifest(runtimeDir2);
2686
+ const cliDepsRoot = join13(workspace, ".playcademy", "node_modules");
2687
+ return {
2688
+ runtimeDir: runtimeDir2,
2689
+ runtimeEntry: join13(runtimeDir2, manifest2.entry),
2690
+ polyfillsEntry: join13(runtimeDir2, manifest2.polyfills),
2691
+ workspaceNodeModules,
2692
+ cliDepsRoot,
2693
+ manifest: manifest2
2694
+ };
2496
2695
  }
2497
- cachedPaths = { edgePlaySrc, constantsSrc };
2498
- return cachedPaths;
2696
+ const monorepoRoot = getMonorepoRoot();
2697
+ const runtimeDir = await ensureDevBackendRuntime();
2698
+ const manifest = await readRuntimeManifest(runtimeDir);
2699
+ return {
2700
+ runtimeDir,
2701
+ runtimeEntry: join13(runtimeDir, manifest.entry),
2702
+ polyfillsEntry: join13(runtimeDir, manifest.polyfills),
2703
+ workspaceNodeModules,
2704
+ cliDepsRoot: monorepoRoot,
2705
+ manifest
2706
+ };
2499
2707
  }
2500
2708
 
2501
2709
  // src/lib/deploy/bundle.ts
@@ -2503,153 +2711,134 @@ async function discoverCustomRoutes(config) {
2503
2711
  const workspace = getWorkspace();
2504
2712
  const customRoutesConfig = config.integrations?.customRoutes;
2505
2713
  const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
2506
- const customRoutes = await discoverRoutes(join13(workspace, customRoutesDir));
2714
+ const customRoutes = await discoverRoutes(join14(workspace, customRoutesDir));
2507
2715
  const customRouteData = customRoutes.map((r) => ({
2508
2716
  path: r.path,
2509
2717
  file: r.file,
2510
- // Use relative path (e.g., 'server/api/test.ts'), not absolute
2718
+ // relative path (e.g., 'server/api/test.ts'), not absolute
2511
2719
  methods: r.methods
2512
2720
  }));
2513
2721
  return { customRouteData, customRoutesDir };
2514
2722
  }
2515
- function resolveEmbeddedSourcePaths() {
2516
- const workspace = getWorkspace();
2517
- if (isCompiledBinary) {
2518
- const { edgePlaySrc: edgePlaySrc2, constantsSrc } = extractEmbeddedSources();
2519
- const constantsEntry2 = join13(constantsSrc, "index.ts");
2520
- const workspaceNodeModules2 = join13(workspace, "node_modules");
2521
- const playcademyNodeModules = join13(workspace, ".playcademy", "node_modules");
2522
- return {
2523
- isBuiltPackage: true,
2524
- edgePlaySrc: edgePlaySrc2,
2525
- constantsEntry: constantsEntry2,
2526
- workspaceNodeModules: workspaceNodeModules2,
2527
- cliDepsRoot: playcademyNodeModules
2528
- };
2723
+ function getForbiddenImportMessage(specifier) {
2724
+ if (specifier === "@playcademy/sdk/server") {
2725
+ return "Use `c.get('sdk')` instead of importing the server SDK directly.";
2529
2726
  }
2530
- const distDir = new URL(".", import.meta.url).pathname;
2531
- const embeddedEdgeSrc = join13(distDir, "edge-play", "src");
2532
- const isBuiltPackage = existsSync8(embeddedEdgeSrc);
2533
- const monorepoRoot = getMonorepoRoot();
2534
- const monorepoEdgeSrc = join13(monorepoRoot, "packages/edge-play/src");
2535
- const edgePlaySrc = isBuiltPackage ? embeddedEdgeSrc : monorepoEdgeSrc;
2536
- const cliPackageRoot = isBuiltPackage ? join13(distDir, "../../..") : join13(monorepoRoot, "packages/cli");
2537
- const cliDepsRoot = isBuiltPackage ? join13(cliPackageRoot, "node_modules") : monorepoRoot;
2538
- const workspaceNodeModules = join13(workspace, "node_modules");
2539
- const constantsEntry = isBuiltPackage ? join13(embeddedEdgeSrc, "..", "..", "constants", "src", "index.ts") : join13(monorepoRoot, "packages", "constants", "src", "index.ts");
2727
+ if (specifier.startsWith("@playcademy/edge-play")) {
2728
+ return "Built-in backend runtime helpers are internal to the CLI-owned runtime.";
2729
+ }
2730
+ return "CLI-owned backend runtime packages are not available to game backend code.";
2731
+ }
2732
+ function createForbiddenRuntimeImportsPlugin(workspace) {
2540
2733
  return {
2541
- isBuiltPackage,
2542
- edgePlaySrc,
2543
- constantsEntry,
2544
- workspaceNodeModules,
2545
- cliDepsRoot
2734
+ name: "forbidden-backend-runtime-imports",
2735
+ setup(build2) {
2736
+ build2.onResolve(
2737
+ {
2738
+ filter: /^@playcademy\/(?:sdk\/server|edge-play(?:\/.*)?|constants(?:\/.*)?)$/
2739
+ },
2740
+ (args) => {
2741
+ if (!args.importer) {
2742
+ return null;
2743
+ }
2744
+ const importerPath = relative3(workspace, args.importer).replace(/\\/g, "/");
2745
+ return {
2746
+ errors: [
2747
+ {
2748
+ text: `Unsupported backend import "${args.path}" in ${importerPath}. ${getForbiddenImportMessage(args.path)} Use \`Context\`, \`c.env\`, \`c.get('sdk')\`, and \`c.get('playcademyUser')\` instead.`
2749
+ }
2750
+ ]
2751
+ };
2752
+ }
2753
+ );
2754
+ }
2546
2755
  };
2547
2756
  }
2548
2757
  function createEsbuildConfig(entryCode, paths, bundleConfig, customRoutesDir, options) {
2549
2758
  const workspace = getWorkspace();
2550
- const { edgePlaySrc, constantsEntry, workspaceNodeModules, cliDepsRoot } = paths;
2759
+ const { runtimeEntry, polyfillsEntry, workspaceNodeModules, cliDepsRoot } = paths;
2551
2760
  return {
2552
- // ──── Input Configuration ────
2761
+ // ──── Input: generated entry code, not a real file ────
2762
+ // resolveDir = workspace so that bare specifiers in the generated code
2763
+ // (e.g. '@game-api/save.ts') resolve relative to the user's project root.
2553
2764
  stdin: {
2554
2765
  contents: entryCode,
2555
- // Generated entry code with custom route imports
2556
- resolveDir: edgePlaySrc,
2557
- // Resolve relative imports from edge-play/src
2766
+ resolveDir: workspace,
2558
2767
  loader: "ts"
2559
- // Treat input as TypeScript
2560
2768
  },
2561
- // ──── Output Configuration ────
2769
+ // ──── Output configuration ────
2562
2770
  bundle: true,
2563
- // Bundle all dependencies into single file
2564
2771
  format: "esm",
2565
- // Output ES modules (required for Cloudflare Workers)
2772
+ // Cloudflare Workers require ES modules
2566
2773
  platform: "browser",
2567
- // Workers use browser APIs, not Node.js
2774
+ // Workers run in a browser-like environment, not Node.js
2568
2775
  target: "es2022",
2569
- // Modern JavaScript for Workers runtime
2570
2776
  write: false,
2571
- // Return code as string (don't write to disk)
2777
+ // Return code as a string; the caller writes/uploads it
2572
2778
  sourcemap: options.sourcemap ? "inline" : false,
2573
2779
  minify: options.minify || false,
2574
- logLevel: "error",
2575
- // Only show errors (suppress warnings)
2576
- // ──── Module Resolution ────
2577
- // Tell esbuild where to find node_modules for bare imports
2578
- // Dev: workspace node_modules + monorepo root (hoisted deps)
2579
- // Published: workspace node_modules + same (user's project)
2580
- // Binary: workspace node_modules + .playcademy/node_modules (hono lives there)
2780
+ logLevel: "silent",
2781
+ // errors propagate as thrown exceptions; we don't want esbuild double-printing to stderr
2782
+ // ──── Module resolution ────
2783
+ // esbuild searches nodePaths in order for bare specifiers.
2784
+ // workspaceNodeModules: game project's own deps (hono, etc. if user installed them)
2785
+ // cliDepsRoot: CLI-managed deps (.playcademy/node_modules in binary mode,
2786
+ // monorepo root in dev mode) — where hono, @hono/node-server live
2581
2787
  nodePaths: [workspaceNodeModules, cliDepsRoot],
2582
- // ──── Build-time Constants ────
2583
- // Inject the Playcademy config as a global constant
2584
- // Code can access it via: const config = PLAYCADEMY_CONFIG
2788
+ // ──── Build-time constants ────
2789
+ // PLAYCADEMY_CONFIG is accessed as a global in the generated entry code and in
2790
+ // the sealed runtime itself (passed to createBaseApp). It carries integrations
2791
+ // config, route metadata, queue metadata, etc.
2585
2792
  define: {
2586
2793
  PLAYCADEMY_CONFIG: JSON.stringify(bundleConfig)
2587
2794
  },
2588
- // ──── Import Aliases ────
2589
2795
  alias: {
2590
- // ┌─ Workspace-only package resolution ─────────────────────────────┐
2591
- // │ @playcademy/constants is a workspace package that users don't
2592
- // │ install. We embed it in dist/ and alias imports to point there.
2593
- // └─────────────────────────────────────────────────────────────────┘
2594
- "@playcademy/constants": constantsEntry,
2595
- // ┌─ Workspace-only edge-play package ──────────────────────────────┐
2596
- // @playcademy/edge-play is used in generated entry code for auth │
2597
- // │ middleware. It's embedded in dist/ like constants.
2598
- // └─────────────────────────────────────────────────────────────────┘
2599
- "@playcademy/edge-play": edgePlaySrc,
2600
- // ┌─ User's custom routes ──────────────────────────────────────────┐
2601
- // │ @game-api is a virtual module that maps to the user's API dir. │
2602
- // Example: import * as route from '@game-api/hello.ts' │
2603
- // │ Resolves to: /user-project/server/api/hello.ts
2604
- // └─────────────────────────────────────────────────────────────────┘
2605
- "@game-api": join13(workspace, customRoutesDir),
2606
- // ┌─ User's server lib directory ───────────────────────────────────┐
2607
- // │ @game-server is a virtual module for server utilities/config │
2608
- // Example: import { getAuth } from '@game-server/lib/auth' │
2609
- // │ Resolves to: /user-project/server/lib/auth.ts
2610
- // └─────────────────────────────────────────────────────────────────┘
2611
- "@game-server": join13(workspace, "server"),
2612
- // ┌─ Node.js polyfills for Cloudflare Workers ──────────────────────┐
2613
- // │ Workers don't have fs, path, os, etc. Redirect to polyfills │
2614
- // │ that throw helpful errors if user code tries to use them. │
2615
- // └─────────────────────────────────────────────────────────────────┘
2616
- fs: join13(edgePlaySrc, "polyfills.js"),
2617
- "fs/promises": join13(edgePlaySrc, "polyfills.js"),
2618
- path: join13(edgePlaySrc, "polyfills.js"),
2619
- os: join13(edgePlaySrc, "polyfills.js"),
2620
- process: join13(edgePlaySrc, "polyfills.js")
2796
+ // ┌─ Sealed backend runtime ────────────────────────────────────────────┐
2797
+ // │ The pre-bundled artifact: edge-play + sdk/server + all internal
2798
+ // │ @playcademy/* deps, zero external imports remaining. This alias
2799
+ // │ is what enforces version isolation — game node_modules are ignored. │
2800
+ // └─────────────────────────────────────────────────────────────────────┘
2801
+ "@playcademy/backend-runtime": runtimeEntry,
2802
+ // ┌─ Game's custom route files ─────────────────────────────────────────┐
2803
+ // │ @game-api is a virtual module pointing to the game's API directory.
2804
+ // │ Example: import * as route from '@game-api/save.ts' │
2805
+ // │ Resolves to: /user-project/server/api/save.ts │
2806
+ // └─────────────────────────────────────────────────────────────────────┘
2807
+ "@game-api": join14(workspace, customRoutesDir),
2808
+ // ┌─ Game's server library directory ───────────────────────────────────┐
2809
+ // │ @game-server is a virtual module for server-side utilities.
2810
+ // │ Example: import { getAuth } from '@game-server/lib/auth' │
2811
+ // │ Resolves to: /user-project/server/lib/auth.ts │
2812
+ // └─────────────────────────────────────────────────────────────────────┘
2813
+ "@game-server": join14(workspace, "server"),
2814
+ // ┌─ Node.js polyfills for Cloudflare Workers ──────────────────────────┐
2815
+ // │ Workers don't have fs, path, os, process, etc. Redirect them to a
2816
+ // │ polyfills stub bundled into the sealed runtime that throws a clear │
2817
+ // error if user code actually calls them at runtime. │
2818
+ // └─────────────────────────────────────────────────────────────────────┘
2819
+ ...createNodePolyfillAliases(polyfillsEntry)
2621
2820
  },
2622
- // ──── Build Plugins ────
2623
- plugins: [textLoaderPlugin()],
2624
- // Support Bun's 'with { type: "text" }' imports
2625
- // ──── External Dependencies ────
2821
+ plugins: [createForbiddenRuntimeImportsPlugin(workspace)],
2626
2822
  external: []
2627
- // Bundle everything (no externals for Workers)
2823
+ // Bundle everything no externals allowed in Workers
2628
2824
  };
2629
2825
  }
2630
- function loadEntryTemplate(paths) {
2631
- const entryPath = paths ? join13(paths.edgePlaySrc, "entry.ts") : join13(getMonorepoRoot(), "packages", "edge-play", "src", "entry.ts");
2632
- return readFileSync3(entryPath, "utf8");
2633
- }
2634
2826
  async function bundleBackend(config, options = {}) {
2635
2827
  const esbuild3 = await import("esbuild");
2636
2828
  const { customRouteData, customRoutesDir } = await discoverCustomRoutes(config);
2637
2829
  const queueHandlers = discoverQueueHandlers();
2638
2830
  validateQueueHandlersConfig(config, queueHandlers);
2639
- const paths = resolveEmbeddedSourcePaths();
2640
- const entryTemplate = loadEntryTemplate(paths);
2831
+ const paths = await resolveBackendRuntimePaths();
2641
2832
  const bundleConfig = {
2642
2833
  ...config,
2643
2834
  __routeMetadata: customRouteData,
2644
2835
  __queueMetadata: queueHandlers
2645
2836
  };
2646
- const hasAuth = Boolean(config.integrations?.auth);
2647
2837
  const entryCode = generateEntryCode(
2648
2838
  customRouteData,
2649
2839
  customRoutesDir,
2650
2840
  queueHandlers,
2651
- hasAuth,
2652
- entryTemplate
2841
+ Boolean(config.integrations?.auth)
2653
2842
  );
2654
2843
  const buildConfig = createEsbuildConfig(
2655
2844
  entryCode,
@@ -2662,9 +2851,8 @@ async function bundleBackend(config, options = {}) {
2662
2851
  if (!result.outputFiles?.[0]) {
2663
2852
  throw new Error("Backend bundling failed: no output");
2664
2853
  }
2665
- const code = result.outputFiles[0].text;
2666
2854
  return {
2667
- code,
2855
+ code: result.outputFiles[0].text,
2668
2856
  config,
2669
2857
  customRoutes: customRouteData
2670
2858
  };
@@ -2672,50 +2860,69 @@ async function bundleBackend(config, options = {}) {
2672
2860
  function buildCustomRouteImportStatements(customRoutes, customRoutesDir) {
2673
2861
  const normalizedRoutesDir = customRoutesDir.replace(/\\/g, "/").replace(/\/+/g, "/");
2674
2862
  const customRoutesPrefix = normalizedRoutesDir.endsWith("/") ? normalizedRoutesDir : `${normalizedRoutesDir}/`;
2675
- return customRoutes.map((route, i) => {
2863
+ return customRoutes.map((route, index) => {
2676
2864
  const normalizedFile = route.file.replace(/\\/g, "/").replace(/\/+/g, "/");
2677
2865
  const importPath = normalizedFile.startsWith(customRoutesPrefix) ? normalizedFile.slice(customRoutesPrefix.length) : normalizedFile;
2678
- return `import * as customRoute${i} from '@game-api/${importPath}'`;
2866
+ return `import * as customRoute${index} from '@game-api/${importPath}'`;
2679
2867
  }).join("\n");
2680
2868
  }
2681
- function generateEntryCode(customRoutes, customRoutesDir, queueHandlers, hasAuth, entryTemplateInput = loadEntryTemplate()) {
2682
- const importStatements = customRoutes.length > 0 ? buildCustomRouteImportStatements(customRoutes, customRoutesDir) : "// No custom routes";
2683
- const queueImportStatements = buildQueueImportStatements(queueHandlers);
2684
- const withCustomRouteImports = entryTemplateInput.replace(
2685
- "// \u26A0\uFE0F BUILD_MARKER: CUSTOM_ROUTE_IMPORTS \u26A0\uFE0F",
2686
- importStatements
2687
- );
2688
- const withImports = withCustomRouteImports.replace(
2689
- "// \u26A0\uFE0F BUILD_MARKER: QUEUE_HANDLER_IMPORTS \u26A0\uFE0F",
2690
- queueImportStatements
2691
- );
2692
- const registrationStatements = customRoutes.length > 0 ? customRoutes.flatMap((route, i) => {
2693
- const methods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
2694
- return methods.map((method) => {
2695
- const methodLower = method.toLowerCase();
2696
- return `if (customRoute${i}.${method}) app.${methodLower}('${route.path}', customRoute${i}.${method})`;
2697
- });
2698
- }) : [];
2699
- const registrationCode = registrationStatements.length > 0 ? ["// Custom routes", ...registrationStatements, ""].join("\n") : "// No custom routes\n";
2700
- const withRoutes = withImports.replace("// \u26A0\uFE0F BUILD_MARKER: CUSTOM_ROUTES \u26A0\uFE0F", registrationCode);
2701
- const queueHandlerCode = buildQueueHandlerCode(queueHandlers);
2702
- const withQueueHandlers = withRoutes.replace(
2703
- "// \u26A0\uFE0F BUILD_MARKER: QUEUE_HANDLER \u26A0\uFE0F",
2704
- queueHandlerCode
2705
- );
2706
- const exportCode = buildWorkerExportCode(queueHandlers);
2707
- const withExport = withQueueHandlers.replace("// \u26A0\uFE0F BUILD_MARKER: WORKER_EXPORT \u26A0\uFE0F", exportCode);
2708
- const sessionMiddlewareStatements = hasAuth ? [
2709
- "import { createSessionMiddleware } from '@playcademy/edge-play'",
2710
- "import { getAuth } from '@game-server/lib/auth'",
2711
- "app.use('*', createSessionMiddleware(getAuth))"
2712
- ] : [];
2713
- const sessionMiddlewareCode = sessionMiddlewareStatements.join("\n");
2714
- const result = withExport.replace(
2715
- "// \u26A0\uFE0F BUILD_MARKER: SESSION_MIDDLEWARE \u26A0\uFE0F",
2716
- sessionMiddlewareCode
2869
+ function buildCustomRouteModuleCode(customRoutes) {
2870
+ if (customRoutes.length === 0) {
2871
+ return "const __customRoutes = []";
2872
+ }
2873
+ return [
2874
+ "const __customRoutes = [",
2875
+ ...customRoutes.map(
2876
+ (route, index) => ` { path: ${JSON.stringify(route.path)}, module: customRoute${index} },`
2877
+ ),
2878
+ "]"
2879
+ ].join("\n");
2880
+ }
2881
+ function generateEntryCode(customRoutes, customRoutesDir, queueHandlers, hasAuth = false) {
2882
+ const runtimeImports = [
2883
+ "createBaseApp",
2884
+ "registerBuiltinRoutes",
2885
+ "registerCustomRouteModules",
2886
+ "registerTerminalHandlers"
2887
+ ];
2888
+ if (hasAuth) {
2889
+ runtimeImports.push("createSessionMiddleware");
2890
+ }
2891
+ if (queueHandlers.length > 0) {
2892
+ runtimeImports.push("createQueueRouter", "registerQueueIngressRoute");
2893
+ }
2894
+ const importBlocks = [
2895
+ `import { ${runtimeImports.join(", ")} } from '@playcademy/backend-runtime'`
2896
+ ];
2897
+ if (hasAuth) {
2898
+ importBlocks.push("import { getAuth } from '@game-server/lib/auth'");
2899
+ }
2900
+ if (customRoutes.length > 0) {
2901
+ importBlocks.push(buildCustomRouteImportStatements(customRoutes, customRoutesDir));
2902
+ }
2903
+ if (queueHandlers.length > 0) {
2904
+ importBlocks.push(buildQueueImportStatements(queueHandlers));
2905
+ }
2906
+ const body = [
2907
+ "// PLAYCADEMY_CONFIG is injected at bundle time via esbuild define",
2908
+ "const app = createBaseApp(PLAYCADEMY_CONFIG)",
2909
+ ""
2910
+ ];
2911
+ if (hasAuth) {
2912
+ body.push("app.use('*', createSessionMiddleware(getAuth))", "");
2913
+ }
2914
+ body.push(
2915
+ "await registerBuiltinRoutes(app, PLAYCADEMY_CONFIG.integrations)",
2916
+ "",
2917
+ buildCustomRouteModuleCode(customRoutes),
2918
+ "registerCustomRouteModules(app, __customRoutes)",
2919
+ "",
2920
+ buildQueueHandlerCode(queueHandlers),
2921
+ "registerTerminalHandlers(app)",
2922
+ "",
2923
+ buildWorkerExportCode(queueHandlers)
2717
2924
  );
2718
- return result;
2925
+ return [...importBlocks, "", ...body].join("\n");
2719
2926
  }
2720
2927
 
2721
2928
  // src/lib/init/prompts.ts
@@ -2729,8 +2936,8 @@ var authSchemaTemplate = loadTemplateString("auth/auth-schema.ts");
2729
2936
  var protectedRouteTemplate = loadTemplateString("api/sample-protected.ts");
2730
2937
 
2731
2938
  // src/lib/init/database.ts
2732
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
2733
- import { join as join14 } from "node:path";
2939
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
2940
+ import { join as join15 } from "node:path";
2734
2941
  var drizzleConfigTemplate = loadTemplateString("database/drizzle-config.ts");
2735
2942
  var dbSchemaExampleTemplate = loadTemplateString("database/db-schema-example.ts");
2736
2943
  var dbSchemaIndexTemplate = loadTemplateString("database/db-schema-index.ts");
@@ -2740,7 +2947,7 @@ var dbSeedTemplate = loadTemplateString("database/db-seed.ts");
2740
2947
  var packageTemplate = loadTemplateString("database/package.json");
2741
2948
  function hasDatabaseSetup() {
2742
2949
  const workspace = getWorkspace();
2743
- return DRIZZLE_CONFIG_FILES.some((filename) => existsSync9(join14(workspace, filename)));
2950
+ return DRIZZLE_CONFIG_FILES.some((filename) => existsSync9(join15(workspace, filename)));
2744
2951
  }
2745
2952
 
2746
2953
  // src/lib/init/scaffold.ts
@@ -2762,17 +2969,17 @@ function hasBucketSetup(config) {
2762
2969
 
2763
2970
  // src/lib/init/types.ts
2764
2971
  import { execSync as execSync2 } from "node:child_process";
2765
- import { existsSync as existsSync12, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
2766
- import { dirname as dirname5, join as join17, resolve as resolve4 } from "node:path";
2972
+ import { existsSync as existsSync12, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
2973
+ import { dirname as dirname5, join as join18, resolve as resolve5 } from "node:path";
2767
2974
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2768
2975
 
2769
2976
  // src/lib/deploy/backend.ts
2770
2977
  import { existsSync as existsSync10 } from "node:fs";
2771
- import { join as join15 } from "node:path";
2978
+ import { join as join16 } from "node:path";
2772
2979
  function getCustomRoutesDirectory(projectPath, config) {
2773
2980
  const customRoutes = config?.integrations?.customRoutes;
2774
2981
  const customRoutesDir = typeof customRoutes === "object" && customRoutes.directory || DEFAULT_API_ROUTES_DIRECTORY;
2775
- return join15(projectPath, customRoutesDir);
2982
+ return join16(projectPath, customRoutesDir);
2776
2983
  }
2777
2984
  function hasLocalCustomRoutes(projectPath, config) {
2778
2985
  const customRoutesDir = getCustomRoutesDirectory(projectPath, config);
@@ -2817,8 +3024,8 @@ function validateIntegrationSecrets(config, envSecrets) {
2817
3024
 
2818
3025
  // src/lib/init/tsconfig.ts
2819
3026
  init_file_loader();
2820
- import { existsSync as existsSync11, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
2821
- import { join as join16 } from "path";
3027
+ import { existsSync as existsSync11, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
3028
+ import { join as join17 } from "path";
2822
3029
  function hasAllRequiredIncludes(config) {
2823
3030
  if (!config.include) {
2824
3031
  return false;
@@ -2891,7 +3098,7 @@ function addToIncludeArrayPreservingComments(content) {
2891
3098
  }
2892
3099
  async function ensureTsconfigIncludes(workspace) {
2893
3100
  for (const filename of TSCONFIG_FILES) {
2894
- const configPath = join16(workspace, filename);
3101
+ const configPath = join17(workspace, filename);
2895
3102
  if (existsSync11(configPath)) {
2896
3103
  try {
2897
3104
  const config = await loadFile(configPath, {
@@ -2903,7 +3110,7 @@ async function ensureTsconfigIncludes(workspace) {
2903
3110
  return filename;
2904
3111
  }
2905
3112
  try {
2906
- const rawContent = readFileSync5(configPath, "utf8");
3113
+ const rawContent = readFileSync4(configPath, "utf8");
2907
3114
  const updatedContent = addToIncludeArrayPreservingComments(rawContent);
2908
3115
  if (updatedContent && updatedContent !== rawContent) {
2909
3116
  writeFileSync5(configPath, updatedContent);
@@ -2943,14 +3150,14 @@ function getDepVersions() {
2943
3150
  return { honoVersion: "^4.9.9", workersTypesVersion: "^4.20251011.0" };
2944
3151
  }
2945
3152
  try {
2946
- const pkgPath = resolve4(
3153
+ const pkgPath = resolve5(
2947
3154
  dirname5(fileURLToPath2(import.meta.url)),
2948
3155
  "..",
2949
3156
  "..",
2950
3157
  "..",
2951
3158
  "package.json"
2952
3159
  );
2953
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
3160
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
2954
3161
  return {
2955
3162
  honoVersion: pkg.dependencies?.hono ?? "latest",
2956
3163
  workersTypesVersion: pkg.devDependencies?.["@cloudflare/workers-types"] ?? "latest"
@@ -2960,8 +3167,8 @@ function getDepVersions() {
2960
3167
  }
2961
3168
  }
2962
3169
  async function setupPlaycademyDependencies(workspace) {
2963
- const playcademyDir = join17(workspace, CLI_DIRECTORIES.WORKSPACE);
2964
- const playcademyPkgPath = join17(playcademyDir, "package.json");
3170
+ const playcademyDir = join18(workspace, CLI_DIRECTORIES.WORKSPACE);
3171
+ const playcademyPkgPath = join18(playcademyDir, "package.json");
2965
3172
  const { honoVersion, workersTypesVersion } = getDepVersions();
2966
3173
  const playcademyPkg = {
2967
3174
  private: true,
@@ -3057,14 +3264,14 @@ async function ensurePlaycademyTypes(options = {}) {
3057
3264
  const bindingsStr = generateBindingsTypeString(features, queueBindings);
3058
3265
  const secretsStr = await generateSecretsTypeString(workspace, verbose);
3059
3266
  const hasAuth = Boolean(config.integrations?.auth);
3060
- const hasAuthFile = existsSync12(join17(workspace, "server/lib/auth.ts"));
3267
+ const hasAuthFile = existsSync12(join18(workspace, "server/lib/auth.ts"));
3061
3268
  const authVariablesString = generateAuthVariablesString(hasAuth, hasAuthFile);
3062
3269
  let envContent = playcademyEnvTemplate.replace("{{BINDINGS}}", bindingsStr);
3063
3270
  envContent = envContent.replace("{{SECRETS}}", secretsStr);
3064
3271
  envContent = envContent.replace("{{AUTH_IMPORT}}", authVariablesString.authImport);
3065
3272
  envContent = envContent.replace("{{VARIABLES}}", authVariablesString.variables);
3066
3273
  envContent = envContent.replace("{{CONTEXT_VARS}}", authVariablesString.contextVars);
3067
- const envPath = join17(workspace, "playcademy-env.d.ts");
3274
+ const envPath = join18(workspace, "playcademy-env.d.ts");
3068
3275
  writeFileSync6(envPath, envContent);
3069
3276
  if (verbose) {
3070
3277
  logger.success(`Generated <playcademy-env.d.ts>`);
@@ -3099,12 +3306,12 @@ import { blue as blue2, cyan as cyan3, green as green3, magenta, red as red3, ye
3099
3306
 
3100
3307
  // src/lib/init/engine/hooks.ts
3101
3308
  import { existsSync as existsSync13, mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "fs";
3102
- import { rename, rm } from "fs/promises";
3103
- import { dirname as dirname6, join as join18 } from "path";
3309
+ import { rename, rm as rm2 } from "fs/promises";
3310
+ import { dirname as dirname6, join as join19 } from "path";
3104
3311
  import JSZip from "jszip";
3105
3312
  async function godotAfterFetch({ destDir }) {
3106
- const addonsDir = join18(destDir, "addons");
3107
- const targetDir = join18(addonsDir, "playcademy");
3313
+ const addonsDir = join19(destDir, "addons");
3314
+ const targetDir = join19(addonsDir, "playcademy");
3108
3315
  let zipData;
3109
3316
  await runStep(
3110
3317
  "Downloading Playcademy Addon for Godot",
@@ -3132,7 +3339,7 @@ async function godotAfterFetch({ destDir }) {
3132
3339
  if (file.dir) {
3133
3340
  return;
3134
3341
  }
3135
- const extractPath = join18(destDir, relativePath);
3342
+ const extractPath = join19(destDir, relativePath);
3136
3343
  extractPromises.push(
3137
3344
  file.async("nodebuffer").then((content) => {
3138
3345
  const dir = dirname6(extractPath);
@@ -3144,14 +3351,14 @@ async function godotAfterFetch({ destDir }) {
3144
3351
  );
3145
3352
  });
3146
3353
  await Promise.all(extractPromises);
3147
- const extractedRoot = join18(destDir, "playcademy", "addons", "playcademy");
3354
+ const extractedRoot = join19(destDir, "playcademy", "addons", "playcademy");
3148
3355
  if (existsSync13(extractedRoot)) {
3149
3356
  if (existsSync13(targetDir)) {
3150
- await rm(targetDir, { recursive: true, force: true });
3357
+ await rm2(targetDir, { recursive: true, force: true });
3151
3358
  }
3152
3359
  await rename(extractedRoot, targetDir);
3153
- const extractedParent = join18(destDir, "playcademy");
3154
- await rm(extractedParent, { recursive: true, force: true });
3360
+ const extractedParent = join19(destDir, "playcademy");
3361
+ await rm2(extractedParent, { recursive: true, force: true });
3155
3362
  }
3156
3363
  },
3157
3364
  "Playcademy Addon extracted",
@@ -3363,9 +3570,9 @@ async function startDevServer(options) {
3363
3570
  return { server: mf, port };
3364
3571
  }
3365
3572
  async function ensureDatabaseDirectory() {
3366
- const dbDir = join19(getWorkspace(), CLI_DIRECTORIES.DATABASE);
3573
+ const dbDir = join20(getWorkspace(), CLI_DIRECTORIES.DATABASE);
3367
3574
  try {
3368
- await mkdir2(dbDir, { recursive: true });
3575
+ await mkdir3(dbDir, { recursive: true });
3369
3576
  } catch (error) {
3370
3577
  throw new Error(`Failed to create database directory: ${getErrorMessage(error)}`, {
3371
3578
  cause: error
@@ -3374,9 +3581,9 @@ async function ensureDatabaseDirectory() {
3374
3581
  return dbDir;
3375
3582
  }
3376
3583
  async function ensureKvDirectory() {
3377
- const kvDir = join19(getWorkspace(), CLI_DIRECTORIES.KV);
3584
+ const kvDir = join20(getWorkspace(), CLI_DIRECTORIES.KV);
3378
3585
  try {
3379
- await mkdir2(kvDir, { recursive: true });
3586
+ await mkdir3(kvDir, { recursive: true });
3380
3587
  } catch (error) {
3381
3588
  throw new Error(`Failed to create KV directory: ${getErrorMessage(error)}`, {
3382
3589
  cause: error
@@ -3385,9 +3592,9 @@ async function ensureKvDirectory() {
3385
3592
  return kvDir;
3386
3593
  }
3387
3594
  async function ensureBucketDirectory() {
3388
- const bucketDir = join19(getWorkspace(), CLI_DIRECTORIES.BUCKET);
3595
+ const bucketDir = join20(getWorkspace(), CLI_DIRECTORIES.BUCKET);
3389
3596
  try {
3390
- await mkdir2(bucketDir, { recursive: true });
3597
+ await mkdir3(bucketDir, { recursive: true });
3391
3598
  } catch (error) {
3392
3599
  throw new Error(`Failed to create bucket directory: ${getErrorMessage(error)}`, {
3393
3600
  cause: error
@@ -3410,7 +3617,7 @@ async function writeBackendServerInfo(port) {
3410
3617
  }
3411
3618
 
3412
3619
  // src/lib/dev/reload.ts
3413
- import { join as join20, relative as relative3 } from "path";
3620
+ import { join as join21, relative as relative4 } from "path";
3414
3621
  import chokidar from "chokidar";
3415
3622
  import { bold as bold6, cyan as cyan5, dim as dim4, green as green4 } from "colorette";
3416
3623
  function formatTime() {
@@ -3427,9 +3634,9 @@ function startHotReload(onReload, options = {}) {
3427
3634
  const customRoutesConfig = options.config?.integrations?.customRoutes;
3428
3635
  const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
3429
3636
  const watchPaths = [
3430
- join20(workspace, customRoutesDir),
3431
- join20(workspace, "playcademy.config.js"),
3432
- join20(workspace, "playcademy.config.json")
3637
+ join21(workspace, customRoutesDir),
3638
+ join21(workspace, "playcademy.config.js"),
3639
+ join21(workspace, "playcademy.config.json")
3433
3640
  ];
3434
3641
  const watcher = chokidar.watch(watchPaths, {
3435
3642
  persistent: true,
@@ -3441,7 +3648,7 @@ function startHotReload(onReload, options = {}) {
3441
3648
  });
3442
3649
  const logSuccess = options.onSuccess || ((changedPath, eventType) => {
3443
3650
  if (changedPath) {
3444
- const relativePath = relative3(workspace, changedPath);
3651
+ const relativePath = relative4(workspace, changedPath);
3445
3652
  const timestamp = dim4(formatTime());
3446
3653
  const brand = bold6(cyan5("[playcademy]"));
3447
3654
  const event = eventType === "changed" ? green4("reload") : green4(eventType || "reload");