playcademy 0.14.7 → 0.14.9

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.
@@ -10,8 +10,8 @@ import { PlaycademyClient, verifyGameToken } from '@playcademy/sdk/server'
10
10
 
11
11
  import { populateProcessEnv, reconstructSecrets } from './setup'
12
12
 
13
- import type { Hono } from 'hono'
14
- import type { HonoEnv } from '../types'
13
+ import type { Context, Hono } from 'hono'
14
+ import type { AssetsFetcher, HonoEnv } from '../types'
15
15
  import type { RuntimeConfig } from './types'
16
16
 
17
17
  /**
@@ -124,12 +124,97 @@ export function registerApiNotFoundHandler(app: Hono<HonoEnv>): void {
124
124
  })
125
125
  }
126
126
 
127
+ /**
128
+ * Detect if ASSETS binding is an R2 bucket or Workers Assets Fetcher
129
+ * Returns true for R2Bucket, false for AssetsFetcher
130
+ */
131
+ function isR2AssetsBinding(assets: AssetsFetcher | R2Bucket): assets is R2Bucket {
132
+ // R2Bucket has .get() method, Fetcher has .fetch() method
133
+ return 'get' in assets && typeof assets.get === 'function' && !('fetch' in assets)
134
+ }
135
+
136
+ /**
137
+ * Serve assets from Workers Assets binding (Fetcher)
138
+ * Default strategy for games with files <25MB
139
+ */
140
+ async function serveFromWorkersAssets(
141
+ c: Context<HonoEnv>,
142
+ assets: AssetsFetcher,
143
+ ): Promise<Response> {
144
+ // Try to fetch the requested asset
145
+ const response = await assets.fetch(c.req.raw)
146
+
147
+ // If found, return it
148
+ if (response.status !== 404) {
149
+ return response
150
+ }
151
+
152
+ // If not found and it's a file request (has extension), return 404
153
+ const path = new URL(c.req.url).pathname
154
+ if (path.includes('.')) {
155
+ return response
156
+ }
157
+
158
+ // Otherwise, fall back to index.html for SPA routing
159
+ const indexUrl = new URL(c.req.url)
160
+ indexUrl.pathname = '/index.html'
161
+
162
+ return await assets.fetch(new Request(indexUrl.toString()))
163
+ }
164
+
165
+ /**
166
+ * Serve assets from R2 bucket binding
167
+ * Fallback strategy for games with large files (≥25MB) like Godot WASM
168
+ */
169
+ async function serveFromR2(c: Context<HonoEnv>, bucket: R2Bucket): Promise<Response> {
170
+ const path = new URL(c.req.url).pathname
171
+ const key = path === '/' ? 'index.html' : path.slice(1)
172
+
173
+ // Try to fetch the requested asset
174
+ const object = await bucket.get(key)
175
+
176
+ if (object) {
177
+ return new Response(object.body, {
178
+ headers: {
179
+ 'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
180
+ 'Cache-Control':
181
+ key === 'index.html' ? 'no-cache' : 'public, max-age=31536000, immutable',
182
+ ETag: object.etag,
183
+ },
184
+ })
185
+ }
186
+
187
+ // If not found and it's a file request (has extension), return 404
188
+ if (path.includes('.')) {
189
+ return c.json({ error: 'Not Found', message: 'Asset not found', path }, 404)
190
+ }
191
+
192
+ // Otherwise, fall back to index.html for SPA routing
193
+ const indexObject = await bucket.get('index.html')
194
+
195
+ if (indexObject) {
196
+ return new Response(indexObject.body, {
197
+ headers: {
198
+ 'Content-Type': 'text/html',
199
+ 'Cache-Control': 'no-cache',
200
+ },
201
+ })
202
+ }
203
+
204
+ return c.json({ error: 'Not Found', message: 'Asset not found', path }, 404)
205
+ }
206
+
127
207
  /**
128
208
  * Register asset fallback handler
129
209
  *
130
- * Serves static assets from Workers Assets binding.
210
+ * Serves static assets from either Workers Assets or R2 bucket binding.
211
+ * Automatically detects which binding type is present and routes accordingly.
131
212
  * MUST be registered last as it's a catch-all for non-API routes.
132
213
  *
214
+ * Supports two deployment strategies:
215
+ * - Workers Assets (default): For games with files <25MB
216
+ * - R2 Bucket (fallback): For games with large files like Godot WASM
217
+ *
133
218
  * SPA Routing Support:
134
219
  * - First tries to fetch the requested path (e.g., /assets/main.js)
135
220
  * - If 404 and NOT an API route, falls back to /index.html
@@ -148,24 +233,8 @@ export function registerAssetFallback(app: Hono<HonoEnv>): void {
148
233
  )
149
234
  }
150
235
 
151
- // Try to fetch the requested asset
152
- const response = await c.env.ASSETS.fetch(c.req.raw)
153
-
154
- // If found, return it
155
- if (response.status !== 404) {
156
- return response
157
- }
158
-
159
- // If not found and it's a file request (has extension), return 404
160
- const path = new URL(c.req.url).pathname
161
- if (path.includes('.')) {
162
- return response
163
- }
164
-
165
- // Otherwise, fall back to index.html for predictable routing
166
- const indexUrl = new URL(c.req.url)
167
- indexUrl.pathname = '/index.html'
168
-
169
- return await c.env.ASSETS.fetch(new Request(indexUrl.toString()))
236
+ return isR2AssetsBinding(c.env.ASSETS)
237
+ ? await serveFromR2(c, c.env.ASSETS)
238
+ : await serveFromWorkersAssets(c, c.env.ASSETS)
170
239
  })
171
240
  }
@@ -10,6 +10,12 @@
10
10
  import type { PlaycademyClient, PlaycademyConfig } from '@playcademy/sdk/server'
11
11
  import type { RouteMetadata } from './entry/types'
12
12
 
13
+ /**
14
+ * Workers Assets Fetcher binding
15
+ * Standard Cloudflare binding for serving static assets
16
+ */
17
+ export type AssetsFetcher = { fetch(request: Request): Promise<Response> }
18
+
13
19
  /**
14
20
  * Enabled integrations from playcademy.config.js
15
21
  */
@@ -37,8 +43,12 @@ export interface ServerEnv {
37
43
  /** Game-specific secrets (optional) */
38
44
  secrets?: Record<string, string>
39
45
 
40
- /** Workers Assets binding for static files (Cloudflare-specific) */
41
- ASSETS?: { fetch(request: Request): Promise<Response> }
46
+ /**
47
+ * Frontend assets binding (Cloudflare-specific)
48
+ * - AssetsFetcher: Workers Assets binding (default for files <25MB)
49
+ * - R2Bucket: R2 bucket binding (fallback for large files like Godot WASM)
50
+ */
51
+ ASSETS?: AssetsFetcher | R2Bucket
42
52
 
43
53
  /** KV namespace binding (optional, Cloudflare-specific) */
44
54
  KV?: KVNamespace