@b9g/platform-cloudflare 0.1.2 → 0.1.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.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @b9g/platform-cloudflare
2
+
3
+ Cloudflare Workers platform adapter for Shovel. Runs ServiceWorker applications on Cloudflare's edge network with KV storage and Durable Objects support.
4
+
5
+ ## Features
6
+
7
+ - Cloudflare Workers integration
8
+ - KV storage for caching
9
+ - R2 bucket support for assets
10
+ - Durable Objects for stateful apps
11
+ - Standards-compliant ServiceWorker API
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @b9g/platform-cloudflare
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```javascript
22
+ import CloudflarePlatform from '@b9g/platform-cloudflare';
23
+
24
+ const platform = new CloudflarePlatform({
25
+ cache: { type: 'kv', binding: 'CACHE_KV' },
26
+ filesystem: { type: 'r2', binding: 'ASSETS_R2' }
27
+ });
28
+
29
+ export default {
30
+ async fetch(request, env, ctx) {
31
+ return await platform.handleRequest(request);
32
+ }
33
+ };
34
+ ```
35
+
36
+ ## Requirements
37
+
38
+ Shovel requires Node.js compatibility for AsyncLocalStorage (used by AsyncContext polyfill for `self.cookieStore`). Add to your `wrangler.toml`:
39
+
40
+ ```toml
41
+ compatibility_date = "2024-09-23"
42
+ compatibility_flags = ["nodejs_compat"]
43
+ ```
44
+
45
+ ## Exports
46
+
47
+ ### Classes
48
+
49
+ - `CloudflarePlatform` - Cloudflare Workers platform implementation (extends BasePlatform)
50
+ - `CFAssetsDirectoryHandle` - FileSystemDirectoryHandle for Cloudflare Workers Static Assets
51
+ - `CFAssetsFileHandle` - FileSystemFileHandle for Cloudflare Workers Static Assets
52
+
53
+ ### Functions
54
+
55
+ - `createOptionsFromEnv(env)` - Create platform options from Cloudflare env bindings
56
+ - `generateWranglerConfig(options)` - Generate wrangler.toml configuration
57
+
58
+ ### Types
59
+
60
+ - `CloudflarePlatformOptions` - Configuration options for CloudflarePlatform
61
+ - `CFAssetsBinding` - Type for Cloudflare Workers Static Assets binding
62
+
63
+ ### Constants
64
+
65
+ - `cloudflareWorkerBanner` - ES Module wrapper banner for Cloudflare Workers
66
+ - `cloudflareWorkerFooter` - ES Module wrapper footer
67
+
68
+ ### Default Export
69
+
70
+ - `CloudflarePlatform` - The platform class
71
+
72
+ ## API
73
+
74
+ ### `new CloudflarePlatform(options?)`
75
+
76
+ Creates a new Cloudflare platform instance.
77
+
78
+ **Options:**
79
+ - `cache`: Cache configuration (KV binding)
80
+ - `filesystem`: Filesystem configuration (R2 binding)
81
+ - `env`: Cloudflare environment bindings
82
+
83
+ ### Bindings
84
+
85
+ Configure bindings in `wrangler.toml`:
86
+
87
+ ```toml
88
+ compatibility_date = "2024-09-23"
89
+ compatibility_flags = ["nodejs_compat"]
90
+
91
+ [[kv_namespaces]]
92
+ binding = "CACHE_KV"
93
+ id = "your-kv-namespace-id"
94
+
95
+ [[r2_buckets]]
96
+ binding = "ASSETS_R2"
97
+ bucket_name = "your-bucket-name"
98
+ ```
99
+
100
+ ## Cache Backends
101
+
102
+ - `kv`: Cloudflare KV storage
103
+ - `cache-api`: Cloudflare Cache API (default)
104
+
105
+ ## Filesystem Backends
106
+
107
+ - `r2`: Cloudflare R2 bucket storage
108
+
109
+ ## License
110
+
111
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/platform-cloudflare",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "Cloudflare Workers platform adapter for Shovel - already ServiceWorker-based!",
5
5
  "keywords": [
6
6
  "shovel",
@@ -11,13 +11,14 @@
11
11
  "serviceworker"
12
12
  ],
13
13
  "dependencies": {
14
- "@b9g/platform": "^0.1.1",
15
- "@b9g/cache": "^0.1.1",
16
- "@b9g/assets": "^0.1.1",
17
- "@cloudflare/workers-types": "^4.20241218.0"
14
+ "@b9g/assets": "^0.1.5",
15
+ "@b9g/cache": "^0.1.4",
16
+ "@b9g/platform": "^0.1.5",
17
+ "@cloudflare/workers-types": "^4.20241218.0",
18
+ "miniflare": "^4.20251118.1"
18
19
  },
19
20
  "devDependencies": {
20
- "@b9g/libuild": "^0.1.10",
21
+ "@b9g/libuild": "^0.1.11",
21
22
  "bun-types": "latest"
22
23
  },
23
24
  "type": "module",
@@ -28,18 +29,6 @@
28
29
  "types": "./src/index.d.ts",
29
30
  "import": "./src/index.js"
30
31
  },
31
- "./platform": {
32
- "types": "./src/platform.d.ts",
33
- "import": "./src/platform.js"
34
- },
35
- "./platform.js": {
36
- "types": "./src/platform.d.ts",
37
- "import": "./src/platform.js"
38
- },
39
- "./wrangler": {
40
- "types": "./src/wrangler.d.ts",
41
- "import": "./src/wrangler.js"
42
- },
43
32
  "./package.json": "./package.json",
44
33
  "./index": {
45
34
  "types": "./src/index.d.ts",
@@ -49,17 +38,13 @@
49
38
  "types": "./src/index.d.ts",
50
39
  "import": "./src/index.js"
51
40
  },
52
- "./wrangler.js": {
53
- "types": "./src/wrangler.d.ts",
54
- "import": "./src/wrangler.js"
55
- },
56
- "./wrapper": {
57
- "types": "./src/wrapper.d.ts",
58
- "import": "./src/wrapper.js"
41
+ "./filesystem-assets": {
42
+ "types": "./src/filesystem-assets.d.ts",
43
+ "import": "./src/filesystem-assets.js"
59
44
  },
60
- "./wrapper.js": {
61
- "types": "./src/wrapper.d.ts",
62
- "import": "./src/wrapper.js"
45
+ "./filesystem-assets.js": {
46
+ "types": "./src/filesystem-assets.d.ts",
47
+ "import": "./src/filesystem-assets.js"
63
48
  }
64
49
  }
65
50
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * CFAssetsDirectoryHandle - FileSystemDirectoryHandle over CF ASSETS binding
3
+ *
4
+ * Wraps Cloudflare's Workers Static Assets binding to provide the standard
5
+ * File System Access API interface, enabling shovel's `self.dirs.open("dist")`
6
+ * to work seamlessly with bundled static assets.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // In production CF Worker
11
+ * const dist = new CFAssetsDirectoryHandle(env.ASSETS, "/assets");
12
+ * const file = await dist.getFileHandle("style.abc123.css");
13
+ * const content = await (await file.getFile()).text();
14
+ * ```
15
+ */
16
+ /**
17
+ * Cloudflare ASSETS binding interface
18
+ */
19
+ export interface CFAssetsBinding {
20
+ fetch(request: Request | string): Promise<Response>;
21
+ }
22
+ /**
23
+ * FileSystemDirectoryHandle implementation over Cloudflare ASSETS binding.
24
+ *
25
+ * Provides read-only access to static assets deployed with a CF Worker.
26
+ * Directory listing is not supported (ASSETS binding limitation).
27
+ */
28
+ export declare class CFAssetsDirectoryHandle implements FileSystemDirectoryHandle {
29
+ #private;
30
+ readonly kind: "directory";
31
+ readonly name: string;
32
+ constructor(assets: CFAssetsBinding, basePath?: string);
33
+ getFileHandle(name: string, _options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>;
34
+ getDirectoryHandle(name: string, _options?: FileSystemGetDirectoryOptions): Promise<FileSystemDirectoryHandle>;
35
+ removeEntry(_name: string, _options?: FileSystemRemoveOptions): Promise<void>;
36
+ resolve(_possibleDescendant: FileSystemHandle): Promise<string[] | null>;
37
+ entries(): AsyncIterableIterator<[string, FileSystemHandle]>;
38
+ keys(): AsyncIterableIterator<string>;
39
+ values(): AsyncIterableIterator<FileSystemHandle>;
40
+ [Symbol.asyncIterator](): AsyncIterableIterator<[string, FileSystemHandle]>;
41
+ isSameEntry(other: FileSystemHandle): Promise<boolean>;
42
+ }
43
+ /**
44
+ * FileSystemFileHandle implementation for CF ASSETS binding files.
45
+ */
46
+ export declare class CFAssetsFileHandle implements FileSystemFileHandle {
47
+ #private;
48
+ readonly kind: "file";
49
+ readonly name: string;
50
+ constructor(assets: CFAssetsBinding, path: string, name: string);
51
+ getFile(): Promise<File>;
52
+ createWritable(_options?: FileSystemCreateWritableOptions): Promise<FileSystemWritableFileStream>;
53
+ createSyncAccessHandle(): Promise<FileSystemSyncAccessHandle>;
54
+ isSameEntry(other: FileSystemHandle): Promise<boolean>;
55
+ }
@@ -0,0 +1,101 @@
1
+ /// <reference types="./filesystem-assets.d.ts" />
2
+ // src/filesystem-assets.ts
3
+ var CFAssetsDirectoryHandle = class _CFAssetsDirectoryHandle {
4
+ kind = "directory";
5
+ name;
6
+ #assets;
7
+ #basePath;
8
+ constructor(assets, basePath = "/") {
9
+ this.#assets = assets;
10
+ this.#basePath = basePath.endsWith("/") ? basePath : basePath + "/";
11
+ this.name = basePath.split("/").filter(Boolean).pop() || "assets";
12
+ }
13
+ async getFileHandle(name, _options) {
14
+ const path = this.#basePath + name;
15
+ const response = await this.#assets.fetch(
16
+ new Request("https://assets" + path)
17
+ );
18
+ if (!response.ok) {
19
+ throw new DOMException(
20
+ `A requested file or directory could not be found: ${name}`,
21
+ "NotFoundError"
22
+ );
23
+ }
24
+ return new CFAssetsFileHandle(this.#assets, path, name);
25
+ }
26
+ async getDirectoryHandle(name, _options) {
27
+ return new _CFAssetsDirectoryHandle(this.#assets, this.#basePath + name);
28
+ }
29
+ async removeEntry(_name, _options) {
30
+ throw new DOMException("Assets directory is read-only", "NotAllowedError");
31
+ }
32
+ async resolve(_possibleDescendant) {
33
+ return null;
34
+ }
35
+ async *entries() {
36
+ throw new DOMException(
37
+ "Directory listing not supported for ASSETS binding. Use an asset manifest for enumeration.",
38
+ "NotSupportedError"
39
+ );
40
+ }
41
+ async *keys() {
42
+ throw new DOMException(
43
+ "Directory listing not supported for ASSETS binding",
44
+ "NotSupportedError"
45
+ );
46
+ }
47
+ async *values() {
48
+ throw new DOMException(
49
+ "Directory listing not supported for ASSETS binding",
50
+ "NotSupportedError"
51
+ );
52
+ }
53
+ [Symbol.asyncIterator]() {
54
+ return this.entries();
55
+ }
56
+ isSameEntry(other) {
57
+ return Promise.resolve(
58
+ other instanceof _CFAssetsDirectoryHandle && other.#basePath === this.#basePath
59
+ );
60
+ }
61
+ };
62
+ var CFAssetsFileHandle = class _CFAssetsFileHandle {
63
+ kind = "file";
64
+ name;
65
+ #assets;
66
+ #path;
67
+ constructor(assets, path, name) {
68
+ this.#assets = assets;
69
+ this.#path = path;
70
+ this.name = name;
71
+ }
72
+ async getFile() {
73
+ const response = await this.#assets.fetch(
74
+ new Request("https://assets" + this.#path)
75
+ );
76
+ if (!response.ok) {
77
+ throw new DOMException(
78
+ `A requested file or directory could not be found: ${this.name}`,
79
+ "NotFoundError"
80
+ );
81
+ }
82
+ const blob = await response.blob();
83
+ const contentType = response.headers.get("content-type") || "application/octet-stream";
84
+ return new File([blob], this.name, { type: contentType });
85
+ }
86
+ async createWritable(_options) {
87
+ throw new DOMException("Assets are read-only", "NotAllowedError");
88
+ }
89
+ async createSyncAccessHandle() {
90
+ throw new DOMException("Sync access not supported", "NotSupportedError");
91
+ }
92
+ isSameEntry(other) {
93
+ return Promise.resolve(
94
+ other instanceof _CFAssetsFileHandle && other.#path === this.#path
95
+ );
96
+ }
97
+ };
98
+ export {
99
+ CFAssetsDirectoryHandle,
100
+ CFAssetsFileHandle
101
+ };
package/src/index.d.ts CHANGED
@@ -3,7 +3,70 @@
3
3
  *
4
4
  * Provides ServiceWorker-native deployment for Cloudflare Workers with KV/R2/D1 integration.
5
5
  */
6
- export { CloudflarePlatform, createCloudflarePlatform, type CloudflarePlatformOptions, } from "./platform.js";
7
- export { createOptionsFromEnv, generateWranglerConfig } from "./wrangler.js";
8
- export { cloudflareWorkerBanner, cloudflareWorkerFooter } from "./wrapper.js";
9
- export type { Platform, CacheConfig, StaticConfig, Handler, Server, ServerOptions, ServiceWorkerOptions, ServiceWorkerInstance, } from "@b9g/platform";
6
+ import { BasePlatform, PlatformConfig, Handler, Server, ServerOptions, ServiceWorkerOptions, ServiceWorkerInstance } from "@b9g/platform";
7
+ export type { Platform, Handler, Server, ServerOptions, ServiceWorkerOptions, ServiceWorkerInstance, } from "@b9g/platform";
8
+ export interface CloudflarePlatformOptions extends PlatformConfig {
9
+ /** Cloudflare Workers environment (production, preview, dev) */
10
+ environment?: "production" | "preview" | "dev";
11
+ /** Static assets directory for ASSETS binding (dev mode) */
12
+ 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>;
21
+ }
22
+ /**
23
+ * Cloudflare Workers platform implementation
24
+ */
25
+ export declare class CloudflarePlatform extends BasePlatform {
26
+ #private;
27
+ readonly name: string;
28
+ constructor(options?: CloudflarePlatformOptions);
29
+ /**
30
+ * Create "server" for Cloudflare Workers (which is really just the handler)
31
+ */
32
+ createServer(handler: Handler, _options?: ServerOptions): Server;
33
+ /**
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
38
+ */
39
+ loadServiceWorker(entrypoint: string, _options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
40
+ /**
41
+ * Dispose of platform resources
42
+ */
43
+ dispose(): Promise<void>;
44
+ }
45
+ /**
46
+ * Create platform options from Wrangler environment
47
+ */
48
+ export declare function createOptionsFromEnv(env: any): CloudflarePlatformOptions;
49
+ /**
50
+ * Generate wrangler.toml configuration for a Shovel app from CLI flags
51
+ */
52
+ export declare function generateWranglerConfig(options: {
53
+ name: string;
54
+ entrypoint: string;
55
+ cacheAdapter?: string;
56
+ filesystemAdapter?: string;
57
+ kvNamespaces?: string[];
58
+ r2Buckets?: string[];
59
+ d1Databases?: string[];
60
+ }): 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// 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});\n\t\t\t\t\tlogger.error(\"Error stack\", {stack: error.stack});\n\t\t\t\t\t// Return detailed error in response body for debugging\n\t\t\t\t\treturn new Response(JSON.stringify({\n\t\t\t\t\t\terror: error.message,\n\t\t\t\t\t\tstack: error.stack,\n\t\t\t\t\t\tname: error.name,\n\t\t\t\t\t\turl: request.url\n\t\t\t\t\t}, null, 2), { \n\t\t\t\t\t\tstatus: 500,\n\t\t\t\t\t\theaders: { 'Content-Type': 'application/json' }\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\t\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: topLevelError});\n\t\t\treturn new Response(JSON.stringify({\n\t\t\t\terror: 'Top-level wrapper error: ' + topLevelError.message,\n\t\t\t\tstack: topLevelError.stack,\n\t\t\t\tname: topLevelError.name,\n\t\t\t\turl: request?.url || 'unknown'\n\t\t\t}, null, 2), { \n\t\t\t\tstatus: 500,\n\t\t\t\theaders: { 'Content-Type': 'application/json' }\n\t\t\t});\n\t\t}\n\t}\n};";
69
+ /**
70
+ * Default export for easy importing
71
+ */
72
+ export default CloudflarePlatform;
package/src/index.js CHANGED
@@ -1,16 +1,366 @@
1
1
  /// <reference types="./index.d.ts" />
2
2
  // src/index.ts
3
3
  import {
4
- CloudflarePlatform,
5
- createCloudflarePlatform
6
- } from "./platform.js";
7
- import { createOptionsFromEnv, generateWranglerConfig } from "./wrangler.js";
8
- import { cloudflareWorkerBanner, cloudflareWorkerFooter } from "./wrapper.js";
4
+ BasePlatform
5
+ } from "@b9g/platform";
6
+ import { getLogger } from "@logtape/logtape";
7
+ var logger = getLogger(["platform-cloudflare"]);
8
+ var CloudflarePlatform = class extends BasePlatform {
9
+ name;
10
+ #options;
11
+ #miniflare = null;
12
+ #assetsMiniflare = null;
13
+ // Separate instance for ASSETS binding
14
+ #assetsBinding = null;
15
+ constructor(options = {}) {
16
+ super(options);
17
+ this.name = "cloudflare";
18
+ this.#options = {
19
+ environment: "production",
20
+ assetsDirectory: void 0,
21
+ kvNamespaces: {},
22
+ r2Buckets: {},
23
+ d1Databases: {},
24
+ durableObjects: {},
25
+ ...options
26
+ };
27
+ }
28
+ /**
29
+ * Create "server" for Cloudflare Workers (which is really just the handler)
30
+ */
31
+ createServer(handler, _options = {}) {
32
+ return {
33
+ async listen() {
34
+ logger.info("Worker handler ready", {});
35
+ },
36
+ async close() {
37
+ logger.info("Worker handler stopped", {});
38
+ },
39
+ address: () => ({ port: 443, host: "cloudflare-workers" }),
40
+ get url() {
41
+ return "https://cloudflare-workers";
42
+ },
43
+ get ready() {
44
+ return true;
45
+ }
46
+ };
47
+ }
48
+ /**
49
+ * Load ServiceWorker-style entrypoint in Cloudflare Workers
50
+ *
51
+ * In production: Uses the native CF Worker environment
52
+ * In dev mode: Uses miniflare (workerd) for true dev/prod parity
53
+ */
54
+ async loadServiceWorker(entrypoint, _options = {}) {
55
+ const isCloudflareWorker = typeof globalThis.addEventListener === "function" && typeof globalThis.caches !== "undefined" && typeof globalThis.FetchEvent !== "undefined";
56
+ if (isCloudflareWorker) {
57
+ logger.info("Running in native ServiceWorker environment", {});
58
+ const instance = {
59
+ runtime: globalThis,
60
+ handleRequest: async (request) => {
61
+ const event = new FetchEvent("fetch", { request });
62
+ globalThis.dispatchEvent(event);
63
+ return new Response("Worker handler", { status: 200 });
64
+ },
65
+ install: () => Promise.resolve(),
66
+ activate: () => Promise.resolve(),
67
+ get ready() {
68
+ return true;
69
+ },
70
+ dispose: async () => {
71
+ }
72
+ };
73
+ await import(entrypoint);
74
+ return instance;
75
+ } else {
76
+ return this.#loadServiceWorkerWithMiniflare(entrypoint);
77
+ }
78
+ }
79
+ /**
80
+ * Load ServiceWorker using miniflare (workerd) for dev mode
81
+ */
82
+ async #loadServiceWorkerWithMiniflare(entrypoint) {
83
+ logger.info("Starting miniflare dev server", { entrypoint });
84
+ const { Miniflare } = await import("miniflare");
85
+ const miniflareOptions = {
86
+ modules: false,
87
+ // ServiceWorker format (not ES modules)
88
+ scriptPath: entrypoint,
89
+ // Enable CF-compatible APIs
90
+ compatibilityDate: "2024-09-23",
91
+ compatibilityFlags: ["nodejs_compat"]
92
+ };
93
+ this.#miniflare = new Miniflare(miniflareOptions);
94
+ await this.#miniflare.ready;
95
+ if (this.#options.assetsDirectory) {
96
+ logger.info("Setting up separate ASSETS binding", {
97
+ directory: this.#options.assetsDirectory
98
+ });
99
+ this.#assetsMiniflare = new Miniflare({
100
+ modules: true,
101
+ script: `export default { fetch() { return new Response("assets-only"); } }`,
102
+ assets: {
103
+ directory: this.#options.assetsDirectory,
104
+ binding: "ASSETS"
105
+ },
106
+ compatibilityDate: "2024-09-23"
107
+ });
108
+ const assetsEnv = await this.#assetsMiniflare.getBindings();
109
+ if (assetsEnv.ASSETS) {
110
+ this.#assetsBinding = assetsEnv.ASSETS;
111
+ logger.info("ASSETS binding available", {});
112
+ }
113
+ }
114
+ const mf = this.#miniflare;
115
+ const instance = {
116
+ runtime: mf,
117
+ handleRequest: async (request) => {
118
+ return mf.dispatchFetch(request);
119
+ },
120
+ install: () => Promise.resolve(),
121
+ activate: () => Promise.resolve(),
122
+ get ready() {
123
+ return true;
124
+ },
125
+ dispose: async () => {
126
+ await mf.dispose();
127
+ }
128
+ };
129
+ logger.info("Miniflare dev server ready", {});
130
+ return instance;
131
+ }
132
+ /**
133
+ * Dispose of platform resources
134
+ */
135
+ async dispose() {
136
+ if (this.#miniflare) {
137
+ await this.#miniflare.dispose();
138
+ this.#miniflare = null;
139
+ }
140
+ if (this.#assetsMiniflare) {
141
+ await this.#assetsMiniflare.dispose();
142
+ this.#assetsMiniflare = null;
143
+ }
144
+ this.#assetsBinding = null;
145
+ }
146
+ };
147
+ function createOptionsFromEnv(env) {
148
+ return {
149
+ environment: env.ENVIRONMENT || "production",
150
+ kvNamespaces: extractKVNamespaces(env),
151
+ r2Buckets: extractR2Buckets(env),
152
+ d1Databases: extractD1Databases(env),
153
+ durableObjects: extractDurableObjects(env)
154
+ };
155
+ }
156
+ function extractKVNamespaces(env) {
157
+ const kvNamespaces = {};
158
+ for (const [key, value] of Object.entries(env)) {
159
+ if (key.endsWith("_KV") || key.includes("KV")) {
160
+ kvNamespaces[key] = value;
161
+ }
162
+ }
163
+ return kvNamespaces;
164
+ }
165
+ function extractR2Buckets(env) {
166
+ const r2Buckets = {};
167
+ for (const [key, value] of Object.entries(env)) {
168
+ if (key.endsWith("_R2") || key.includes("R2")) {
169
+ r2Buckets[key] = value;
170
+ }
171
+ }
172
+ return r2Buckets;
173
+ }
174
+ function extractD1Databases(env) {
175
+ const d1Databases = {};
176
+ for (const [key, value] of Object.entries(env)) {
177
+ if (key.endsWith("_D1") || key.includes("D1") || key.endsWith("_DB")) {
178
+ d1Databases[key] = value;
179
+ }
180
+ }
181
+ return d1Databases;
182
+ }
183
+ function extractDurableObjects(env) {
184
+ const durableObjects = {};
185
+ for (const [key, value] of Object.entries(env)) {
186
+ if (key.endsWith("_DO") || key.includes("DURABLE")) {
187
+ durableObjects[key] = value;
188
+ }
189
+ }
190
+ return durableObjects;
191
+ }
192
+ function generateWranglerConfig(options) {
193
+ const {
194
+ name,
195
+ entrypoint,
196
+ cacheAdapter: _cacheAdapter,
197
+ filesystemAdapter,
198
+ kvNamespaces = [],
199
+ r2Buckets = [],
200
+ d1Databases = []
201
+ } = options;
202
+ const autoKVNamespaces = [];
203
+ const autoR2Buckets = filesystemAdapter === "r2" ? ["STORAGE_R2"] : [];
204
+ const allKVNamespaces = [.../* @__PURE__ */ new Set([...kvNamespaces, ...autoKVNamespaces])];
205
+ const allR2Buckets = [.../* @__PURE__ */ new Set([...r2Buckets, ...autoR2Buckets])];
206
+ return `# Generated wrangler.toml for Shovel app
207
+ name = "${name}"
208
+ main = "${entrypoint}"
209
+ compatibility_date = "2024-09-23"
210
+ compatibility_flags = ["nodejs_compat"]
211
+
212
+ # ServiceWorker format (since Shovel apps are ServiceWorker-style)
213
+ usage_model = "bundled"
214
+
215
+ # KV bindings${allKVNamespaces.length > 0 ? "\n" + allKVNamespaces.map(
216
+ (kv) => `[[kv_namespaces]]
217
+ binding = "${kv}"
218
+ id = "your-kv-namespace-id"
219
+ preview_id = "your-preview-kv-namespace-id"`
220
+ ).join("\n\n") : ""}
221
+
222
+ # R2 bindings${allR2Buckets.length > 0 ? "\n" + allR2Buckets.map(
223
+ (bucket) => `[[r2_buckets]]
224
+ binding = "${bucket}"
225
+ bucket_name = "your-bucket-name"`
226
+ ).join("\n\n") : ""}
227
+
228
+ # D1 bindings
229
+ ${d1Databases.map(
230
+ (db) => `[[d1_databases]]
231
+ binding = "${db}"
232
+ database_name = "your-database-name"
233
+ database_id = "your-database-id"`
234
+ ).join("\n\n")}
235
+ `;
236
+ }
237
+ var cloudflareWorkerBanner = `// Cloudflare Worker ES Module wrapper
238
+ let serviceWorkerGlobals = null;
239
+
240
+ // Set up ServiceWorker environment
241
+ if (typeof globalThis.self === 'undefined') {
242
+ globalThis.self = globalThis;
243
+ }
244
+
245
+ // Capture fetch event handlers
246
+ const fetchHandlers = [];
247
+ const originalAddEventListener = globalThis.addEventListener;
248
+ globalThis.addEventListener = function(type, handler, options) {
249
+ if (type === 'fetch') {
250
+ fetchHandlers.push(handler);
251
+ } else {
252
+ originalAddEventListener?.call(this, type, handler, options);
253
+ }
254
+ };
255
+
256
+ // Create a promise-based FetchEvent that can be awaited
257
+ class FetchEvent {
258
+ constructor(type, init) {
259
+ this.type = type;
260
+ this.request = init.request;
261
+ this._response = null;
262
+ this._responsePromise = new Promise((resolve) => {
263
+ this._resolveResponse = resolve;
264
+ });
265
+ }
266
+
267
+ respondWith(response) {
268
+ this._response = response;
269
+ this._resolveResponse(response);
270
+ }
271
+
272
+ async waitUntil(promise) {
273
+ await promise;
274
+ }
275
+ }`;
276
+ var cloudflareWorkerFooter = `
277
+ // Export ES Module for Cloudflare Workers
278
+ export default {
279
+ async fetch(request, env, ctx) {
280
+ try {
281
+ // Set up ServiceWorker-like dirs API for bundled deployment
282
+ if (!globalThis.self.dirs) {
283
+ // For bundled deployment, assets are served via static middleware
284
+ // not through the dirs API
285
+ globalThis.self.dirs = {
286
+ async open(directoryName) {
287
+ if (directoryName === 'assets') {
288
+ // Return a minimal interface that indicates no files available
289
+ // The assets middleware will fall back to dev mode behavior
290
+ return {
291
+ async getFileHandle(fileName) {
292
+ throw new Error(\`NotFoundError: \${fileName} not found in bundled assets\`);
293
+ }
294
+ };
295
+ }
296
+ throw new Error(\`Directory \${directoryName} not available in bundled deployment\`);
297
+ }
298
+ };
299
+ }
300
+
301
+ // Set up caches API
302
+ if (!globalThis.self.caches) {
303
+ globalThis.self.caches = globalThis.caches;
304
+ }
305
+
306
+ // Ensure request.url is a string
307
+ if (typeof request.url !== 'string') {
308
+ return new Response('Invalid request URL: ' + typeof request.url, { status: 500 });
309
+ }
310
+
311
+ // Create proper FetchEvent-like object
312
+ let responseReceived = null;
313
+ const event = {
314
+ request,
315
+ respondWith: (response) => { responseReceived = response; }
316
+ };
317
+
318
+ // Dispatch to ServiceWorker fetch handlers
319
+ for (const handler of fetchHandlers) {
320
+ try {
321
+ logger.debug("Calling handler", {url: request.url});
322
+ await handler(event);
323
+ logger.debug("Handler completed", {hasResponse: !!responseReceived});
324
+ if (responseReceived) {
325
+ return responseReceived;
326
+ }
327
+ } catch (error) {
328
+ logger.error("Handler error", {error});
329
+ logger.error("Error stack", {stack: error.stack});
330
+ // Return detailed error in response body for debugging
331
+ return new Response(JSON.stringify({
332
+ error: error.message,
333
+ stack: error.stack,
334
+ name: error.name,
335
+ url: request.url
336
+ }, null, 2), {
337
+ status: 500,
338
+ headers: { 'Content-Type': 'application/json' }
339
+ });
340
+ }
341
+ }
342
+
343
+ return new Response('No ServiceWorker handler', { status: 404 });
344
+ } catch (topLevelError) {
345
+ logger.error("Top-level error", {error: topLevelError});
346
+ return new Response(JSON.stringify({
347
+ error: 'Top-level wrapper error: ' + topLevelError.message,
348
+ stack: topLevelError.stack,
349
+ name: topLevelError.name,
350
+ url: request?.url || 'unknown'
351
+ }, null, 2), {
352
+ status: 500,
353
+ headers: { 'Content-Type': 'application/json' }
354
+ });
355
+ }
356
+ }
357
+ };`;
358
+ var src_default = CloudflarePlatform;
9
359
  export {
10
360
  CloudflarePlatform,
11
361
  cloudflareWorkerBanner,
12
362
  cloudflareWorkerFooter,
13
- createCloudflarePlatform,
14
363
  createOptionsFromEnv,
364
+ src_default as default,
15
365
  generateWranglerConfig
16
366
  };
package/src/platform.d.ts DELETED
@@ -1,66 +0,0 @@
1
- /**
2
- * Cloudflare Workers platform implementation for Shovel
3
- *
4
- * Uses bundled adapters to avoid dynamic imports in Workers environment
5
- * Supports KV for caching and R2 for filesystem operations
6
- */
7
- import { BasePlatform, PlatformConfig, CacheConfig, Handler, Server, ServerOptions, ServiceWorkerOptions, ServiceWorkerInstance } from "@b9g/platform";
8
- export interface CloudflarePlatformOptions extends PlatformConfig {
9
- /** Cloudflare Workers environment (production, preview, dev) */
10
- environment?: "production" | "preview" | "dev";
11
- /** KV namespace bindings */
12
- kvNamespaces?: Record<string, any>;
13
- /** R2 bucket bindings */
14
- r2Buckets?: Record<string, any>;
15
- /** D1 database bindings */
16
- d1Databases?: Record<string, any>;
17
- /** Durable Object bindings */
18
- durableObjects?: Record<string, any>;
19
- }
20
- /**
21
- * Cloudflare Workers platform implementation
22
- */
23
- export declare class CloudflarePlatform extends BasePlatform {
24
- readonly name = "cloudflare";
25
- private options;
26
- constructor(options?: CloudflarePlatformOptions);
27
- /**
28
- * Get filesystem directory handle (memory-only in Workers runtime)
29
- */
30
- getDirectoryHandle(name: string): Promise<FileSystemDirectoryHandle>;
31
- /**
32
- * Get platform-specific default cache configuration for Cloudflare Workers
33
- */
34
- protected getDefaultCacheConfig(): CacheConfig;
35
- /**
36
- * Override cache creation to use bundled KV adapter
37
- */
38
- createCaches(config?: CacheConfig): Promise<CacheStorage>;
39
- /**
40
- * Create "server" for Cloudflare Workers (which is really just the handler)
41
- */
42
- createServer(handler: Handler, _options?: ServerOptions): Server;
43
- /**
44
- * Load ServiceWorker-style entrypoint in Cloudflare Workers
45
- *
46
- * Cloudflare Workers are already ServiceWorker-based, so we can use
47
- * the global environment directly in production
48
- */
49
- loadServiceWorker(entrypoint: string, options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
50
- /**
51
- * Get filesystem root for File System Access API
52
- */
53
- getFileSystemRoot(name?: string): Promise<FileSystemDirectoryHandle>;
54
- /**
55
- * Dispose of platform resources
56
- */
57
- dispose(): Promise<void>;
58
- }
59
- /**
60
- * Create a Cloudflare platform instance
61
- */
62
- export declare function createCloudflarePlatform(options?: CloudflarePlatformOptions): CloudflarePlatform;
63
- /**
64
- * Default export for easy importing
65
- */
66
- export default createCloudflarePlatform;
package/src/platform.js DELETED
@@ -1,137 +0,0 @@
1
- /// <reference types="./platform.d.ts" />
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined")
6
- return require.apply(this, arguments);
7
- throw Error('Dynamic require of "' + x + '" is not supported');
8
- });
9
-
10
- // src/platform.ts
11
- import {
12
- BasePlatform
13
- } from "@b9g/platform";
14
- import { FileSystemRegistry, getFileSystemRoot, MemoryBucket } from "@b9g/filesystem";
15
- var CloudflarePlatform = class extends BasePlatform {
16
- name = "cloudflare";
17
- options;
18
- constructor(options = {}) {
19
- super(options);
20
- this.options = {
21
- environment: "production",
22
- kvNamespaces: {},
23
- r2Buckets: {},
24
- d1Databases: {},
25
- durableObjects: {},
26
- ...options
27
- };
28
- FileSystemRegistry.register("memory", new MemoryBucket());
29
- if (this.options.r2Buckets?.default) {
30
- try {
31
- const { R2FileSystemAdapter } = __require("@b9g/filesystem-r2");
32
- FileSystemRegistry.register("r2", new R2FileSystemAdapter(this.options.r2Buckets.default));
33
- } catch {
34
- console.warn("[Cloudflare] R2 adapter not available, using memory filesystem");
35
- }
36
- }
37
- }
38
- /**
39
- * Get filesystem directory handle (memory-only in Workers runtime)
40
- */
41
- async getDirectoryHandle(name) {
42
- const adapter = new MemoryBucket();
43
- return await adapter.getDirectoryHandle(name);
44
- }
45
- /**
46
- * Get platform-specific default cache configuration for Cloudflare Workers
47
- */
48
- getDefaultCacheConfig() {
49
- return {
50
- pages: { type: "cloudflare" },
51
- // Use Cloudflare's native Cache API
52
- api: { type: "cloudflare" },
53
- // Use Cloudflare's native Cache API
54
- static: { type: "cloudflare" }
55
- // Static files handled by CDN
56
- };
57
- }
58
- /**
59
- * Override cache creation to use bundled KV adapter
60
- */
61
- async createCaches(config) {
62
- return globalThis.caches;
63
- }
64
- /**
65
- * Create "server" for Cloudflare Workers (which is really just the handler)
66
- */
67
- createServer(handler, _options = {}) {
68
- return {
69
- async listen() {
70
- console.info("[Cloudflare] Worker handler ready");
71
- },
72
- async close() {
73
- console.info("[Cloudflare] Worker handler stopped");
74
- },
75
- address: () => ({ port: 443, host: "cloudflare-workers" }),
76
- get url() {
77
- return "https://cloudflare-workers";
78
- },
79
- get ready() {
80
- return true;
81
- }
82
- };
83
- }
84
- /**
85
- * Load ServiceWorker-style entrypoint in Cloudflare Workers
86
- *
87
- * Cloudflare Workers are already ServiceWorker-based, so we can use
88
- * the global environment directly in production
89
- */
90
- async loadServiceWorker(entrypoint, options = {}) {
91
- const isCloudflareWorker = typeof globalThis.addEventListener === "function" && typeof globalThis.caches !== "undefined" && typeof globalThis.FetchEvent !== "undefined";
92
- if (isCloudflareWorker) {
93
- console.info("[Cloudflare] Running in native ServiceWorker environment");
94
- const instance = {
95
- runtime: globalThis,
96
- handleRequest: async (request) => {
97
- const event = new FetchEvent("fetch", { request });
98
- globalThis.dispatchEvent(event);
99
- return new Response("Worker handler", { status: 200 });
100
- },
101
- install: () => Promise.resolve(),
102
- activate: () => Promise.resolve(),
103
- collectStaticRoutes: async () => [],
104
- // Not supported in Workers
105
- get ready() {
106
- return true;
107
- },
108
- dispose: async () => {
109
- }
110
- };
111
- await import(entrypoint);
112
- return instance;
113
- } else {
114
- throw new Error("Cloudflare platform development mode not yet implemented. Use Node platform for development.");
115
- }
116
- }
117
- /**
118
- * Get filesystem root for File System Access API
119
- */
120
- async getFileSystemRoot(name = "default") {
121
- return await getFileSystemRoot(name);
122
- }
123
- /**
124
- * Dispose of platform resources
125
- */
126
- async dispose() {
127
- }
128
- };
129
- function createCloudflarePlatform(options) {
130
- return new CloudflarePlatform(options);
131
- }
132
- var platform_default = createCloudflarePlatform;
133
- export {
134
- CloudflarePlatform,
135
- createCloudflarePlatform,
136
- platform_default as default
137
- };
package/src/wrangler.d.ts DELETED
@@ -1,20 +0,0 @@
1
- /**
2
- * Wrangler integration utilities for Cloudflare Workers
3
- */
4
- import type { CloudflarePlatformOptions } from "./platform.js";
5
- /**
6
- * Create platform options from Wrangler environment
7
- */
8
- export declare function createOptionsFromEnv(env: any): CloudflarePlatformOptions;
9
- /**
10
- * Generate wrangler.toml configuration for a Shovel app from CLI flags
11
- */
12
- export declare function generateWranglerConfig(options: {
13
- name: string;
14
- entrypoint: string;
15
- cacheAdapter?: string;
16
- filesystemAdapter?: string;
17
- kvNamespaces?: string[];
18
- r2Buckets?: string[];
19
- d1Databases?: string[];
20
- }): string;
package/src/wrangler.js DELETED
@@ -1,95 +0,0 @@
1
- /// <reference types="./wrangler.d.ts" />
2
- // src/wrangler.ts
3
- function createOptionsFromEnv(env) {
4
- return {
5
- environment: env.ENVIRONMENT || "production",
6
- kvNamespaces: extractKVNamespaces(env),
7
- r2Buckets: extractR2Buckets(env),
8
- d1Databases: extractD1Databases(env),
9
- durableObjects: extractDurableObjects(env)
10
- };
11
- }
12
- function extractKVNamespaces(env) {
13
- const kvNamespaces = {};
14
- for (const [key, value] of Object.entries(env)) {
15
- if (key.endsWith("_KV") || key.includes("KV")) {
16
- kvNamespaces[key] = value;
17
- }
18
- }
19
- return kvNamespaces;
20
- }
21
- function extractR2Buckets(env) {
22
- const r2Buckets = {};
23
- for (const [key, value] of Object.entries(env)) {
24
- if (key.endsWith("_R2") || key.includes("R2")) {
25
- r2Buckets[key] = value;
26
- }
27
- }
28
- return r2Buckets;
29
- }
30
- function extractD1Databases(env) {
31
- const d1Databases = {};
32
- for (const [key, value] of Object.entries(env)) {
33
- if (key.endsWith("_D1") || key.includes("D1") || key.endsWith("_DB")) {
34
- d1Databases[key] = value;
35
- }
36
- }
37
- return d1Databases;
38
- }
39
- function extractDurableObjects(env) {
40
- const durableObjects = {};
41
- for (const [key, value] of Object.entries(env)) {
42
- if (key.endsWith("_DO") || key.includes("DURABLE")) {
43
- durableObjects[key] = value;
44
- }
45
- }
46
- return durableObjects;
47
- }
48
- function generateWranglerConfig(options) {
49
- const {
50
- name,
51
- entrypoint,
52
- cacheAdapter,
53
- filesystemAdapter,
54
- kvNamespaces = [],
55
- r2Buckets = [],
56
- d1Databases = []
57
- } = options;
58
- const autoKVNamespaces = [];
59
- const autoR2Buckets = filesystemAdapter === "r2" ? ["STORAGE_R2"] : [];
60
- const allKVNamespaces = [.../* @__PURE__ */ new Set([...kvNamespaces, ...autoKVNamespaces])];
61
- const allR2Buckets = [.../* @__PURE__ */ new Set([...r2Buckets, ...autoR2Buckets])];
62
- return `# Generated wrangler.toml for Shovel app
63
- name = "${name}"
64
- main = "${entrypoint}"
65
- compatibility_date = "2024-01-01"
66
-
67
- # ServiceWorker format (since Shovel apps are ServiceWorker-style)
68
- usage_model = "bundled"
69
-
70
- # KV bindings${allKVNamespaces.length > 0 ? "\n" + allKVNamespaces.map(
71
- (kv) => `[[kv_namespaces]]
72
- binding = "${kv}"
73
- id = "your-kv-namespace-id"
74
- preview_id = "your-preview-kv-namespace-id"`
75
- ).join("\n\n") : ""}
76
-
77
- # R2 bindings${allR2Buckets.length > 0 ? "\n" + allR2Buckets.map(
78
- (bucket) => `[[r2_buckets]]
79
- binding = "${bucket}"
80
- bucket_name = "your-bucket-name"`
81
- ).join("\n\n") : ""}
82
-
83
- # D1 bindings
84
- ${d1Databases.map(
85
- (db) => `[[d1_databases]]
86
- binding = "${db}"
87
- database_name = "your-database-name"
88
- database_id = "your-database-id"`
89
- ).join("\n\n")}
90
- `;
91
- }
92
- export {
93
- createOptionsFromEnv,
94
- generateWranglerConfig
95
- };
package/src/wrapper.d.ts DELETED
@@ -1,12 +0,0 @@
1
- /**
2
- * Cloudflare Workers ES Module wrapper
3
- * Converts ServiceWorker code to ES Module format for Cloudflare Workers
4
- */
5
- /**
6
- * Generate banner code for ServiceWorker → ES Module conversion
7
- */
8
- 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}";
9
- /**
10
- * Generate footer code for ServiceWorker → ES Module conversion
11
- */
12
- 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// Dispatch to ServiceWorker fetch handlers\n\t\t\tfor (const handler of fetchHandlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconsole.log('[Wrapper] Calling handler for:', request.url);\n\t\t\t\t\tawait handler(event);\n\t\t\t\t\tconsole.log('[Wrapper] Handler completed, response:', !!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\tconsole.error('[Wrapper] Handler error:', error);\n\t\t\t\t\tconsole.error('[Wrapper] Error stack:', error.stack);\n\t\t\t\t\t// Return detailed error in response body for debugging\n\t\t\t\t\treturn new Response(JSON.stringify({\n\t\t\t\t\t\terror: error.message,\n\t\t\t\t\t\tstack: error.stack,\n\t\t\t\t\t\tname: error.name,\n\t\t\t\t\t\turl: request.url\n\t\t\t\t\t}, null, 2), { \n\t\t\t\t\t\tstatus: 500,\n\t\t\t\t\t\theaders: { 'Content-Type': 'application/json' }\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\treturn new Response('No ServiceWorker handler', { status: 404 });\n\t\t} catch (topLevelError) {\n\t\t\tconsole.error('[Wrapper] Top-level error:', topLevelError);\n\t\t\treturn new Response(JSON.stringify({\n\t\t\t\terror: 'Top-level wrapper error: ' + topLevelError.message,\n\t\t\t\tstack: topLevelError.stack,\n\t\t\t\tname: topLevelError.name,\n\t\t\t\turl: request?.url || 'unknown'\n\t\t\t}, null, 2), { \n\t\t\t\tstatus: 500,\n\t\t\t\theaders: { 'Content-Type': 'application/json' }\n\t\t\t});\n\t\t}\n\t}\n};";
package/src/wrapper.js DELETED
@@ -1,127 +0,0 @@
1
- /// <reference types="./wrapper.d.ts" />
2
- // src/wrapper.ts
3
- var cloudflareWorkerBanner = `// Cloudflare Worker ES Module wrapper
4
- let serviceWorkerGlobals = null;
5
-
6
- // Set up ServiceWorker environment
7
- if (typeof globalThis.self === 'undefined') {
8
- globalThis.self = globalThis;
9
- }
10
-
11
- // Capture fetch event handlers
12
- const fetchHandlers = [];
13
- const originalAddEventListener = globalThis.addEventListener;
14
- globalThis.addEventListener = function(type, handler, options) {
15
- if (type === 'fetch') {
16
- fetchHandlers.push(handler);
17
- } else {
18
- originalAddEventListener?.call(this, type, handler, options);
19
- }
20
- };
21
-
22
- // Create a promise-based FetchEvent that can be awaited
23
- class FetchEvent {
24
- constructor(type, init) {
25
- this.type = type;
26
- this.request = init.request;
27
- this._response = null;
28
- this._responsePromise = new Promise((resolve) => {
29
- this._resolveResponse = resolve;
30
- });
31
- }
32
-
33
- respondWith(response) {
34
- this._response = response;
35
- this._resolveResponse(response);
36
- }
37
-
38
- async waitUntil(promise) {
39
- await promise;
40
- }
41
- }`;
42
- var cloudflareWorkerFooter = `
43
- // Export ES Module for Cloudflare Workers
44
- export default {
45
- async fetch(request, env, ctx) {
46
- try {
47
- // Set up ServiceWorker-like dirs API for bundled deployment
48
- if (!globalThis.self.dirs) {
49
- // For bundled deployment, assets are served via static middleware
50
- // not through the dirs API
51
- globalThis.self.dirs = {
52
- async open(directoryName) {
53
- if (directoryName === 'assets') {
54
- // Return a minimal interface that indicates no files available
55
- // The assets middleware will fall back to dev mode behavior
56
- return {
57
- async getFileHandle(fileName) {
58
- throw new Error(\`NotFoundError: \${fileName} not found in bundled assets\`);
59
- }
60
- };
61
- }
62
- throw new Error(\`Directory \${directoryName} not available in bundled deployment\`);
63
- }
64
- };
65
- }
66
-
67
- // Set up caches API
68
- if (!globalThis.self.caches) {
69
- globalThis.self.caches = globalThis.caches;
70
- }
71
-
72
- // Ensure request.url is a string
73
- if (typeof request.url !== 'string') {
74
- return new Response('Invalid request URL: ' + typeof request.url, { status: 500 });
75
- }
76
-
77
- // Create proper FetchEvent-like object
78
- let responseReceived = null;
79
- const event = {
80
- request,
81
- respondWith: (response) => { responseReceived = response; }
82
- };
83
-
84
- // Dispatch to ServiceWorker fetch handlers
85
- for (const handler of fetchHandlers) {
86
- try {
87
- console.log('[Wrapper] Calling handler for:', request.url);
88
- await handler(event);
89
- console.log('[Wrapper] Handler completed, response:', !!responseReceived);
90
- if (responseReceived) {
91
- return responseReceived;
92
- }
93
- } catch (error) {
94
- console.error('[Wrapper] Handler error:', error);
95
- console.error('[Wrapper] Error stack:', error.stack);
96
- // Return detailed error in response body for debugging
97
- return new Response(JSON.stringify({
98
- error: error.message,
99
- stack: error.stack,
100
- name: error.name,
101
- url: request.url
102
- }, null, 2), {
103
- status: 500,
104
- headers: { 'Content-Type': 'application/json' }
105
- });
106
- }
107
- }
108
-
109
- return new Response('No ServiceWorker handler', { status: 404 });
110
- } catch (topLevelError) {
111
- console.error('[Wrapper] Top-level error:', topLevelError);
112
- return new Response(JSON.stringify({
113
- error: 'Top-level wrapper error: ' + topLevelError.message,
114
- stack: topLevelError.stack,
115
- name: topLevelError.name,
116
- url: request?.url || 'unknown'
117
- }, null, 2), {
118
- status: 500,
119
- headers: { 'Content-Type': 'application/json' }
120
- });
121
- }
122
- }
123
- };`;
124
- export {
125
- cloudflareWorkerBanner,
126
- cloudflareWorkerFooter
127
- };