@b9g/platform-cloudflare 0.1.9 → 0.1.11

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.
@@ -0,0 +1,344 @@
1
+ /// <reference types="./directories.d.ts" />
2
+ // src/directories.ts
3
+ import mime from "mime";
4
+ import { getEnv } from "./variables.js";
5
+ var R2FileSystemWritableFileStream = class extends WritableStream {
6
+ constructor(r2Bucket, key) {
7
+ const chunks = [];
8
+ super({
9
+ write: (chunk) => {
10
+ chunks.push(chunk);
11
+ return Promise.resolve();
12
+ },
13
+ close: async () => {
14
+ const totalLength = chunks.reduce(
15
+ (sum, chunk) => sum + chunk.length,
16
+ 0
17
+ );
18
+ const buffer = new Uint8Array(totalLength);
19
+ let offset = 0;
20
+ for (const chunk of chunks) {
21
+ buffer.set(chunk, offset);
22
+ offset += chunk.length;
23
+ }
24
+ await r2Bucket.put(key, buffer);
25
+ },
26
+ abort: async () => {
27
+ chunks.length = 0;
28
+ }
29
+ });
30
+ }
31
+ };
32
+ var R2FileSystemFileHandle = class _R2FileSystemFileHandle {
33
+ kind;
34
+ name;
35
+ #r2Bucket;
36
+ #key;
37
+ constructor(r2Bucket, key) {
38
+ this.kind = "file";
39
+ this.#r2Bucket = r2Bucket;
40
+ this.#key = key;
41
+ this.name = key.split("/").pop() || key;
42
+ }
43
+ async getFile() {
44
+ const r2Object = await this.#r2Bucket.get(this.#key);
45
+ if (!r2Object) {
46
+ throw new DOMException("File not found", "NotFoundError");
47
+ }
48
+ const arrayBuffer = await r2Object.arrayBuffer();
49
+ return new File([arrayBuffer], this.name, {
50
+ lastModified: r2Object.uploaded.getTime(),
51
+ type: r2Object.httpMetadata?.contentType || this.#getMimeType(this.#key)
52
+ });
53
+ }
54
+ async createWritable() {
55
+ return new R2FileSystemWritableFileStream(
56
+ this.#r2Bucket,
57
+ this.#key
58
+ );
59
+ }
60
+ async createSyncAccessHandle() {
61
+ throw new DOMException(
62
+ "Synchronous access handles are not supported for R2 storage",
63
+ "InvalidStateError"
64
+ );
65
+ }
66
+ async isSameEntry(other) {
67
+ if (other.kind !== "file") return false;
68
+ if (!(other instanceof _R2FileSystemFileHandle)) return false;
69
+ return this.#key === other.#key;
70
+ }
71
+ async queryPermission() {
72
+ return "granted";
73
+ }
74
+ async requestPermission() {
75
+ return "granted";
76
+ }
77
+ #getMimeType(key) {
78
+ return mime.getType(key) || "application/octet-stream";
79
+ }
80
+ };
81
+ var R2FileSystemDirectoryHandle = class _R2FileSystemDirectoryHandle {
82
+ kind;
83
+ name;
84
+ #r2Bucket;
85
+ #prefix;
86
+ constructor(r2Bucket, prefix) {
87
+ this.kind = "directory";
88
+ this.#r2Bucket = r2Bucket;
89
+ this.#prefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
90
+ this.name = this.#prefix.split("/").pop() || "root";
91
+ }
92
+ async getFileHandle(name, options) {
93
+ const key = this.#prefix ? `${this.#prefix}/${name}` : name;
94
+ const exists = await this.#r2Bucket.head(key);
95
+ if (!exists && options?.create) {
96
+ await this.#r2Bucket.put(key, new Uint8Array(0));
97
+ } else if (!exists) {
98
+ throw new DOMException("File not found", "NotFoundError");
99
+ }
100
+ return new R2FileSystemFileHandle(this.#r2Bucket, key);
101
+ }
102
+ async getDirectoryHandle(name, options) {
103
+ const newPrefix = this.#prefix ? `${this.#prefix}/${name}` : name;
104
+ if (options?.create) {
105
+ const markerKey = `${newPrefix}/.shovel_directory_marker`;
106
+ const exists = await this.#r2Bucket.head(markerKey);
107
+ if (!exists) {
108
+ await this.#r2Bucket.put(markerKey, new Uint8Array(0));
109
+ }
110
+ }
111
+ return new _R2FileSystemDirectoryHandle(this.#r2Bucket, newPrefix);
112
+ }
113
+ async removeEntry(name, options) {
114
+ const key = this.#prefix ? `${this.#prefix}/${name}` : name;
115
+ const fileExists = await this.#r2Bucket.head(key);
116
+ if (fileExists) {
117
+ await this.#r2Bucket.delete(key);
118
+ return;
119
+ }
120
+ if (options?.recursive) {
121
+ const dirPrefix = `${key}/`;
122
+ const listed = await this.#r2Bucket.list({ prefix: dirPrefix });
123
+ const deletePromises = listed.objects.map(
124
+ (object) => this.#r2Bucket.delete(object.key)
125
+ );
126
+ await Promise.all(deletePromises);
127
+ const markerKey = `${key}/.shovel_directory_marker`;
128
+ const markerExists = await this.#r2Bucket.head(markerKey);
129
+ if (markerExists) {
130
+ await this.#r2Bucket.delete(markerKey);
131
+ }
132
+ } else {
133
+ throw new DOMException(
134
+ "Directory is not empty",
135
+ "InvalidModificationError"
136
+ );
137
+ }
138
+ }
139
+ async resolve(_possibleDescendant) {
140
+ return null;
141
+ }
142
+ async *entries() {
143
+ const listPrefix = this.#prefix ? `${this.#prefix}/` : "";
144
+ try {
145
+ const result = await this.#r2Bucket.list({
146
+ prefix: listPrefix,
147
+ delimiter: "/"
148
+ });
149
+ for (const object of result.objects) {
150
+ if (object.key !== listPrefix) {
151
+ const name = object.key.substring(listPrefix.length);
152
+ if (!name.includes("/") && !name.endsWith(".shovel_directory_marker")) {
153
+ yield [
154
+ name,
155
+ new R2FileSystemFileHandle(this.#r2Bucket, object.key)
156
+ ];
157
+ }
158
+ }
159
+ }
160
+ for (const prefix of result.delimitedPrefixes) {
161
+ const name = prefix.substring(listPrefix.length).replace(/\/$/, "");
162
+ if (name) {
163
+ yield [
164
+ name,
165
+ new _R2FileSystemDirectoryHandle(
166
+ this.#r2Bucket,
167
+ prefix.replace(/\/$/, "")
168
+ )
169
+ ];
170
+ }
171
+ }
172
+ } catch (error) {
173
+ throw new DOMException("Directory not found", "NotFoundError");
174
+ }
175
+ }
176
+ async *keys() {
177
+ for await (const [name] of this.entries()) {
178
+ yield name;
179
+ }
180
+ }
181
+ async *values() {
182
+ for await (const [, handle] of this.entries()) {
183
+ yield handle;
184
+ }
185
+ }
186
+ async isSameEntry(other) {
187
+ if (other.kind !== "directory") return false;
188
+ if (!(other instanceof _R2FileSystemDirectoryHandle)) return false;
189
+ return this.#prefix === other.#prefix;
190
+ }
191
+ async queryPermission() {
192
+ return "granted";
193
+ }
194
+ async requestPermission() {
195
+ return "granted";
196
+ }
197
+ };
198
+ var CFAssetsFileHandle = class _CFAssetsFileHandle {
199
+ kind;
200
+ name;
201
+ #assets;
202
+ #path;
203
+ constructor(assets, path, name) {
204
+ this.kind = "file";
205
+ this.#assets = assets;
206
+ this.#path = path;
207
+ this.name = name;
208
+ }
209
+ async getFile() {
210
+ const response = await this.#assets.fetch(
211
+ new Request("https://assets" + this.#path)
212
+ );
213
+ if (!response.ok) {
214
+ throw new DOMException(
215
+ `A requested file or directory could not be found: ${this.name}`,
216
+ "NotFoundError"
217
+ );
218
+ }
219
+ const blob = await response.blob();
220
+ const contentType = response.headers.get("content-type") || "application/octet-stream";
221
+ return new File([blob], this.name, { type: contentType });
222
+ }
223
+ async createWritable(_options) {
224
+ throw new DOMException("Assets are read-only", "NotAllowedError");
225
+ }
226
+ async createSyncAccessHandle() {
227
+ throw new DOMException("Sync access not supported", "NotSupportedError");
228
+ }
229
+ isSameEntry(other) {
230
+ return Promise.resolve(
231
+ other instanceof _CFAssetsFileHandle && other.#path === this.#path
232
+ );
233
+ }
234
+ };
235
+ var CFAssetsDirectoryHandle = class _CFAssetsDirectoryHandle {
236
+ kind;
237
+ name;
238
+ #assets;
239
+ #basePath;
240
+ constructor(assets, basePath = "/") {
241
+ this.kind = "directory";
242
+ this.#assets = assets;
243
+ this.#basePath = basePath.endsWith("/") ? basePath : basePath + "/";
244
+ this.name = basePath.split("/").filter(Boolean).pop() || "assets";
245
+ }
246
+ async getFileHandle(name, _options) {
247
+ const path = this.#basePath + name;
248
+ const response = await this.#assets.fetch(
249
+ new Request("https://assets" + path)
250
+ );
251
+ if (!response.ok) {
252
+ throw new DOMException(
253
+ `A requested file or directory could not be found: ${name}`,
254
+ "NotFoundError"
255
+ );
256
+ }
257
+ return new CFAssetsFileHandle(this.#assets, path, name);
258
+ }
259
+ async getDirectoryHandle(name, _options) {
260
+ return new _CFAssetsDirectoryHandle(this.#assets, this.#basePath + name);
261
+ }
262
+ async removeEntry(_name, _options) {
263
+ throw new DOMException("Assets directory is read-only", "NotAllowedError");
264
+ }
265
+ async resolve(_possibleDescendant) {
266
+ return null;
267
+ }
268
+ // eslint-disable-next-line require-yield
269
+ async *entries() {
270
+ throw new DOMException(
271
+ "Directory listing not supported for ASSETS binding. Use an asset manifest for enumeration.",
272
+ "NotSupportedError"
273
+ );
274
+ }
275
+ // eslint-disable-next-line require-yield
276
+ async *keys() {
277
+ throw new DOMException(
278
+ "Directory listing not supported for ASSETS binding",
279
+ "NotSupportedError"
280
+ );
281
+ }
282
+ // eslint-disable-next-line require-yield
283
+ async *values() {
284
+ throw new DOMException(
285
+ "Directory listing not supported for ASSETS binding",
286
+ "NotSupportedError"
287
+ );
288
+ }
289
+ [Symbol.asyncIterator]() {
290
+ return this.entries();
291
+ }
292
+ isSameEntry(other) {
293
+ return Promise.resolve(
294
+ other instanceof _CFAssetsDirectoryHandle && other.#basePath === this.#basePath
295
+ );
296
+ }
297
+ };
298
+ var CloudflareR2Directory = class extends R2FileSystemDirectoryHandle {
299
+ constructor(name, options = {}) {
300
+ const env = getEnv();
301
+ const bindingName = options.binding || `${name.toUpperCase()}_R2`;
302
+ const r2Bucket = env[bindingName];
303
+ if (!r2Bucket) {
304
+ throw new Error(
305
+ `R2 bucket binding "${bindingName}" not found. Configure in wrangler.toml:
306
+
307
+ [[r2_buckets]]
308
+ binding = "${bindingName}"
309
+ bucket_name = "your-bucket-name"`
310
+ );
311
+ }
312
+ const prefix = options.path ?? "";
313
+ const normalizedPrefix = prefix.startsWith("/") ? prefix.slice(1) : prefix;
314
+ super(r2Bucket, normalizedPrefix);
315
+ }
316
+ };
317
+ var CloudflareAssetsDirectory = class extends CFAssetsDirectoryHandle {
318
+ constructor(_name, options = {}) {
319
+ const env = getEnv();
320
+ const assets = env.ASSETS;
321
+ if (!assets) {
322
+ throw new Error(
323
+ `ASSETS binding not found. Configure in wrangler.toml:
324
+
325
+ [assets]
326
+ directory = "./public"`
327
+ );
328
+ }
329
+ const basePath = options.path ?? "/";
330
+ const normalizedBase = basePath === "/" ? "/" : basePath.startsWith("/") ? basePath : `/${basePath}`;
331
+ super(assets, normalizedBase);
332
+ }
333
+ };
334
+ var directories_default = CloudflareR2Directory;
335
+ export {
336
+ CFAssetsDirectoryHandle,
337
+ CFAssetsFileHandle,
338
+ CloudflareAssetsDirectory,
339
+ CloudflareR2Directory,
340
+ R2FileSystemDirectoryHandle,
341
+ R2FileSystemFileHandle,
342
+ R2FileSystemWritableFileStream,
343
+ directories_default as default
344
+ };
package/src/index.d.ts CHANGED
@@ -2,9 +2,17 @@
2
2
  * @b9g/platform-cloudflare - Cloudflare Workers platform adapter for Shovel
3
3
  *
4
4
  * Provides ServiceWorker-native deployment for Cloudflare Workers with KV/R2/D1 integration.
5
+ *
6
+ * Architecture:
7
+ * - Uses ServiceWorkerGlobals from @b9g/platform for full feature parity with Node/Bun
8
+ * - AsyncContext provides per-request access to Cloudflare's env/ctx
9
+ * - Directories use R2 via lazy factory (accessed when directories.open() is called)
10
+ * - Caches use Cloudflare's native Cache API
5
11
  */
6
- import { BasePlatform, PlatformConfig, Handler, Server, ServerOptions, ServiceWorkerOptions, ServiceWorkerInstance } from "@b9g/platform";
12
+ import { BasePlatform, PlatformConfig, type PlatformDefaults, Handler, Server, ServerOptions, ServiceWorkerOptions, ServiceWorkerInstance, EntryWrapperOptions, PlatformESBuildConfig, type LoggerStorage } from "@b9g/platform";
13
+ import { type ShovelConfig } from "@b9g/platform/runtime";
7
14
  import { CustomCacheStorage } from "@b9g/cache";
15
+ import type { DirectoryStorage } from "@b9g/filesystem";
8
16
  export type { Platform, Handler, Server, ServerOptions, ServiceWorkerOptions, ServiceWorkerInstance, } from "@b9g/platform";
9
17
  export interface CloudflarePlatformOptions extends PlatformConfig {
10
18
  /** Cloudflare Workers environment (production, preview, dev) */
@@ -13,6 +21,8 @@ export interface CloudflarePlatformOptions extends PlatformConfig {
13
21
  assetsDirectory?: string;
14
22
  /** Working directory for config file resolution */
15
23
  cwd?: string;
24
+ /** Shovel configuration (caches, directories, etc.) */
25
+ config?: ShovelConfig;
16
26
  }
17
27
  /**
18
28
  * Cloudflare Workers platform implementation
@@ -22,56 +32,53 @@ export declare class CloudflarePlatform extends BasePlatform {
22
32
  readonly name: string;
23
33
  constructor(options?: CloudflarePlatformOptions);
24
34
  /**
25
- * Create cache storage
26
- * Uses config from shovel.json with memory cache default.
27
- *
28
- * Note: This is for the platform/test runner context. Inside actual
29
- * Cloudflare Workers, native caches are available via globalThis.caches
30
- * (captured by the banner as globalThis.__cloudflareCaches).
35
+ * Create cache storage using config from shovel.json
36
+ * Default: Cloudflare's native Cache API
37
+ * Merges with runtime defaults (actual class references) for fallback behavior
31
38
  */
32
39
  createCaches(): Promise<CustomCacheStorage>;
33
40
  /**
34
- * Create "server" for Cloudflare Workers (which is really just the handler)
41
+ * Create directory storage for Cloudflare Workers
42
+ * Directories must be configured via shovel.json (no platform defaults)
43
+ */
44
+ createDirectories(): Promise<DirectoryStorage>;
45
+ /**
46
+ * Create logger storage for Cloudflare Workers
47
+ */
48
+ createLoggers(): Promise<LoggerStorage>;
49
+ /**
50
+ * Create "server" for Cloudflare Workers (stub for Platform interface)
35
51
  */
36
52
  createServer(handler: Handler, _options?: ServerOptions): Server;
37
53
  /**
38
- * Load ServiceWorker-style entrypoint using miniflare (workerd)
39
- *
40
- * Note: In production Cloudflare Workers, the banner/footer wrapper code
41
- * handles request dispatch directly - loadServiceWorker is only used for
42
- * local development with miniflare.
54
+ * Load ServiceWorker using miniflare (workerd) for dev mode
43
55
  */
44
56
  loadServiceWorker(entrypoint: string, _options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
57
+ dispose(): Promise<void>;
45
58
  /**
46
- * Dispose of platform resources
59
+ * Get virtual entry wrapper for Cloudflare Workers
60
+ *
61
+ * Wraps user code with:
62
+ * 1. Config import (shovel:config virtual module)
63
+ * 2. Runtime initialization (ServiceWorkerGlobals)
64
+ * 3. User code import (registers fetch handlers)
65
+ * 4. ES module export for Cloudflare Workers format
66
+ *
67
+ * Note: Unlike Node/Bun, Cloudflare bundles user code inline, so the
68
+ * entryPath is embedded directly in the wrapper.
47
69
  */
48
- dispose(): Promise<void>;
70
+ getEntryWrapper(entryPath: string, _options?: EntryWrapperOptions): string;
71
+ /**
72
+ * Get Cloudflare-specific esbuild configuration
73
+ *
74
+ * Note: Cloudflare Workers natively support import.meta.env, so no define alias
75
+ * is needed. The nodejs_compat flag enables node:* built-in modules at runtime,
76
+ * so we externalize them during bundling.
77
+ */
78
+ getESBuildConfig(): PlatformESBuildConfig;
79
+ /**
80
+ * Get Cloudflare-specific defaults for config generation
81
+ */
82
+ getDefaults(): PlatformDefaults;
49
83
  }
50
- /**
51
- * Create platform options from Wrangler environment
52
- */
53
- export declare function createOptionsFromEnv(env: any): CloudflarePlatformOptions;
54
- /**
55
- * Generate wrangler.toml configuration for a Shovel app from CLI flags
56
- */
57
- export declare function generateWranglerConfig(options: {
58
- name: string;
59
- entrypoint: string;
60
- cacheAdapter?: string;
61
- filesystemAdapter?: string;
62
- kvNamespaces?: string[];
63
- r2Buckets?: string[];
64
- d1Databases?: string[];
65
- }): string;
66
- /**
67
- * Generate banner code for ServiceWorker → ES Module conversion
68
- */
69
- export declare const cloudflareWorkerBanner = "// Cloudflare Worker ES Module wrapper\nlet serviceWorkerGlobals = null;\n\n// Capture native Cloudflare caches before any framework code runs\nconst nativeCaches = globalThis.caches;\n\n// Set up ServiceWorker environment\nif (typeof globalThis.self === 'undefined') {\n\tglobalThis.self = globalThis;\n}\n\n// Store native caches for access via globalThis.__cloudflareCaches\nglobalThis.__cloudflareCaches = nativeCaches;\n\n// Capture fetch event handlers\nconst fetchHandlers = [];\nconst originalAddEventListener = globalThis.addEventListener;\nglobalThis.addEventListener = function(type, handler, options) {\n\tif (type === 'fetch') {\n\t\tfetchHandlers.push(handler);\n\t} else {\n\t\toriginalAddEventListener?.call(this, type, handler, options);\n\t}\n};\n\n// Create a promise-based FetchEvent that can be awaited\nclass FetchEvent {\n\tconstructor(type, init) {\n\t\tthis.type = type;\n\t\tthis.request = init.request;\n\t\tthis._response = null;\n\t\tthis._responsePromise = new Promise((resolve) => {\n\t\t\tthis._resolveResponse = resolve;\n\t\t});\n\t}\n\t\n\trespondWith(response) {\n\t\tthis._response = response;\n\t\tthis._resolveResponse(response);\n\t}\n\t\n\tasync waitUntil(promise) {\n\t\tawait promise;\n\t}\n}";
70
- /**
71
- * Generate footer code for ServiceWorker → ES Module conversion
72
- */
73
- export declare const cloudflareWorkerFooter = "\n// Export ES Module for Cloudflare Workers\nexport default {\n\tasync fetch(request, env, ctx) {\n\t\ttry {\n\t\t\t// Set up ServiceWorker-like dirs API for bundled deployment\n\t\t\tif (!globalThis.self.dirs) {\n\t\t\t\t// For bundled deployment, assets are served via static middleware\n\t\t\t\t// not through the dirs API\n\t\t\t\tglobalThis.self.dirs = {\n\t\t\t\t\tasync open(directoryName) {\n\t\t\t\t\t\tif (directoryName === 'assets') {\n\t\t\t\t\t\t\t// Return a minimal interface that indicates no files available\n\t\t\t\t\t\t\t// The assets middleware will fall back to dev mode behavior\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tasync getFileHandle(fileName) {\n\t\t\t\t\t\t\t\t\tthrow new Error(`NotFoundError: ${fileName} not found in bundled assets`);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow new Error(`Directory ${directoryName} not available in bundled deployment`);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\t\t\t\n\t\t\t// Set up caches API\n\t\t\tif (!globalThis.self.caches) {\n\t\t\t\tglobalThis.self.caches = globalThis.caches;\n\t\t\t}\n\t\t\t\n\t\t\t// Ensure request.url is a string\n\t\t\tif (typeof request.url !== 'string') {\n\t\t\t\treturn new Response('Invalid request URL: ' + typeof request.url, { status: 500 });\n\t\t\t}\n\t\t\t\n\t\t\t// Create proper FetchEvent-like object\n\t\t\tlet responseReceived = null;\n\t\t\tconst event = { \n\t\t\t\trequest, \n\t\t\t\trespondWith: (response) => { responseReceived = response; }\n\t\t\t};\n\t\t\t\n\t\t\t// Helper for error responses\n\t\t\tconst createErrorResponse = (err) => {\n\t\t\t\tconst isDev = typeof import.meta !== \"undefined\" && import.meta.env?.MODE !== \"production\";\n\t\t\t\tif (isDev) {\n\t\t\t\t\tconst escapeHtml = (str) => str.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n\t\t\t\t\treturn new Response(`<!DOCTYPE html>\n<html>\n<head>\n <title>500 Internal Server Error</title>\n <style>\n body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }\n h1 { color: #c00; }\n .message { font-size: 1.2em; color: #333; }\n pre { background: #f5f5f5; padding: 1rem; overflow-x: auto; border-radius: 4px; }\n </style>\n</head>\n<body>\n <h1>500 Internal Server Error</h1>\n <p class=\"message\">${escapeHtml(err.message)}</p>\n <pre>${escapeHtml(err.stack || \"No stack trace available\")}</pre>\n</body>\n</html>`, { status: 500, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } });\n\t\t\t\t} else {\n\t\t\t\t\treturn new Response(\"Internal Server Error\", { status: 500, headers: { \"Content-Type\": \"text/plain\" } });\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Dispatch to ServiceWorker fetch handlers\n\t\t\tfor (const handler of fetchHandlers) {\n\t\t\t\ttry {\n\t\t\t\t\tawait handler(event);\n\t\t\t\t\tif (responseReceived) {\n\t\t\t\t\t\treturn responseReceived;\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\"Handler error:\", error);\n\t\t\t\t\treturn createErrorResponse(error);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn new Response('No ServiceWorker handler', { status: 404 });\n\t\t} catch (topLevelError) {\n\t\t\tconsole.error(\"Top-level error:\", topLevelError);\n\t\t\tconst isDev = typeof import.meta !== \"undefined\" && import.meta.env?.MODE !== \"production\";\n\t\t\tif (isDev) {\n\t\t\t\tconst escapeHtml = (str) => String(str).replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n\t\t\t\treturn new Response(`<!DOCTYPE html>\n<html>\n<head>\n <title>500 Internal Server Error</title>\n <style>\n body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }\n h1 { color: #c00; }\n .message { font-size: 1.2em; color: #333; }\n pre { background: #f5f5f5; padding: 1rem; overflow-x: auto; border-radius: 4px; }\n </style>\n</head>\n<body>\n <h1>500 Internal Server Error</h1>\n <p class=\"message\">${escapeHtml(topLevelError.message)}</p>\n <pre>${escapeHtml(topLevelError.stack || \"No stack trace available\")}</pre>\n</body>\n</html>`, { status: 500, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } });\n\t\t\t} else {\n\t\t\t\treturn new Response(\"Internal Server Error\", { status: 500, headers: { \"Content-Type\": \"text/plain\" } });\n\t\t\t}\n\t\t}\n\t}\n};";
74
- /**
75
- * Default export for easy importing
76
- */
77
84
  export default CloudflarePlatform;