playcademy 0.12.3 → 0.12.5

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.
@@ -20,9 +20,28 @@
20
20
  <body
21
21
  class="min-h-screen flex items-center justify-center bg-black dark:bg-white transition-colors relative overflow-hidden"
22
22
  >
23
+ <!-- Top left badges -->
24
+ <a
25
+ href="{{PLAYCADEMY_HUB_URL}}"
26
+ target="_blank"
27
+ rel="noopener noreferrer"
28
+ class="fixed top-5 left-5 px-2 py-1 rounded bg-[rgba(23,23,23,0.9)] hover:bg-[rgba(23,23,23,1)] dark:bg-[rgba(255,255,255,0.9)] dark:hover:bg-[rgba(255,255,255,1)] text-white dark:text-black border border-white/20 dark:border-black/20 backdrop-blur-lg transition-colors z-[9999] no-underline"
29
+ style="font-family: 'VT323', monospace; font-size: 1rem; letter-spacing: 0.05em"
30
+ >
31
+ PLAYCADEMY
32
+ </a>
33
+ <a
34
+ href="/api"
35
+ class="fixed top-5 left-[7.5rem] px-2 py-1 rounded bg-[rgba(23,23,23,0.9)] hover:bg-[rgba(23,23,23,1)] dark:bg-[rgba(255,255,255,0.9)] dark:hover:bg-[rgba(255,255,255,1)] text-white dark:text-black border border-white/20 dark:border-black/20 backdrop-blur-lg transition-colors z-[9999] no-underline"
36
+ style="font-family: 'VT323', monospace; font-size: 1rem; letter-spacing: 0.05em"
37
+ >
38
+ VIEW ROUTES
39
+ </a>
40
+
41
+ <!-- Theme toggle -->
23
42
  <button
24
43
  id="themeToggle"
25
- class="fixed top-5 right-5 w-8 h-8 flex items-center justify-center cursor-pointer opacity-40 hover:opacity-100 transition-opacity"
44
+ class="fixed top-5 right-5 w-8 h-8 flex items-center justify-center cursor-pointer opacity-40 hover:opacity-100 transition-opacity z-[9999]"
26
45
  >
27
46
  <svg
28
47
  id="sunIcon"
@@ -47,6 +66,9 @@
47
66
  </svg>
48
67
  </button>
49
68
 
69
+ <!-- Pixel trail container -->
70
+ <div id="pixelTrail" class="absolute inset-0 z-0"></div>
71
+
50
72
  <!-- Subtle background enhancements: vignette + grid (theme-aware) -->
51
73
  <div class="pointer-events-none absolute inset-0 z-0">
52
74
  <!-- Light mode vignette (white on black) -->
@@ -93,45 +115,14 @@
93
115
  ></div>
94
116
  </div>
95
117
 
96
- <div class="max-w-2xl mx-auto px-6 py-12 relative z-10">
97
- <div class="text-center space-y-8">
98
- <div>
99
- <h1
100
- class="text-4xl md:text-5xl font-bold text-white dark:text-black mb-3"
101
- style="font-family: 'Tomorrow', sans-serif; letter-spacing: -0.02em"
102
- >
103
- {{GAME_NAME}}
104
- </h1>
105
- <p
106
- class="text-lg text-gray-500 dark:text-gray-500"
107
- style="font-family: 'VT323', monospace; letter-spacing: 0.1em"
108
- >
109
- GAME BACKEND API
110
- </p>
111
- </div>
112
-
113
- <a
114
- href="/api"
115
- 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"
116
- style="font-family: 'VT323', monospace; letter-spacing: 0.05em; font-size: 1rem"
117
- >
118
- VIEW ROUTES →
119
- </a>
120
-
121
- <div
122
- class="pt-8 text-xs text-gray-600 dark:text-gray-500"
123
- style="font-family: 'VT323', monospace; letter-spacing: 0.05em"
124
- >
125
- POWERED BY
126
- <a
127
- href="{{PLAYCADEMY_HUB_URL}}"
128
- target="_blank"
129
- rel="noopener noreferrer"
130
- class="font-bold hover:text-gray-500 dark:hover:text-gray-600 transition-colors underline decoration-dotted underline-offset-2"
131
- >PLAYCADEMY</a
132
- >
133
- </div>
134
- </div>
118
+ <!-- Center content -->
119
+ <div class="relative z-10">
120
+ <h1
121
+ class="text-6xl md:text-7xl lg:text-6xl font-bold text-white dark:text-black text-center"
122
+ style="font-family: 'Tomorrow', sans-serif; letter-spacing: -0.02em"
123
+ >
124
+ {{GAME_NAME}}
125
+ </h1>
135
126
  </div>
136
127
 
137
128
  <script>
@@ -152,10 +143,9 @@
152
143
  }
153
144
  }
154
145
 
155
- // Initialize theme from localStorage or system preference
146
+ // Initialize theme from localStorage, defaulting to dark mode
156
147
  const savedTheme = localStorage.getItem('theme')
157
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
158
- const isDark = savedTheme === 'dark' || (!savedTheme && prefersDark)
148
+ const isDark = savedTheme !== 'light' // Default to dark unless explicitly set to light
159
149
  setTheme(isDark)
160
150
 
161
151
  // Toggle on click
@@ -165,5 +155,99 @@
165
155
  localStorage.setItem('theme', willBeDark ? 'dark' : 'light')
166
156
  })
167
157
  </script>
158
+
159
+ <script>
160
+ // Pixel Trail Effect
161
+ const pixelTrail = document.getElementById('pixelTrail')
162
+ const PIXEL_SIZE = 20 // px
163
+ const FADE_DURATION = 300 // ms
164
+
165
+ // Arcade color palette - vibrant neon colors
166
+ const ARCADE_COLORS = [
167
+ 'rgba(255, 0, 255, 0.8)', // Neon Pink/Magenta
168
+ 'rgba(0, 255, 255, 0.8)', // Neon Cyan
169
+ 'rgba(255, 255, 0, 0.8)', // Neon Yellow
170
+ 'rgba(0, 255, 0, 0.8)', // Neon Green
171
+ 'rgba(255, 105, 180, 0.8)', // Hot Pink
172
+ 'rgba(138, 43, 226, 0.8)', // Blue Violet
173
+ 'rgba(50, 205, 50, 0.8)', // Lime Green
174
+ ]
175
+
176
+ let pixelGrid = {}
177
+ let columns = 0
178
+ let rows = 0
179
+
180
+ function getRandomArcadeColor() {
181
+ return ARCADE_COLORS[Math.floor(Math.random() * ARCADE_COLORS.length)]
182
+ }
183
+
184
+ function initPixelGrid() {
185
+ const width = window.innerWidth
186
+ const height = window.innerHeight
187
+ columns = Math.ceil(width / PIXEL_SIZE)
188
+ rows = Math.ceil(height / PIXEL_SIZE)
189
+
190
+ // Clear existing grid
191
+ pixelTrail.innerHTML = ''
192
+ pixelGrid = {}
193
+
194
+ // Create pixel grid
195
+ for (let row = 0; row < rows; row++) {
196
+ for (let col = 0; col < columns; col++) {
197
+ const pixel = document.createElement('div')
198
+ pixel.className = 'absolute pointer-events-none transition-opacity'
199
+ pixel.style.width = `${PIXEL_SIZE}px`
200
+ pixel.style.height = `${PIXEL_SIZE}px`
201
+ pixel.style.left = `${col * PIXEL_SIZE}px`
202
+ pixel.style.top = `${row * PIXEL_SIZE}px`
203
+ pixel.style.opacity = '0'
204
+ pixel.style.transitionDuration = `${FADE_DURATION}ms`
205
+ pixel.style.borderRadius = '2px' // Slight rounding for arcade feel
206
+
207
+ pixelTrail.appendChild(pixel)
208
+ pixelGrid[`${col}-${row}`] = pixel
209
+ }
210
+ }
211
+ }
212
+
213
+ function animatePixel(col, row) {
214
+ const key = `${col}-${row}`
215
+ const pixel = pixelGrid[key]
216
+ if (!pixel) return
217
+
218
+ // Cancel any ongoing animation
219
+ clearTimeout(pixel.fadeTimeout)
220
+
221
+ // Assign random arcade color
222
+ pixel.style.backgroundColor = getRandomArcadeColor()
223
+
224
+ // Show pixel
225
+ pixel.style.opacity = '1'
226
+
227
+ // Fade out after duration
228
+ pixel.fadeTimeout = setTimeout(() => {
229
+ pixel.style.opacity = '0'
230
+ }, FADE_DURATION)
231
+ }
232
+
233
+ function handleMouseMove(e) {
234
+ const col = Math.floor(e.clientX / PIXEL_SIZE)
235
+ const row = Math.floor(e.clientY / PIXEL_SIZE)
236
+ animatePixel(col, row)
237
+ }
238
+
239
+ // Initialize on load
240
+ initPixelGrid()
241
+
242
+ // Add event listeners
243
+ document.body.addEventListener('mousemove', handleMouseMove)
244
+
245
+ // Reinitialize on window resize
246
+ let resizeTimeout
247
+ window.addEventListener('resize', () => {
248
+ clearTimeout(resizeTimeout)
249
+ resizeTimeout = setTimeout(initPixelGrid, 200)
250
+ })
251
+ </script>
168
252
  </body>
169
253
  </html>
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { UserInfo, ApiKey, BackendDeploymentResponse } from '@playcademy/data/types';
2
+ import { SchemaInfo } from '@playcademy/cloudflare';
2
3
  import { OrganizationConfig, CourseConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/timeback/types';
3
4
  export { ComponentConfig, ComponentResourceConfig, CourseConfig, DerivedComponentConfig, DerivedComponentResourceConfig, DerivedCourseConfig, DerivedOrganizationConfig, DerivedResourceConfig, DerivedTimebackConfig, OrganizationConfig, ResourceConfig, TimebackGrade, TimebackSourcedIds, TimebackSubject } from '@playcademy/timeback/types';
4
5
  import { PlaycademyClient } from '@playcademy/sdk';
@@ -222,6 +223,32 @@ interface PlaycademyConfig {
222
223
  integrations?: IntegrationsConfig;
223
224
  }
224
225
 
226
+ /**
227
+ * Resource bindings for backend deployment
228
+ * Provider-agnostic abstraction for cloud resources
229
+ */
230
+ interface BackendResourceBindings {
231
+ /** SQL database instances to create and bind (maps to D1 on Cloudflare) */
232
+ database?: string[];
233
+ /** Key-value store namespaces to create and bind (maps to KV on Cloudflare) */
234
+ keyValue?: string[];
235
+ /** Object storage buckets to bind (maps to R2 on Cloudflare) */
236
+ bucket?: string[];
237
+ }
238
+ /**
239
+ * Backend deployment bundle for uploading to Playcademy platform
240
+ */
241
+ interface BackendDeploymentBundle {
242
+ /** Bundled JavaScript code ready for deployment */
243
+ code: string;
244
+ /** Game configuration */
245
+ config: PlaycademyConfig;
246
+ /** Optional resource bindings (database, storage, etc.) */
247
+ bindings?: BackendResourceBindings;
248
+ /** Optional schema information for database setup */
249
+ schema?: SchemaInfo;
250
+ }
251
+
225
252
  type GameMetadata = {
226
253
  description?: string;
227
254
  emoji?: string;
@@ -620,6 +647,43 @@ interface UpdateExistingGameOptions {
620
647
  previousBackendSize?: number;
621
648
  verbose?: boolean;
622
649
  }
650
+ /**
651
+ * Backend bundle with CLI-specific metadata
652
+ * Extends SDK's BackendDeploymentBundle with route discovery metadata
653
+ */
654
+ interface BackendBundle extends BackendDeploymentBundle {
655
+ /** Discovered custom routes (for CLI display and tracking) */
656
+ customRoutes: Array<{
657
+ path: string;
658
+ file: string;
659
+ methods?: string[];
660
+ }>;
661
+ }
662
+ /**
663
+ * Bundle configuration options
664
+ */
665
+ interface BundleOptions {
666
+ /** Include source map for debugging (default: false) */
667
+ sourcemap?: boolean;
668
+ /** Minify output (default: false) */
669
+ minify?: boolean;
670
+ }
671
+ /**
672
+ * Resolved paths for bundling embedded sources
673
+ * Used to support both monorepo development and published package scenarios
674
+ */
675
+ interface EmbeddedSourcePaths {
676
+ /** Whether we're running from a published package (vs monorepo dev) */
677
+ isBuiltPackage: boolean;
678
+ /** Path to edge-play sources (embedded or monorepo) */
679
+ edgePlaySrc: string;
680
+ /** Path to constants entry point (embedded or monorepo) */
681
+ constantsEntry: string;
682
+ /** User's workspace node_modules */
683
+ workspaceNodeModules: string;
684
+ /** CLI's node_modules (monorepo root in dev, same as workspace in published) */
685
+ cliNodeModules: string;
686
+ }
623
687
 
624
688
  /**
625
689
  * Types for the CLI local HTTP server that handles OAuth callbacks
@@ -801,4 +865,4 @@ interface DeploymentDiffOptions {
801
865
  integrations?: IntegrationsDiff;
802
866
  }
803
867
 
804
- export type { ApiConfig, ApiErrorResponse, ApiKeyListItem, ApiKeyWithSecret, ApiRequestOptions, AuthProfile, AuthStore, BackendDeploymentWithHash, BackendDiff, BuildDiff, CallbackServerResult, ConfigDiff, CreateApiKeyResponse, CustomRoutesIntegrationOptions, DatabaseIntegrationOptions, DeployConfig, DeployNewGameOptions, DeployedGameInfo, DeploymentChanges, DeploymentContext, DeploymentDiffOptions, DeploymentPlan, DeploymentResult, EnvironmentAuthProfiles, GameStore, IntegrationChangeDetector, IntegrationChangeDetectors, IntegrationConfigChange, IntegrationsConfig, IntegrationsDiff, LoginCredentials, LoginResponse, PlaycademyConfig, PreviewOptions, PreviewResponse, SignInResponse, SsoCallbackData, TimebackIntegrationConfig, TokenType, UpdateExistingGameOptions };
868
+ export type { ApiConfig, ApiErrorResponse, ApiKeyListItem, ApiKeyWithSecret, ApiRequestOptions, AuthProfile, AuthStore, BackendBundle, BackendDeploymentWithHash, BackendDiff, BuildDiff, BundleOptions, CallbackServerResult, ConfigDiff, CreateApiKeyResponse, CustomRoutesIntegrationOptions, DatabaseIntegrationOptions, DeployConfig, DeployNewGameOptions, DeployedGameInfo, DeploymentChanges, DeploymentContext, DeploymentDiffOptions, DeploymentPlan, DeploymentResult, EmbeddedSourcePaths, EnvironmentAuthProfiles, GameStore, IntegrationChangeDetector, IntegrationChangeDetectors, IntegrationConfigChange, IntegrationsConfig, IntegrationsDiff, LoginCredentials, LoginResponse, PlaycademyConfig, PreviewOptions, PreviewResponse, SignInResponse, SsoCallbackData, TimebackIntegrationConfig, TokenType, UpdateExistingGameOptions };
package/dist/index.js CHANGED
@@ -5222,9 +5222,8 @@ async function registerCustomRoutes(app, routes) {
5222
5222
  }
5223
5223
 
5224
5224
  // src/lib/deploy/bundle.ts
5225
- var entryTemplate = entry_default;
5226
- async function bundleBackend(config, options = {}) {
5227
- const esbuild = await import("esbuild");
5225
+ var entryTemplate = entry_default.toString();
5226
+ async function discoverCustomRoutes(config) {
5228
5227
  const workspace = getWorkspace();
5229
5228
  const customRoutesConfig = config.integrations?.customRoutes;
5230
5229
  const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
@@ -5235,89 +5234,115 @@ async function bundleBackend(config, options = {}) {
5235
5234
  // Use relative path (e.g., 'server/api/test.ts'), not absolute
5236
5235
  methods: r.methods
5237
5236
  }));
5238
- const bundleConfig = {
5239
- ...config,
5240
- customRoutes: customRouteData
5241
- };
5242
- const entryCode = generateEntryCode(customRouteData, customRoutesDir);
5237
+ return { customRouteData, customRoutesDir };
5238
+ }
5239
+ function resolveEmbeddedSourcePaths() {
5240
+ const workspace = getWorkspace();
5243
5241
  const distDir = new URL(".", import.meta.url).pathname;
5244
5242
  const embeddedEdgeSrc = join6(distDir, "edge-play", "src");
5243
+ const isBuiltPackage = existsSync7(embeddedEdgeSrc);
5245
5244
  const monorepoRoot = getMonorepoRoot();
5246
5245
  const monorepoEdgeSrc = join6(monorepoRoot, "packages/edge-play/src");
5247
- const isBuiltPackage = existsSync7(embeddedEdgeSrc);
5248
5246
  const edgePlaySrc = isBuiltPackage ? embeddedEdgeSrc : monorepoEdgeSrc;
5249
5247
  const cliPackageRoot = isBuiltPackage ? join6(distDir, "../../..") : join6(monorepoRoot, "packages/cli");
5250
5248
  const cliNodeModules = isBuiltPackage ? join6(cliPackageRoot, "node_modules") : monorepoRoot;
5251
5249
  const workspaceNodeModules = join6(workspace, "node_modules");
5252
- const constantsEntry = isBuiltPackage ? (
5253
- // dist/edge-play/src → dist/vendor/constants/index.ts
5254
- join6(embeddedEdgeSrc, "..", "..", "vendor", "constants", "index.ts")
5255
- ) : join6(monorepoRoot, "packages", "constants", "src", "index.ts");
5256
- console.log({
5250
+ const constantsEntry = isBuiltPackage ? join6(embeddedEdgeSrc, "..", "..", "constants", "src", "index.ts") : join6(monorepoRoot, "packages", "constants", "src", "index.ts");
5251
+ return {
5257
5252
  isBuiltPackage,
5258
- distDir,
5259
- constantsEntry,
5260
- embeddedEdgeSrc,
5261
5253
  edgePlaySrc,
5262
- cliPackageRoot,
5263
- cliNodeModules,
5264
- workspaceNodeModules
5265
- });
5266
- const result = await esbuild.build({
5254
+ constantsEntry,
5255
+ workspaceNodeModules,
5256
+ cliNodeModules
5257
+ };
5258
+ }
5259
+ function createEsbuildConfig(entryCode, paths, bundleConfig, customRoutesDir, options) {
5260
+ const workspace = getWorkspace();
5261
+ const { isBuiltPackage, edgePlaySrc, constantsEntry, workspaceNodeModules, cliNodeModules } = paths;
5262
+ return {
5263
+ // ──── Input Configuration ────
5267
5264
  stdin: {
5268
5265
  contents: entryCode,
5266
+ // Generated entry code with custom route imports
5269
5267
  resolveDir: edgePlaySrc,
5270
- // For relative imports like ./register-routes
5268
+ // Resolve relative imports from edge-play/src
5271
5269
  loader: "ts"
5270
+ // Treat input as TypeScript
5272
5271
  },
5272
+ // ──── Output Configuration ────
5273
5273
  bundle: true,
5274
+ // Bundle all dependencies into single file
5274
5275
  format: "esm",
5276
+ // Output ES modules (required for Cloudflare Workers)
5275
5277
  platform: "browser",
5278
+ // Workers use browser APIs, not Node.js
5276
5279
  target: "es2022",
5280
+ // Modern JavaScript for Workers runtime
5277
5281
  write: false,
5282
+ // Return code as string (don't write to disk)
5278
5283
  sourcemap: options.sourcemap ? "inline" : false,
5279
5284
  minify: options.minify || false,
5280
5285
  logLevel: "error",
5281
- nodePaths: [workspaceNodeModules, cliNodeModules, join6(cliPackageRoot, "..")],
5282
- // Check workspace first, then CLI package and project root
5283
- loader: {
5284
- ".ts": "ts"
5285
- },
5286
+ // Only show errors (suppress warnings)
5287
+ // ──── Module Resolution ────
5288
+ // Tell esbuild where to find node_modules for bare imports
5289
+ // In dev: Need both workspace and monorepo root (hoisted deps)
5290
+ // In published: Both point to same location (/user-project/node_modules)
5291
+ nodePaths: isBuiltPackage ? [workspaceNodeModules] : [workspaceNodeModules, cliNodeModules],
5292
+ // ──── Build-time Constants ────
5293
+ // Inject the Playcademy config as a global constant
5294
+ // Code can access it via: const config = PLAYCADEMY_CONFIG
5286
5295
  define: {
5287
5296
  PLAYCADEMY_CONFIG: JSON.stringify(bundleConfig)
5288
5297
  },
5298
+ // ──── Import Aliases ────
5289
5299
  alias: {
5290
- // Map workspace-only package to embedded constants for published CLI
5300
+ // ┌─ Workspace-only package resolution ─────────────────────────────┐
5301
+ // │ @playcademy/constants is a workspace package that users don't │
5302
+ // │ install. We embed it in dist/ and alias imports to point there. │
5303
+ // └─────────────────────────────────────────────────────────────────┘
5291
5304
  "@playcademy/constants": constantsEntry,
5292
- /**
5293
- * @game-api alias maps to the user's custom routes directory
5294
- *
5295
- * This allows custom route imports in the entry code:
5296
- * import * as customRoute0 from '@game-api/hello.ts'
5297
- *
5298
- * The alias resolves to the absolute path of the custom routes directory in the
5299
- * user's game project (configured via integrations.customRoutes.directory), enabling esbuild
5300
- * to bundle custom routes into the worker.
5301
- */
5305
+ // ┌─ User's custom routes ──────────────────────────────────────────┐
5306
+ // @game-api is a virtual module that maps to the user's API dir. │
5307
+ // │ Example: import * as route from '@game-api/hello.ts' │
5308
+ // Resolves to: /user-project/server/api/hello.ts │
5309
+ // └─────────────────────────────────────────────────────────────────┘
5302
5310
  "@game-api": join6(workspace, customRoutesDir),
5303
- /**
5304
- * Node.js module polyfills for Cloudflare Workers environment
5305
- *
5306
- * Cloudflare Workers don't have Node.js APIs (fs, path, os, etc.).
5307
- * These aliases redirect Node.js imports to a polyfill that throws helpful errors.
5308
- *
5309
- * This prevents bundling errors and provides clear runtime messages if
5310
- * user code accidentally imports Node.js modules that won't work in Workers.
5311
- */
5311
+ // ┌─ Node.js polyfills for Cloudflare Workers ──────────────────────┐
5312
+ // Workers don't have fs, path, os, etc. Redirect to polyfills │
5313
+ // │ that throw helpful errors if user code tries to use them.
5314
+ // └─────────────────────────────────────────────────────────────────┘
5312
5315
  fs: join6(edgePlaySrc, "polyfills.js"),
5313
5316
  "fs/promises": join6(edgePlaySrc, "polyfills.js"),
5314
5317
  path: join6(edgePlaySrc, "polyfills.js"),
5315
5318
  os: join6(edgePlaySrc, "polyfills.js"),
5316
5319
  process: join6(edgePlaySrc, "polyfills.js")
5317
5320
  },
5321
+ // ──── Build Plugins ────
5318
5322
  plugins: [textLoaderPlugin()],
5323
+ // Support Bun's 'with { type: "text" }' imports
5324
+ // ──── External Dependencies ────
5319
5325
  external: []
5320
- });
5326
+ // Bundle everything (no externals for Workers)
5327
+ };
5328
+ }
5329
+ async function bundleBackend(config, options = {}) {
5330
+ const esbuild = await import("esbuild");
5331
+ const { customRouteData, customRoutesDir } = await discoverCustomRoutes(config);
5332
+ const bundleConfig = {
5333
+ ...config,
5334
+ customRoutes: customRouteData
5335
+ };
5336
+ const entryCode = generateEntryCode(customRouteData, customRoutesDir);
5337
+ const paths = resolveEmbeddedSourcePaths();
5338
+ const buildConfig = createEsbuildConfig(
5339
+ entryCode,
5340
+ paths,
5341
+ bundleConfig,
5342
+ customRoutesDir,
5343
+ options
5344
+ );
5345
+ const result = await esbuild.build(buildConfig);
5321
5346
  if (!result.outputFiles?.[0]) {
5322
5347
  throw new Error("Backend bundling failed: no output");
5323
5348
  }
@@ -5616,7 +5641,8 @@ async function getSchemaInfo(previousSchemaSnapshot) {
5616
5641
  return null;
5617
5642
  }
5618
5643
  try {
5619
- const { generateSQLiteDrizzleJson, generateSQLiteMigration } = await import("drizzle-kit/api");
5644
+ const drizzleKitApi = await import("drizzle-kit/api");
5645
+ const { generateSQLiteDrizzleJson, generateSQLiteMigration } = drizzleKitApi;
5620
5646
  const schemaModule = await import(schemaPath);
5621
5647
  const currentSchema = schemaModule.default || schemaModule;
5622
5648
  const nextJson = await generateSQLiteDrizzleJson(currentSchema);
package/dist/utils.js CHANGED
@@ -1162,9 +1162,8 @@ async function transpileRoute(filePath) {
1162
1162
  }
1163
1163
 
1164
1164
  // src/lib/deploy/bundle.ts
1165
- var entryTemplate = entry_default;
1166
- async function bundleBackend(config, options = {}) {
1167
- const esbuild = await import("esbuild");
1165
+ var entryTemplate = entry_default.toString();
1166
+ async function discoverCustomRoutes(config) {
1168
1167
  const workspace = getWorkspace();
1169
1168
  const customRoutesConfig = config.integrations?.customRoutes;
1170
1169
  const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
@@ -1175,89 +1174,115 @@ async function bundleBackend(config, options = {}) {
1175
1174
  // Use relative path (e.g., 'server/api/test.ts'), not absolute
1176
1175
  methods: r.methods
1177
1176
  }));
1178
- const bundleConfig = {
1179
- ...config,
1180
- customRoutes: customRouteData
1181
- };
1182
- const entryCode = generateEntryCode(customRouteData, customRoutesDir);
1177
+ return { customRouteData, customRoutesDir };
1178
+ }
1179
+ function resolveEmbeddedSourcePaths() {
1180
+ const workspace = getWorkspace();
1183
1181
  const distDir = new URL(".", import.meta.url).pathname;
1184
1182
  const embeddedEdgeSrc = join2(distDir, "edge-play", "src");
1183
+ const isBuiltPackage = existsSync2(embeddedEdgeSrc);
1185
1184
  const monorepoRoot = getMonorepoRoot();
1186
1185
  const monorepoEdgeSrc = join2(monorepoRoot, "packages/edge-play/src");
1187
- const isBuiltPackage = existsSync2(embeddedEdgeSrc);
1188
1186
  const edgePlaySrc = isBuiltPackage ? embeddedEdgeSrc : monorepoEdgeSrc;
1189
1187
  const cliPackageRoot = isBuiltPackage ? join2(distDir, "../../..") : join2(monorepoRoot, "packages/cli");
1190
1188
  const cliNodeModules = isBuiltPackage ? join2(cliPackageRoot, "node_modules") : monorepoRoot;
1191
1189
  const workspaceNodeModules = join2(workspace, "node_modules");
1192
- const constantsEntry = isBuiltPackage ? (
1193
- // dist/edge-play/src → dist/vendor/constants/index.ts
1194
- join2(embeddedEdgeSrc, "..", "..", "vendor", "constants", "index.ts")
1195
- ) : join2(monorepoRoot, "packages", "constants", "src", "index.ts");
1196
- console.log({
1190
+ const constantsEntry = isBuiltPackage ? join2(embeddedEdgeSrc, "..", "..", "constants", "src", "index.ts") : join2(monorepoRoot, "packages", "constants", "src", "index.ts");
1191
+ return {
1197
1192
  isBuiltPackage,
1198
- distDir,
1199
- constantsEntry,
1200
- embeddedEdgeSrc,
1201
1193
  edgePlaySrc,
1202
- cliPackageRoot,
1203
- cliNodeModules,
1204
- workspaceNodeModules
1205
- });
1206
- const result = await esbuild.build({
1194
+ constantsEntry,
1195
+ workspaceNodeModules,
1196
+ cliNodeModules
1197
+ };
1198
+ }
1199
+ function createEsbuildConfig(entryCode, paths, bundleConfig, customRoutesDir, options) {
1200
+ const workspace = getWorkspace();
1201
+ const { isBuiltPackage, edgePlaySrc, constantsEntry, workspaceNodeModules, cliNodeModules } = paths;
1202
+ return {
1203
+ // ──── Input Configuration ────
1207
1204
  stdin: {
1208
1205
  contents: entryCode,
1206
+ // Generated entry code with custom route imports
1209
1207
  resolveDir: edgePlaySrc,
1210
- // For relative imports like ./register-routes
1208
+ // Resolve relative imports from edge-play/src
1211
1209
  loader: "ts"
1210
+ // Treat input as TypeScript
1212
1211
  },
1212
+ // ──── Output Configuration ────
1213
1213
  bundle: true,
1214
+ // Bundle all dependencies into single file
1214
1215
  format: "esm",
1216
+ // Output ES modules (required for Cloudflare Workers)
1215
1217
  platform: "browser",
1218
+ // Workers use browser APIs, not Node.js
1216
1219
  target: "es2022",
1220
+ // Modern JavaScript for Workers runtime
1217
1221
  write: false,
1222
+ // Return code as string (don't write to disk)
1218
1223
  sourcemap: options.sourcemap ? "inline" : false,
1219
1224
  minify: options.minify || false,
1220
1225
  logLevel: "error",
1221
- nodePaths: [workspaceNodeModules, cliNodeModules, join2(cliPackageRoot, "..")],
1222
- // Check workspace first, then CLI package and project root
1223
- loader: {
1224
- ".ts": "ts"
1225
- },
1226
+ // Only show errors (suppress warnings)
1227
+ // ──── Module Resolution ────
1228
+ // Tell esbuild where to find node_modules for bare imports
1229
+ // In dev: Need both workspace and monorepo root (hoisted deps)
1230
+ // In published: Both point to same location (/user-project/node_modules)
1231
+ nodePaths: isBuiltPackage ? [workspaceNodeModules] : [workspaceNodeModules, cliNodeModules],
1232
+ // ──── Build-time Constants ────
1233
+ // Inject the Playcademy config as a global constant
1234
+ // Code can access it via: const config = PLAYCADEMY_CONFIG
1226
1235
  define: {
1227
1236
  PLAYCADEMY_CONFIG: JSON.stringify(bundleConfig)
1228
1237
  },
1238
+ // ──── Import Aliases ────
1229
1239
  alias: {
1230
- // Map workspace-only package to embedded constants for published CLI
1240
+ // ┌─ Workspace-only package resolution ─────────────────────────────┐
1241
+ // │ @playcademy/constants is a workspace package that users don't │
1242
+ // │ install. We embed it in dist/ and alias imports to point there. │
1243
+ // └─────────────────────────────────────────────────────────────────┘
1231
1244
  "@playcademy/constants": constantsEntry,
1232
- /**
1233
- * @game-api alias maps to the user's custom routes directory
1234
- *
1235
- * This allows custom route imports in the entry code:
1236
- * import * as customRoute0 from '@game-api/hello.ts'
1237
- *
1238
- * The alias resolves to the absolute path of the custom routes directory in the
1239
- * user's game project (configured via integrations.customRoutes.directory), enabling esbuild
1240
- * to bundle custom routes into the worker.
1241
- */
1245
+ // ┌─ User's custom routes ──────────────────────────────────────────┐
1246
+ // @game-api is a virtual module that maps to the user's API dir. │
1247
+ // │ Example: import * as route from '@game-api/hello.ts' │
1248
+ // Resolves to: /user-project/server/api/hello.ts │
1249
+ // └─────────────────────────────────────────────────────────────────┘
1242
1250
  "@game-api": join2(workspace, customRoutesDir),
1243
- /**
1244
- * Node.js module polyfills for Cloudflare Workers environment
1245
- *
1246
- * Cloudflare Workers don't have Node.js APIs (fs, path, os, etc.).
1247
- * These aliases redirect Node.js imports to a polyfill that throws helpful errors.
1248
- *
1249
- * This prevents bundling errors and provides clear runtime messages if
1250
- * user code accidentally imports Node.js modules that won't work in Workers.
1251
- */
1251
+ // ┌─ Node.js polyfills for Cloudflare Workers ──────────────────────┐
1252
+ // Workers don't have fs, path, os, etc. Redirect to polyfills │
1253
+ // │ that throw helpful errors if user code tries to use them.
1254
+ // └─────────────────────────────────────────────────────────────────┘
1252
1255
  fs: join2(edgePlaySrc, "polyfills.js"),
1253
1256
  "fs/promises": join2(edgePlaySrc, "polyfills.js"),
1254
1257
  path: join2(edgePlaySrc, "polyfills.js"),
1255
1258
  os: join2(edgePlaySrc, "polyfills.js"),
1256
1259
  process: join2(edgePlaySrc, "polyfills.js")
1257
1260
  },
1261
+ // ──── Build Plugins ────
1258
1262
  plugins: [textLoaderPlugin()],
1263
+ // Support Bun's 'with { type: "text" }' imports
1264
+ // ──── External Dependencies ────
1259
1265
  external: []
1260
- });
1266
+ // Bundle everything (no externals for Workers)
1267
+ };
1268
+ }
1269
+ async function bundleBackend(config, options = {}) {
1270
+ const esbuild = await import("esbuild");
1271
+ const { customRouteData, customRoutesDir } = await discoverCustomRoutes(config);
1272
+ const bundleConfig = {
1273
+ ...config,
1274
+ customRoutes: customRouteData
1275
+ };
1276
+ const entryCode = generateEntryCode(customRouteData, customRoutesDir);
1277
+ const paths = resolveEmbeddedSourcePaths();
1278
+ const buildConfig = createEsbuildConfig(
1279
+ entryCode,
1280
+ paths,
1281
+ bundleConfig,
1282
+ customRoutesDir,
1283
+ options
1284
+ );
1285
+ const result = await esbuild.build(buildConfig);
1261
1286
  if (!result.outputFiles?.[0]) {
1262
1287
  throw new Error("Backend bundling failed: no output");
1263
1288
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playcademy",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
4
  "type": "module",
5
5
  "module": "./dist/index.js",
6
6
  "main": "./dist/index.js",