@b9g/platform-cloudflare 0.1.8 → 0.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/platform-cloudflare",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Cloudflare Workers platform adapter for Shovel - already ServiceWorker-based!",
5
5
  "keywords": [
6
6
  "shovel",
@@ -11,11 +11,11 @@
11
11
  "serviceworker"
12
12
  ],
13
13
  "dependencies": {
14
- "@b9g/assets": "^0.1.14",
15
- "@b9g/cache": "^0.1.4",
16
- "@b9g/platform": "^0.1.10",
17
- "@cloudflare/workers-types": "^4.20241218.0",
14
+ "@b9g/assets": "^0.1.15",
15
+ "@b9g/cache": "^0.1.5",
16
+ "@b9g/platform": "^0.1.12",
18
17
  "@logtape/logtape": "^1.2.0",
18
+ "mime": "^4.0.4",
19
19
  "miniflare": "^4.20251118.1"
20
20
  },
21
21
  "devDependencies": {
@@ -46,6 +46,22 @@
46
46
  "./filesystem-assets.js": {
47
47
  "types": "./src/filesystem-assets.d.ts",
48
48
  "import": "./src/filesystem-assets.js"
49
+ },
50
+ "./cloudflare-runtime": {
51
+ "types": "./src/cloudflare-runtime.d.ts",
52
+ "import": "./src/cloudflare-runtime.js"
53
+ },
54
+ "./cloudflare-runtime.js": {
55
+ "types": "./src/cloudflare-runtime.d.ts",
56
+ "import": "./src/cloudflare-runtime.js"
57
+ },
58
+ "./filesystem-r2": {
59
+ "types": "./src/filesystem-r2.d.ts",
60
+ "import": "./src/filesystem-r2.js"
61
+ },
62
+ "./filesystem-r2.js": {
63
+ "types": "./src/filesystem-r2.d.ts",
64
+ "import": "./src/filesystem-r2.js"
49
65
  }
50
66
  }
51
67
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Cloudflare Worker Runtime - Browser-safe ServiceWorkerGlobals setup
3
+ *
4
+ * This module is BROWSER-SAFE and can be bundled into Cloudflare Workers.
5
+ * It only imports from browser-compatible modules:
6
+ * - @b9g/platform/runtime (no fs/path)
7
+ * - @b9g/filesystem (no fs/path in the index)
8
+ * - @b9g/async-context (browser-safe)
9
+ *
10
+ * DO NOT import from @b9g/platform (the index) - it pulls in Node-only code.
11
+ */
12
+ import { ShovelServiceWorkerRegistration } from "@b9g/platform/runtime";
13
+ /**
14
+ * Cloudflare's ExecutionContext - passed to each request handler
15
+ * Used for ctx.waitUntil() to extend request lifetime
16
+ */
17
+ export interface ExecutionContext {
18
+ waitUntil(promise: Promise<unknown>): void;
19
+ passThroughOnException(): void;
20
+ }
21
+ /**
22
+ * Get the current request's Cloudflare env object
23
+ * Contains all bindings: KV namespaces, R2 buckets, D1 databases, etc.
24
+ */
25
+ export declare function getEnv<T = Record<string, unknown>>(): T | undefined;
26
+ /**
27
+ * Get the current request's Cloudflare ExecutionContext
28
+ * Used for ctx.waitUntil() and other lifecycle methods
29
+ */
30
+ export declare function getCtx(): ExecutionContext | undefined;
31
+ /**
32
+ * Initialize the Cloudflare runtime with ServiceWorkerGlobals
33
+ * Called once when the worker module loads (before user code runs)
34
+ */
35
+ export declare function initializeRuntime(): ShovelServiceWorkerRegistration;
36
+ /**
37
+ * Create the ES module fetch handler for Cloudflare Workers
38
+ */
39
+ export declare function createFetchHandler(registration: ShovelServiceWorkerRegistration): (request: Request, env: unknown, ctx: ExecutionContext) => Promise<Response>;
@@ -0,0 +1,102 @@
1
+ /// <reference types="./cloudflare-runtime.d.ts" />
2
+ // src/cloudflare-runtime.ts
3
+ import {
4
+ ServiceWorkerGlobals,
5
+ ShovelServiceWorkerRegistration,
6
+ CustomLoggerStorage
7
+ } from "@b9g/platform/runtime";
8
+ import { CustomDirectoryStorage } from "@b9g/filesystem";
9
+ import { AsyncContext } from "@b9g/async-context";
10
+ import { getLogger } from "@logtape/logtape";
11
+ import { R2FileSystemDirectoryHandle } from "./filesystem-r2.js";
12
+ var envStorage = new AsyncContext.Variable();
13
+ var ctxStorage = new AsyncContext.Variable();
14
+ function getEnv() {
15
+ return envStorage.get();
16
+ }
17
+ function getCtx() {
18
+ return ctxStorage.get();
19
+ }
20
+ var _registration = null;
21
+ var _globals = null;
22
+ function initializeRuntime() {
23
+ if (_registration) {
24
+ return _registration;
25
+ }
26
+ _registration = new ShovelServiceWorkerRegistration();
27
+ const directories = new CustomDirectoryStorage(
28
+ createCloudflareR2DirectoryFactory()
29
+ );
30
+ _globals = new ServiceWorkerGlobals({
31
+ registration: _registration,
32
+ caches: globalThis.caches,
33
+ // Cloudflare's native Cache API
34
+ directories,
35
+ loggers: new CustomLoggerStorage((...cats) => getLogger(cats))
36
+ });
37
+ _globals.install();
38
+ return _registration;
39
+ }
40
+ function createFetchHandler(registration) {
41
+ return async (request, env, ctx) => {
42
+ return envStorage.run(
43
+ env,
44
+ () => ctxStorage.run(ctx, async () => {
45
+ try {
46
+ return await registration.handleRequest(request);
47
+ } catch (error) {
48
+ console.error("ServiceWorker error:", error);
49
+ const err = error instanceof Error ? error : new Error(String(error));
50
+ const isDev = typeof import.meta !== "undefined" && import.meta.env?.MODE !== "production";
51
+ if (isDev) {
52
+ return new Response(
53
+ `<!DOCTYPE html>
54
+ <html>
55
+ <head><title>500 Internal Server Error</title>
56
+ <style>body{font-family:system-ui;padding:2rem;max-width:800px;margin:0 auto}h1{color:#c00}pre{background:#f5f5f5;padding:1rem;overflow-x:auto}</style>
57
+ </head>
58
+ <body>
59
+ <h1>500 Internal Server Error</h1>
60
+ <p>${escapeHtml(err.message)}</p>
61
+ <pre>${escapeHtml(err.stack || "No stack trace")}</pre>
62
+ </body></html>`,
63
+ { status: 500, headers: { "Content-Type": "text/html" } }
64
+ );
65
+ }
66
+ return new Response("Internal Server Error", { status: 500 });
67
+ }
68
+ })
69
+ );
70
+ };
71
+ }
72
+ function escapeHtml(str) {
73
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
74
+ }
75
+ function createCloudflareR2DirectoryFactory() {
76
+ return async (name) => {
77
+ const env = getEnv();
78
+ if (!env) {
79
+ throw new Error(
80
+ `Cannot access directory "${name}": Cloudflare env not available. Are you accessing directories outside of a request context?`
81
+ );
82
+ }
83
+ const bindingName = `${name.toUpperCase()}_R2`;
84
+ const r2Bucket = env[bindingName];
85
+ if (!r2Bucket) {
86
+ throw new Error(
87
+ `R2 bucket binding "${bindingName}" not found. Configure in wrangler.toml:
88
+
89
+ [[r2_buckets]]
90
+ binding = "${bindingName}"
91
+ bucket_name = "your-bucket-name"`
92
+ );
93
+ }
94
+ return new R2FileSystemDirectoryHandle(r2Bucket, "");
95
+ };
96
+ }
97
+ export {
98
+ createFetchHandler,
99
+ getCtx,
100
+ getEnv,
101
+ initializeRuntime
102
+ };
package/src/env.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ // Vite-style import.meta.env declaration
2
+ interface ImportMetaEnv {
3
+ MODE?: string;
4
+ [key: string]: string | undefined;
5
+ }
6
+
7
+ interface ImportMeta {
8
+ readonly env?: ImportMetaEnv;
9
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Cloudflare R2 implementation of File System Access API
3
+ *
4
+ * Implements FileSystemDirectoryHandle and FileSystemFileHandle using Cloudflare R2 bindings
5
+ * to provide R2 cloud storage with File System Access API compatibility.
6
+ */
7
+ import type { FileSystemConfig } from "@b9g/filesystem";
8
+ /** R2 object metadata */
9
+ export interface R2Object {
10
+ key: string;
11
+ uploaded: Date;
12
+ httpMetadata?: {
13
+ contentType?: string;
14
+ };
15
+ arrayBuffer(): Promise<ArrayBuffer>;
16
+ }
17
+ /** R2 list result */
18
+ export interface R2Objects {
19
+ objects: Array<{
20
+ key: string;
21
+ }>;
22
+ delimitedPrefixes: string[];
23
+ }
24
+ /** R2 bucket interface */
25
+ export interface R2Bucket {
26
+ get(key: string): Promise<R2Object | null>;
27
+ head(key: string): Promise<R2Object | null>;
28
+ put(key: string, value: ArrayBuffer | Uint8Array): Promise<R2Object>;
29
+ delete(key: string): Promise<void>;
30
+ list(options?: {
31
+ prefix?: string;
32
+ delimiter?: string;
33
+ }): Promise<R2Objects>;
34
+ }
35
+ /**
36
+ * Cloudflare R2 implementation of FileSystemWritableFileStream
37
+ */
38
+ export declare class R2FileSystemWritableFileStream extends WritableStream<Uint8Array> {
39
+ #private;
40
+ constructor(r2Bucket: R2Bucket, key: string);
41
+ }
42
+ /**
43
+ * Cloudflare R2 implementation of FileSystemFileHandle
44
+ */
45
+ export declare class R2FileSystemFileHandle implements FileSystemFileHandle {
46
+ #private;
47
+ readonly kind: "file";
48
+ readonly name: string;
49
+ constructor(r2Bucket: R2Bucket, key: string);
50
+ getFile(): Promise<File>;
51
+ createWritable(): Promise<FileSystemWritableFileStream>;
52
+ createSyncAccessHandle(): Promise<FileSystemSyncAccessHandle>;
53
+ isSameEntry(other: FileSystemHandle): Promise<boolean>;
54
+ queryPermission(): Promise<PermissionState>;
55
+ requestPermission(): Promise<PermissionState>;
56
+ }
57
+ /**
58
+ * Cloudflare R2 implementation of FileSystemDirectoryHandle
59
+ */
60
+ export declare class R2FileSystemDirectoryHandle implements FileSystemDirectoryHandle {
61
+ #private;
62
+ readonly kind: "directory";
63
+ readonly name: string;
64
+ constructor(r2Bucket: R2Bucket, prefix: string);
65
+ getFileHandle(name: string, options?: {
66
+ create?: boolean;
67
+ }): Promise<FileSystemFileHandle>;
68
+ getDirectoryHandle(name: string, options?: {
69
+ create?: boolean;
70
+ }): Promise<FileSystemDirectoryHandle>;
71
+ removeEntry(name: string, options?: {
72
+ recursive?: boolean;
73
+ }): Promise<void>;
74
+ resolve(_possibleDescendant: FileSystemHandle): Promise<string[] | null>;
75
+ entries(): AsyncIterableIterator<[string, FileSystemHandle]>;
76
+ keys(): AsyncIterableIterator<string>;
77
+ values(): AsyncIterableIterator<FileSystemHandle>;
78
+ isSameEntry(other: FileSystemHandle): Promise<boolean>;
79
+ queryPermission(): Promise<PermissionState>;
80
+ requestPermission(): Promise<PermissionState>;
81
+ }
82
+ /**
83
+ * R2 filesystem adapter
84
+ */
85
+ export declare class R2FileSystemAdapter {
86
+ #private;
87
+ constructor(r2Bucket: R2Bucket, config?: FileSystemConfig);
88
+ getFileSystemRoot(name?: string): Promise<FileSystemDirectoryHandle>;
89
+ getConfig(): FileSystemConfig;
90
+ dispose(): Promise<void>;
91
+ }
@@ -0,0 +1,230 @@
1
+ /// <reference types="./filesystem-r2.d.ts" />
2
+ // src/filesystem-r2.ts
3
+ import mime from "mime";
4
+ var R2FileSystemWritableFileStream = class extends WritableStream {
5
+ #chunks;
6
+ #r2Bucket;
7
+ #key;
8
+ constructor(r2Bucket, key) {
9
+ const chunks = [];
10
+ super({
11
+ write: (chunk) => {
12
+ chunks.push(chunk);
13
+ return Promise.resolve();
14
+ },
15
+ close: async () => {
16
+ const totalLength = chunks.reduce(
17
+ (sum, chunk) => sum + chunk.length,
18
+ 0
19
+ );
20
+ const buffer = new Uint8Array(totalLength);
21
+ let offset = 0;
22
+ for (const chunk of chunks) {
23
+ buffer.set(chunk, offset);
24
+ offset += chunk.length;
25
+ }
26
+ await r2Bucket.put(key, buffer);
27
+ },
28
+ abort: async () => {
29
+ chunks.length = 0;
30
+ }
31
+ });
32
+ this.#chunks = chunks;
33
+ this.#r2Bucket = r2Bucket;
34
+ this.#key = key;
35
+ }
36
+ };
37
+ var R2FileSystemFileHandle = class _R2FileSystemFileHandle {
38
+ kind;
39
+ name;
40
+ #r2Bucket;
41
+ #key;
42
+ constructor(r2Bucket, key) {
43
+ this.kind = "file";
44
+ this.#r2Bucket = r2Bucket;
45
+ this.#key = key;
46
+ this.name = key.split("/").pop() || key;
47
+ }
48
+ async getFile() {
49
+ const r2Object = await this.#r2Bucket.get(this.#key);
50
+ if (!r2Object) {
51
+ throw new DOMException("File not found", "NotFoundError");
52
+ }
53
+ const arrayBuffer = await r2Object.arrayBuffer();
54
+ return new File([arrayBuffer], this.name, {
55
+ lastModified: r2Object.uploaded.getTime(),
56
+ type: r2Object.httpMetadata?.contentType || this.#getMimeType(this.#key)
57
+ });
58
+ }
59
+ async createWritable() {
60
+ return new R2FileSystemWritableFileStream(this.#r2Bucket, this.#key);
61
+ }
62
+ async createSyncAccessHandle() {
63
+ throw new DOMException(
64
+ "Synchronous access handles are not supported for R2 storage",
65
+ "InvalidStateError"
66
+ );
67
+ }
68
+ async isSameEntry(other) {
69
+ if (other.kind !== "file")
70
+ return false;
71
+ if (!(other instanceof _R2FileSystemFileHandle))
72
+ return false;
73
+ return this.#key === other.#key;
74
+ }
75
+ async queryPermission() {
76
+ return "granted";
77
+ }
78
+ async requestPermission() {
79
+ return "granted";
80
+ }
81
+ #getMimeType(key) {
82
+ return mime.getType(key) || "application/octet-stream";
83
+ }
84
+ };
85
+ var R2FileSystemDirectoryHandle = class _R2FileSystemDirectoryHandle {
86
+ kind;
87
+ name;
88
+ #r2Bucket;
89
+ #prefix;
90
+ constructor(r2Bucket, prefix) {
91
+ this.kind = "directory";
92
+ this.#r2Bucket = r2Bucket;
93
+ this.#prefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
94
+ this.name = this.#prefix.split("/").pop() || "root";
95
+ }
96
+ async getFileHandle(name, options) {
97
+ const key = this.#prefix ? `${this.#prefix}/${name}` : name;
98
+ const exists = await this.#r2Bucket.head(key);
99
+ if (!exists && options?.create) {
100
+ await this.#r2Bucket.put(key, new Uint8Array(0));
101
+ } else if (!exists) {
102
+ throw new DOMException("File not found", "NotFoundError");
103
+ }
104
+ return new R2FileSystemFileHandle(this.#r2Bucket, key);
105
+ }
106
+ async getDirectoryHandle(name, options) {
107
+ const newPrefix = this.#prefix ? `${this.#prefix}/${name}` : name;
108
+ if (options?.create) {
109
+ const markerKey = `${newPrefix}/.shovel_directory_marker`;
110
+ const exists = await this.#r2Bucket.head(markerKey);
111
+ if (!exists) {
112
+ await this.#r2Bucket.put(markerKey, new Uint8Array(0));
113
+ }
114
+ }
115
+ return new _R2FileSystemDirectoryHandle(this.#r2Bucket, newPrefix);
116
+ }
117
+ async removeEntry(name, options) {
118
+ const key = this.#prefix ? `${this.#prefix}/${name}` : name;
119
+ const fileExists = await this.#r2Bucket.head(key);
120
+ if (fileExists) {
121
+ await this.#r2Bucket.delete(key);
122
+ return;
123
+ }
124
+ if (options?.recursive) {
125
+ const dirPrefix = `${key}/`;
126
+ const listed = await this.#r2Bucket.list({ prefix: dirPrefix });
127
+ const deletePromises = listed.objects.map(
128
+ (object) => this.#r2Bucket.delete(object.key)
129
+ );
130
+ await Promise.all(deletePromises);
131
+ const markerKey = `${key}/.shovel_directory_marker`;
132
+ const markerExists = await this.#r2Bucket.head(markerKey);
133
+ if (markerExists) {
134
+ await this.#r2Bucket.delete(markerKey);
135
+ }
136
+ } else {
137
+ throw new DOMException(
138
+ "Directory is not empty",
139
+ "InvalidModificationError"
140
+ );
141
+ }
142
+ }
143
+ async resolve(_possibleDescendant) {
144
+ return null;
145
+ }
146
+ async *entries() {
147
+ const listPrefix = this.#prefix ? `${this.#prefix}/` : "";
148
+ try {
149
+ const result = await this.#r2Bucket.list({
150
+ prefix: listPrefix,
151
+ delimiter: "/"
152
+ // Only get immediate children
153
+ });
154
+ for (const object of result.objects) {
155
+ if (object.key !== listPrefix) {
156
+ const name = object.key.substring(listPrefix.length);
157
+ if (!name.includes("/") && !name.endsWith(".shovel_directory_marker")) {
158
+ yield [
159
+ name,
160
+ new R2FileSystemFileHandle(this.#r2Bucket, object.key)
161
+ ];
162
+ }
163
+ }
164
+ }
165
+ for (const prefix of result.delimitedPrefixes) {
166
+ const name = prefix.substring(listPrefix.length).replace(/\/$/, "");
167
+ if (name) {
168
+ yield [
169
+ name,
170
+ new _R2FileSystemDirectoryHandle(
171
+ this.#r2Bucket,
172
+ prefix.replace(/\/$/, "")
173
+ )
174
+ ];
175
+ }
176
+ }
177
+ } catch (error) {
178
+ throw new DOMException("Directory not found", "NotFoundError");
179
+ }
180
+ }
181
+ async *keys() {
182
+ for await (const [name] of this.entries()) {
183
+ yield name;
184
+ }
185
+ }
186
+ async *values() {
187
+ for await (const [, handle] of this.entries()) {
188
+ yield handle;
189
+ }
190
+ }
191
+ async isSameEntry(other) {
192
+ if (other.kind !== "directory")
193
+ return false;
194
+ if (!(other instanceof _R2FileSystemDirectoryHandle))
195
+ return false;
196
+ return this.#prefix === other.#prefix;
197
+ }
198
+ async queryPermission() {
199
+ return "granted";
200
+ }
201
+ async requestPermission() {
202
+ return "granted";
203
+ }
204
+ };
205
+ var R2FileSystemAdapter = class {
206
+ #config;
207
+ #r2Bucket;
208
+ constructor(r2Bucket, config = {}) {
209
+ this.#config = {
210
+ name: "r2",
211
+ ...config
212
+ };
213
+ this.#r2Bucket = r2Bucket;
214
+ }
215
+ async getFileSystemRoot(name = "default") {
216
+ const prefix = `filesystems/${name}`;
217
+ return new R2FileSystemDirectoryHandle(this.#r2Bucket, prefix);
218
+ }
219
+ getConfig() {
220
+ return { ...this.#config };
221
+ }
222
+ async dispose() {
223
+ }
224
+ };
225
+ export {
226
+ R2FileSystemAdapter,
227
+ R2FileSystemDirectoryHandle,
228
+ R2FileSystemFileHandle,
229
+ R2FileSystemWritableFileStream
230
+ };
package/src/index.d.ts CHANGED
@@ -2,23 +2,50 @@
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, Handler, Server, ServerOptions, ServiceWorkerOptions, ServiceWorkerInstance, EntryWrapperOptions, PlatformEsbuildConfig } from "@b9g/platform";
13
+ import { CustomCacheStorage } from "@b9g/cache";
14
+ import { ShovelServiceWorkerRegistration } from "@b9g/platform/runtime";
15
+ import type { ExecutionContext } from "./cloudflare-runtime.js";
7
16
  export type { Platform, Handler, Server, ServerOptions, ServiceWorkerOptions, ServiceWorkerInstance, } from "@b9g/platform";
17
+ /**
18
+ * Get the current request's Cloudflare env object
19
+ * Contains all bindings: KV namespaces, R2 buckets, D1 databases, etc.
20
+ */
21
+ export declare function getEnv<T = Record<string, unknown>>(): T | undefined;
22
+ /**
23
+ * Get the current request's Cloudflare ExecutionContext
24
+ * Used for ctx.waitUntil() and other lifecycle methods
25
+ */
26
+ export declare function getCtx(): ExecutionContext | undefined;
8
27
  export interface CloudflarePlatformOptions extends PlatformConfig {
9
28
  /** Cloudflare Workers environment (production, preview, dev) */
10
29
  environment?: "production" | "preview" | "dev";
11
30
  /** Static assets directory for ASSETS binding (dev mode) */
12
31
  assetsDirectory?: string;
13
- /** KV namespace bindings */
14
- kvNamespaces?: Record<string, any>;
15
- /** R2 bucket bindings */
16
- r2Buckets?: Record<string, any>;
17
- /** D1 database bindings */
18
- d1Databases?: Record<string, any>;
19
- /** Durable Object bindings */
20
- durableObjects?: Record<string, any>;
32
+ /** Working directory for config file resolution */
33
+ cwd?: string;
21
34
  }
35
+ /**
36
+ * Initialize the Cloudflare runtime with ServiceWorkerGlobals
37
+ * Called once when the worker module loads (before user code runs)
38
+ *
39
+ * This sets up:
40
+ * - ServiceWorkerGlobals (caches, directories, cookieStore, addEventListener, etc.)
41
+ * - Per-request env/ctx via AsyncContext
42
+ */
43
+ export declare function initializeRuntime(): ShovelServiceWorkerRegistration;
44
+ /**
45
+ * Create the ES module fetch handler for Cloudflare Workers
46
+ * This wraps requests with AsyncContext so env/ctx are available everywhere
47
+ */
48
+ export declare function createFetchHandler(registration: ShovelServiceWorkerRegistration): (request: Request, env: unknown, ctx: ExecutionContext) => Promise<Response>;
22
49
  /**
23
50
  * Cloudflare Workers platform implementation
24
51
  */
@@ -27,28 +54,42 @@ export declare class CloudflarePlatform extends BasePlatform {
27
54
  readonly name: string;
28
55
  constructor(options?: CloudflarePlatformOptions);
29
56
  /**
30
- * Create "server" for Cloudflare Workers (which is really just the handler)
57
+ * Create cache storage
58
+ * Uses Cloudflare's native Cache API
59
+ */
60
+ createCaches(): Promise<CustomCacheStorage>;
61
+ /**
62
+ * Create "server" for Cloudflare Workers (stub for Platform interface)
31
63
  */
32
64
  createServer(handler: Handler, _options?: ServerOptions): Server;
33
65
  /**
34
- * Load ServiceWorker-style entrypoint in Cloudflare Workers
35
- *
36
- * In production: Uses the native CF Worker environment
37
- * In dev mode: Uses miniflare (workerd) for true dev/prod parity
66
+ * Load ServiceWorker using miniflare (workerd) for dev mode
38
67
  */
39
68
  loadServiceWorker(entrypoint: string, _options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
69
+ dispose(): Promise<void>;
70
+ /**
71
+ * Get virtual entry wrapper for Cloudflare Workers
72
+ *
73
+ * Wraps user code with:
74
+ * 1. Config import (shovel:config virtual module)
75
+ * 2. Runtime initialization (ServiceWorkerGlobals)
76
+ * 3. User code import (registers fetch handlers)
77
+ * 4. ES module export for Cloudflare Workers format
78
+ *
79
+ * Note: Unlike Node/Bun, Cloudflare bundles user code inline, so the
80
+ * entryPath is embedded directly in the wrapper.
81
+ */
82
+ getEntryWrapper(entryPath: string, _options?: EntryWrapperOptions): string;
40
83
  /**
41
- * Dispose of platform resources
84
+ * Get Cloudflare-specific esbuild configuration
85
+ *
86
+ * Note: Cloudflare Workers natively support import.meta.env, so no define alias
87
+ * is needed. The nodejs_compat flag enables node:* built-in modules at runtime,
88
+ * so we externalize them during bundling.
42
89
  */
43
- dispose(): Promise<void>;
90
+ getEsbuildConfig(): PlatformEsbuildConfig;
44
91
  }
45
- /**
46
- * Create platform options from Wrangler environment
47
- */
48
92
  export declare function createOptionsFromEnv(env: any): CloudflarePlatformOptions;
49
- /**
50
- * Generate wrangler.toml configuration for a Shovel app from CLI flags
51
- */
52
93
  export declare function generateWranglerConfig(options: {
53
94
  name: string;
54
95
  entrypoint: string;
@@ -58,15 +99,4 @@ export declare function generateWranglerConfig(options: {
58
99
  r2Buckets?: string[];
59
100
  d1Databases?: string[];
60
101
  }): string;
61
- /**
62
- * Generate banner code for ServiceWorker → ES Module conversion
63
- */
64
- export declare const cloudflareWorkerBanner = "// Cloudflare Worker ES Module wrapper\nlet serviceWorkerGlobals = null;\n\n// Set up ServiceWorker environment\nif (typeof globalThis.self === 'undefined') {\n\tglobalThis.self = globalThis;\n}\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}";
65
- /**
66
- * Generate footer code for ServiceWorker → ES Module conversion
67
- */
68
- 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\tlogger.debug(\"Calling handler\", {url: request.url});\n\t\t\t\t\tawait handler(event);\n\t\t\t\t\tlogger.debug(\"Handler completed\", {hasResponse: !!responseReceived});\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\tlogger.error(\"Handler error: {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\tlogger.error(\"Top-level error: {error}\", {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};";
69
- /**
70
- * Default export for easy importing
71
- */
72
102
  export default CloudflarePlatform;
package/src/index.js CHANGED
@@ -3,33 +3,133 @@
3
3
  import {
4
4
  BasePlatform
5
5
  } from "@b9g/platform";
6
+ import { CustomCacheStorage } from "@b9g/cache";
7
+ import {
8
+ ServiceWorkerGlobals,
9
+ ShovelServiceWorkerRegistration,
10
+ CustomLoggerStorage
11
+ } from "@b9g/platform/runtime";
12
+ import { CustomDirectoryStorage } from "@b9g/filesystem";
13
+ import { AsyncContext } from "@b9g/async-context";
6
14
  import { getLogger } from "@logtape/logtape";
7
- var logger = getLogger(["platform-cloudflare"]);
15
+ import { R2FileSystemDirectoryHandle } from "./filesystem-r2.js";
16
+ var logger = getLogger(["platform"]);
17
+ var envStorage = new AsyncContext.Variable();
18
+ var ctxStorage = new AsyncContext.Variable();
19
+ function getEnv() {
20
+ return envStorage.get();
21
+ }
22
+ function getCtx() {
23
+ return ctxStorage.get();
24
+ }
25
+ var _registration = null;
26
+ var _globals = null;
27
+ function initializeRuntime() {
28
+ if (_registration) {
29
+ return _registration;
30
+ }
31
+ _registration = new ShovelServiceWorkerRegistration();
32
+ const directories = new CustomDirectoryStorage(
33
+ createCloudflareR2DirectoryFactory()
34
+ );
35
+ _globals = new ServiceWorkerGlobals({
36
+ registration: _registration,
37
+ caches: globalThis.caches,
38
+ // Use Cloudflare's native Cache API
39
+ directories,
40
+ loggers: new CustomLoggerStorage((...cats) => getLogger(cats))
41
+ });
42
+ _globals.install();
43
+ return _registration;
44
+ }
45
+ function createFetchHandler(registration) {
46
+ return async (request, env, ctx) => {
47
+ return envStorage.run(
48
+ env,
49
+ () => ctxStorage.run(ctx, async () => {
50
+ try {
51
+ return await registration.handleRequest(request);
52
+ } catch (error) {
53
+ console.error("ServiceWorker error:", error);
54
+ const err = error instanceof Error ? error : new Error(String(error));
55
+ const isDev = typeof import.meta !== "undefined" && import.meta.env?.MODE !== "production";
56
+ if (isDev) {
57
+ return new Response(
58
+ `<!DOCTYPE html>
59
+ <html>
60
+ <head><title>500 Internal Server Error</title>
61
+ <style>body{font-family:system-ui;padding:2rem;max-width:800px;margin:0 auto}h1{color:#c00}pre{background:#f5f5f5;padding:1rem;overflow-x:auto}</style>
62
+ </head>
63
+ <body>
64
+ <h1>500 Internal Server Error</h1>
65
+ <p>${escapeHtml(err.message)}</p>
66
+ <pre>${escapeHtml(err.stack || "No stack trace")}</pre>
67
+ </body></html>`,
68
+ { status: 500, headers: { "Content-Type": "text/html" } }
69
+ );
70
+ }
71
+ return new Response("Internal Server Error", { status: 500 });
72
+ }
73
+ })
74
+ );
75
+ };
76
+ }
77
+ function escapeHtml(str) {
78
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
79
+ }
80
+ function createCloudflareR2DirectoryFactory() {
81
+ return async (name) => {
82
+ const env = getEnv();
83
+ if (!env) {
84
+ throw new Error(
85
+ `Cannot access directory "${name}": Cloudflare env not available. This usually means you're trying to access directories outside of a request context.`
86
+ );
87
+ }
88
+ const bindingName = `${name.toUpperCase()}_R2`;
89
+ const r2Bucket = env[bindingName];
90
+ if (!r2Bucket) {
91
+ throw new Error(
92
+ `R2 bucket binding "${bindingName}" not found in env. Configure in wrangler.toml:
93
+
94
+ [[r2_buckets]]
95
+ binding = "${bindingName}"
96
+ bucket_name = "your-bucket-name"`
97
+ );
98
+ }
99
+ return new R2FileSystemDirectoryHandle(r2Bucket, "");
100
+ };
101
+ }
8
102
  var CloudflarePlatform = class extends BasePlatform {
9
103
  name;
10
104
  #options;
11
105
  #miniflare;
12
106
  #assetsMiniflare;
13
- // Separate instance for ASSETS binding
14
- #assetsBinding;
15
107
  constructor(options = {}) {
16
108
  super(options);
17
109
  this.#miniflare = null;
18
110
  this.#assetsMiniflare = null;
19
- this.#assetsBinding = null;
20
111
  this.name = "cloudflare";
112
+ const cwd = options.cwd ?? ".";
21
113
  this.#options = {
22
- environment: "production",
23
- assetsDirectory: void 0,
24
- kvNamespaces: {},
25
- r2Buckets: {},
26
- d1Databases: {},
27
- durableObjects: {},
28
- ...options
114
+ environment: options.environment ?? "production",
115
+ assetsDirectory: options.assetsDirectory,
116
+ cwd
29
117
  };
30
118
  }
31
119
  /**
32
- * Create "server" for Cloudflare Workers (which is really just the handler)
120
+ * Create cache storage
121
+ * Uses Cloudflare's native Cache API
122
+ */
123
+ async createCaches() {
124
+ return new CustomCacheStorage(async (name) => {
125
+ if (globalThis.caches) {
126
+ return globalThis.caches.open(name);
127
+ }
128
+ throw new Error("Cloudflare caches not available in this context");
129
+ });
130
+ }
131
+ /**
132
+ * Create "server" for Cloudflare Workers (stub for Platform interface)
33
133
  */
34
134
  createServer(handler, _options = {}) {
35
135
  return {
@@ -48,55 +148,22 @@ var CloudflarePlatform = class extends BasePlatform {
48
148
  }
49
149
  };
50
150
  }
51
- /**
52
- * Load ServiceWorker-style entrypoint in Cloudflare Workers
53
- *
54
- * In production: Uses the native CF Worker environment
55
- * In dev mode: Uses miniflare (workerd) for true dev/prod parity
56
- */
57
- async loadServiceWorker(entrypoint, _options = {}) {
58
- const isCloudflareWorker = typeof globalThis.addEventListener === "function" && typeof globalThis.caches !== "undefined" && typeof globalThis.FetchEvent !== "undefined";
59
- if (isCloudflareWorker) {
60
- logger.info("Running in native ServiceWorker environment", {});
61
- const instance = {
62
- runtime: globalThis,
63
- handleRequest: async (request) => {
64
- const event = new FetchEvent("fetch", { request });
65
- globalThis.dispatchEvent(event);
66
- return new Response("Worker handler", { status: 200 });
67
- },
68
- install: () => Promise.resolve(),
69
- activate: () => Promise.resolve(),
70
- get ready() {
71
- return true;
72
- },
73
- dispose: async () => {
74
- }
75
- };
76
- await import(entrypoint);
77
- return instance;
78
- } else {
79
- return this.#loadServiceWorkerWithMiniflare(entrypoint);
80
- }
81
- }
82
151
  /**
83
152
  * Load ServiceWorker using miniflare (workerd) for dev mode
84
153
  */
85
- async #loadServiceWorkerWithMiniflare(entrypoint) {
154
+ async loadServiceWorker(entrypoint, _options = {}) {
86
155
  logger.info("Starting miniflare dev server", { entrypoint });
87
156
  const { Miniflare } = await import("miniflare");
88
157
  const miniflareOptions = {
89
- modules: false,
90
- // ServiceWorker format (not ES modules)
158
+ modules: true,
91
159
  scriptPath: entrypoint,
92
- // Enable CF-compatible APIs
93
160
  compatibilityDate: "2024-09-23",
94
161
  compatibilityFlags: ["nodejs_compat"]
95
162
  };
96
163
  this.#miniflare = new Miniflare(miniflareOptions);
97
164
  await this.#miniflare.ready;
98
165
  if (this.#options.assetsDirectory) {
99
- logger.info("Setting up separate ASSETS binding", {
166
+ logger.info("Setting up ASSETS binding", {
100
167
  directory: this.#options.assetsDirectory
101
168
  });
102
169
  this.#assetsMiniflare = new Miniflare({
@@ -108,11 +175,7 @@ var CloudflarePlatform = class extends BasePlatform {
108
175
  },
109
176
  compatibilityDate: "2024-09-23"
110
177
  });
111
- const assetsEnv = await this.#assetsMiniflare.getBindings();
112
- if (assetsEnv.ASSETS) {
113
- this.#assetsBinding = assetsEnv.ASSETS;
114
- logger.info("ASSETS binding available", {});
115
- }
178
+ await this.#assetsMiniflare.ready;
116
179
  }
117
180
  const mf = this.#miniflare;
118
181
  const instance = {
@@ -142,9 +205,6 @@ var CloudflarePlatform = class extends BasePlatform {
142
205
  logger.info("Miniflare dev server ready", {});
143
206
  return instance;
144
207
  }
145
- /**
146
- * Dispose of platform resources
147
- */
148
208
  async dispose() {
149
209
  if (this.#miniflare) {
150
210
  await this.#miniflare.dispose();
@@ -154,67 +214,75 @@ var CloudflarePlatform = class extends BasePlatform {
154
214
  await this.#assetsMiniflare.dispose();
155
215
  this.#assetsMiniflare = null;
156
216
  }
157
- this.#assetsBinding = null;
217
+ }
218
+ /**
219
+ * Get virtual entry wrapper for Cloudflare Workers
220
+ *
221
+ * Wraps user code with:
222
+ * 1. Config import (shovel:config virtual module)
223
+ * 2. Runtime initialization (ServiceWorkerGlobals)
224
+ * 3. User code import (registers fetch handlers)
225
+ * 4. ES module export for Cloudflare Workers format
226
+ *
227
+ * Note: Unlike Node/Bun, Cloudflare bundles user code inline, so the
228
+ * entryPath is embedded directly in the wrapper.
229
+ */
230
+ getEntryWrapper(entryPath, _options) {
231
+ const safePath = JSON.stringify(entryPath);
232
+ return `// Cloudflare Worker Entry - uses ServiceWorkerGlobals for feature parity with Node/Bun
233
+ import { initializeRuntime, createFetchHandler } from "@b9g/platform-cloudflare/cloudflare-runtime";
234
+ import { configureLogging } from "@b9g/platform/runtime";
235
+ import { config } from "shovel:config"; // Virtual module - resolved at build time
236
+
237
+ // Configure logging before anything else
238
+ await configureLogging(config.logging);
239
+
240
+ // Initialize runtime BEFORE user code (installs globals like addEventListener)
241
+ const registration = initializeRuntime();
242
+
243
+ // Import user's ServiceWorker code (calls addEventListener('fetch', ...))
244
+ import ${safePath};
245
+
246
+ // Export ES module handler for Cloudflare Workers
247
+ export default {
248
+ fetch: createFetchHandler(registration)
249
+ };
250
+ `;
251
+ }
252
+ /**
253
+ * Get Cloudflare-specific esbuild configuration
254
+ *
255
+ * Note: Cloudflare Workers natively support import.meta.env, so no define alias
256
+ * is needed. The nodejs_compat flag enables node:* built-in modules at runtime,
257
+ * so we externalize them during bundling.
258
+ */
259
+ getEsbuildConfig() {
260
+ return {
261
+ platform: "browser",
262
+ conditions: ["worker", "browser"],
263
+ // Externalize node:* builtins - available at runtime via nodejs_compat flag
264
+ external: ["node:*"],
265
+ // Cloudflare bundles user code inline via `import "user-entry"`
266
+ bundlesUserCodeInline: true
267
+ };
158
268
  }
159
269
  };
160
270
  function createOptionsFromEnv(env) {
161
271
  return {
162
- environment: env.ENVIRONMENT || "production",
163
- kvNamespaces: extractKVNamespaces(env),
164
- r2Buckets: extractR2Buckets(env),
165
- d1Databases: extractD1Databases(env),
166
- durableObjects: extractDurableObjects(env)
272
+ environment: env.ENVIRONMENT || "production"
167
273
  };
168
274
  }
169
- function extractKVNamespaces(env) {
170
- const kvNamespaces = {};
171
- for (const [key, value] of Object.entries(env)) {
172
- if (key.endsWith("_KV") || key.includes("KV")) {
173
- kvNamespaces[key] = value;
174
- }
175
- }
176
- return kvNamespaces;
177
- }
178
- function extractR2Buckets(env) {
179
- const r2Buckets = {};
180
- for (const [key, value] of Object.entries(env)) {
181
- if (key.endsWith("_R2") || key.includes("R2")) {
182
- r2Buckets[key] = value;
183
- }
184
- }
185
- return r2Buckets;
186
- }
187
- function extractD1Databases(env) {
188
- const d1Databases = {};
189
- for (const [key, value] of Object.entries(env)) {
190
- if (key.endsWith("_D1") || key.includes("D1") || key.endsWith("_DB")) {
191
- d1Databases[key] = value;
192
- }
193
- }
194
- return d1Databases;
195
- }
196
- function extractDurableObjects(env) {
197
- const durableObjects = {};
198
- for (const [key, value] of Object.entries(env)) {
199
- if (key.endsWith("_DO") || key.includes("DURABLE")) {
200
- durableObjects[key] = value;
201
- }
202
- }
203
- return durableObjects;
204
- }
205
275
  function generateWranglerConfig(options) {
206
276
  const {
207
277
  name,
208
278
  entrypoint,
209
- cacheAdapter: _cacheAdapter,
210
279
  filesystemAdapter,
211
280
  kvNamespaces = [],
212
281
  r2Buckets = [],
213
282
  d1Databases = []
214
283
  } = options;
215
- const autoKVNamespaces = [];
216
284
  const autoR2Buckets = filesystemAdapter === "r2" ? ["STORAGE_R2"] : [];
217
- const allKVNamespaces = [.../* @__PURE__ */ new Set([...kvNamespaces, ...autoKVNamespaces])];
285
+ const allKVNamespaces = [...new Set(kvNamespaces)];
218
286
  const allR2Buckets = [.../* @__PURE__ */ new Set([...r2Buckets, ...autoR2Buckets])];
219
287
  return `# Generated wrangler.toml for Shovel app
220
288
  name = "${name}"
@@ -222,189 +290,28 @@ main = "${entrypoint}"
222
290
  compatibility_date = "2024-09-23"
223
291
  compatibility_flags = ["nodejs_compat"]
224
292
 
225
- # ServiceWorker format (since Shovel apps are ServiceWorker-style)
226
- usage_model = "bundled"
227
-
228
- # KV bindings${allKVNamespaces.length > 0 ? "\n" + allKVNamespaces.map(
229
- (kv) => `[[kv_namespaces]]
293
+ ${allKVNamespaces.length > 0 ? allKVNamespaces.map((kv) => `[[kv_namespaces]]
230
294
  binding = "${kv}"
231
- id = "your-kv-namespace-id"
232
- preview_id = "your-preview-kv-namespace-id"`
233
- ).join("\n\n") : ""}
295
+ id = "your-kv-id"`).join("\n\n") : "# No KV namespaces configured"}
234
296
 
235
- # R2 bindings${allR2Buckets.length > 0 ? "\n" + allR2Buckets.map(
236
- (bucket) => `[[r2_buckets]]
297
+ ${allR2Buckets.length > 0 ? allR2Buckets.map((bucket) => `[[r2_buckets]]
237
298
  binding = "${bucket}"
238
- bucket_name = "your-bucket-name"`
239
- ).join("\n\n") : ""}
299
+ bucket_name = "your-bucket-name"`).join("\n\n") : "# No R2 buckets configured"}
240
300
 
241
- # D1 bindings
242
- ${d1Databases.map(
243
- (db) => `[[d1_databases]]
301
+ ${d1Databases.length > 0 ? d1Databases.map((db) => `[[d1_databases]]
244
302
  binding = "${db}"
245
- database_name = "your-database-name"
246
- database_id = "your-database-id"`
247
- ).join("\n\n")}
303
+ database_name = "your-db-name"
304
+ database_id = "your-db-id"`).join("\n\n") : "# No D1 databases configured"}
248
305
  `;
249
306
  }
250
- var cloudflareWorkerBanner = `// Cloudflare Worker ES Module wrapper
251
- let serviceWorkerGlobals = null;
252
-
253
- // Set up ServiceWorker environment
254
- if (typeof globalThis.self === 'undefined') {
255
- globalThis.self = globalThis;
256
- }
257
-
258
- // Capture fetch event handlers
259
- const fetchHandlers = [];
260
- const originalAddEventListener = globalThis.addEventListener;
261
- globalThis.addEventListener = function(type, handler, options) {
262
- if (type === 'fetch') {
263
- fetchHandlers.push(handler);
264
- } else {
265
- originalAddEventListener?.call(this, type, handler, options);
266
- }
267
- };
268
-
269
- // Create a promise-based FetchEvent that can be awaited
270
- class FetchEvent {
271
- constructor(type, init) {
272
- this.type = type;
273
- this.request = init.request;
274
- this._response = null;
275
- this._responsePromise = new Promise((resolve) => {
276
- this._resolveResponse = resolve;
277
- });
278
- }
279
-
280
- respondWith(response) {
281
- this._response = response;
282
- this._resolveResponse(response);
283
- }
284
-
285
- async waitUntil(promise) {
286
- await promise;
287
- }
288
- }`;
289
- var cloudflareWorkerFooter = `
290
- // Export ES Module for Cloudflare Workers
291
- export default {
292
- async fetch(request, env, ctx) {
293
- try {
294
- // Set up ServiceWorker-like dirs API for bundled deployment
295
- if (!globalThis.self.dirs) {
296
- // For bundled deployment, assets are served via static middleware
297
- // not through the dirs API
298
- globalThis.self.dirs = {
299
- async open(directoryName) {
300
- if (directoryName === 'assets') {
301
- // Return a minimal interface that indicates no files available
302
- // The assets middleware will fall back to dev mode behavior
303
- return {
304
- async getFileHandle(fileName) {
305
- throw new Error(\`NotFoundError: \${fileName} not found in bundled assets\`);
306
- }
307
- };
308
- }
309
- throw new Error(\`Directory \${directoryName} not available in bundled deployment\`);
310
- }
311
- };
312
- }
313
-
314
- // Set up caches API
315
- if (!globalThis.self.caches) {
316
- globalThis.self.caches = globalThis.caches;
317
- }
318
-
319
- // Ensure request.url is a string
320
- if (typeof request.url !== 'string') {
321
- return new Response('Invalid request URL: ' + typeof request.url, { status: 500 });
322
- }
323
-
324
- // Create proper FetchEvent-like object
325
- let responseReceived = null;
326
- const event = {
327
- request,
328
- respondWith: (response) => { responseReceived = response; }
329
- };
330
-
331
- // Helper for error responses
332
- const createErrorResponse = (err) => {
333
- const isDev = typeof import.meta !== "undefined" && import.meta.env?.MODE !== "production";
334
- if (isDev) {
335
- const escapeHtml = (str) => str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
336
- return new Response(\`<!DOCTYPE html>
337
- <html>
338
- <head>
339
- <title>500 Internal Server Error</title>
340
- <style>
341
- body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
342
- h1 { color: #c00; }
343
- .message { font-size: 1.2em; color: #333; }
344
- pre { background: #f5f5f5; padding: 1rem; overflow-x: auto; border-radius: 4px; }
345
- </style>
346
- </head>
347
- <body>
348
- <h1>500 Internal Server Error</h1>
349
- <p class="message">\${escapeHtml(err.message)}</p>
350
- <pre>\${escapeHtml(err.stack || "No stack trace available")}</pre>
351
- </body>
352
- </html>\`, { status: 500, headers: { "Content-Type": "text/html; charset=utf-8" } });
353
- } else {
354
- return new Response("Internal Server Error", { status: 500, headers: { "Content-Type": "text/plain" } });
355
- }
356
- };
357
-
358
- // Dispatch to ServiceWorker fetch handlers
359
- for (const handler of fetchHandlers) {
360
- try {
361
- logger.debug("Calling handler", {url: request.url});
362
- await handler(event);
363
- logger.debug("Handler completed", {hasResponse: !!responseReceived});
364
- if (responseReceived) {
365
- return responseReceived;
366
- }
367
- } catch (error) {
368
- logger.error("Handler error: {error}", {error});
369
- return createErrorResponse(error);
370
- }
371
- }
372
-
373
- return new Response('No ServiceWorker handler', { status: 404 });
374
- } catch (topLevelError) {
375
- logger.error("Top-level error: {error}", {error: topLevelError});
376
- const isDev = typeof import.meta !== "undefined" && import.meta.env?.MODE !== "production";
377
- if (isDev) {
378
- const escapeHtml = (str) => String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
379
- return new Response(\`<!DOCTYPE html>
380
- <html>
381
- <head>
382
- <title>500 Internal Server Error</title>
383
- <style>
384
- body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
385
- h1 { color: #c00; }
386
- .message { font-size: 1.2em; color: #333; }
387
- pre { background: #f5f5f5; padding: 1rem; overflow-x: auto; border-radius: 4px; }
388
- </style>
389
- </head>
390
- <body>
391
- <h1>500 Internal Server Error</h1>
392
- <p class="message">\${escapeHtml(topLevelError.message)}</p>
393
- <pre>\${escapeHtml(topLevelError.stack || "No stack trace available")}</pre>
394
- </body>
395
- </html>\`, { status: 500, headers: { "Content-Type": "text/html; charset=utf-8" } });
396
- } else {
397
- return new Response("Internal Server Error", { status: 500, headers: { "Content-Type": "text/plain" } });
398
- }
399
- }
400
- }
401
- };`;
402
307
  var src_default = CloudflarePlatform;
403
308
  export {
404
309
  CloudflarePlatform,
405
- cloudflareWorkerBanner,
406
- cloudflareWorkerFooter,
310
+ createFetchHandler,
407
311
  createOptionsFromEnv,
408
312
  src_default as default,
409
- generateWranglerConfig
313
+ generateWranglerConfig,
314
+ getCtx,
315
+ getEnv,
316
+ initializeRuntime
410
317
  };