playcademy 0.14.7 → 0.14.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/edge-play/src/entry/middleware.ts +91 -22
- package/dist/edge-play/src/types.ts +12 -2
- package/dist/index.js +418 -227
- package/dist/utils.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
152
|
-
|
|
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
|
-
/**
|
|
41
|
-
|
|
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
|