@b9g/platform-cloudflare 0.1.4 → 0.1.6

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.4",
3
+ "version": "0.1.6",
4
4
  "description": "Cloudflare Workers platform adapter for Shovel - already ServiceWorker-based!",
5
5
  "keywords": [
6
6
  "shovel",
@@ -11,10 +11,11 @@
11
11
  "serviceworker"
12
12
  ],
13
13
  "dependencies": {
14
- "@b9g/platform": "^0.1.4",
15
- "@b9g/cache": "^0.1.3",
16
- "@b9g/assets": "^0.1.4",
17
- "@cloudflare/workers-types": "^4.20241218.0"
14
+ "@b9g/assets": "^0.1.12",
15
+ "@b9g/cache": "^0.1.4",
16
+ "@b9g/platform": "^0.1.9",
17
+ "@cloudflare/workers-types": "^4.20241218.0",
18
+ "miniflare": "^4.20251118.1"
18
19
  },
19
20
  "devDependencies": {
20
21
  "@b9g/libuild": "^0.1.11",
@@ -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,106 @@
1
+ /// <reference types="./filesystem-assets.d.ts" />
2
+ // src/filesystem-assets.ts
3
+ var CFAssetsDirectoryHandle = class _CFAssetsDirectoryHandle {
4
+ kind;
5
+ name;
6
+ #assets;
7
+ #basePath;
8
+ constructor(assets, basePath = "/") {
9
+ this.kind = "directory";
10
+ this.#assets = assets;
11
+ this.#basePath = basePath.endsWith("/") ? basePath : basePath + "/";
12
+ this.name = basePath.split("/").filter(Boolean).pop() || "assets";
13
+ }
14
+ async getFileHandle(name, _options) {
15
+ const path = this.#basePath + name;
16
+ const response = await this.#assets.fetch(
17
+ new Request("https://assets" + path)
18
+ );
19
+ if (!response.ok) {
20
+ throw new DOMException(
21
+ `A requested file or directory could not be found: ${name}`,
22
+ "NotFoundError"
23
+ );
24
+ }
25
+ return new CFAssetsFileHandle(this.#assets, path, name);
26
+ }
27
+ async getDirectoryHandle(name, _options) {
28
+ return new _CFAssetsDirectoryHandle(this.#assets, this.#basePath + name);
29
+ }
30
+ async removeEntry(_name, _options) {
31
+ throw new DOMException("Assets directory is read-only", "NotAllowedError");
32
+ }
33
+ async resolve(_possibleDescendant) {
34
+ return null;
35
+ }
36
+ // eslint-disable-next-line require-yield
37
+ async *entries() {
38
+ throw new DOMException(
39
+ "Directory listing not supported for ASSETS binding. Use an asset manifest for enumeration.",
40
+ "NotSupportedError"
41
+ );
42
+ }
43
+ // eslint-disable-next-line require-yield
44
+ async *keys() {
45
+ throw new DOMException(
46
+ "Directory listing not supported for ASSETS binding",
47
+ "NotSupportedError"
48
+ );
49
+ }
50
+ // eslint-disable-next-line require-yield
51
+ async *values() {
52
+ throw new DOMException(
53
+ "Directory listing not supported for ASSETS binding",
54
+ "NotSupportedError"
55
+ );
56
+ }
57
+ [Symbol.asyncIterator]() {
58
+ return this.entries();
59
+ }
60
+ isSameEntry(other) {
61
+ return Promise.resolve(
62
+ other instanceof _CFAssetsDirectoryHandle && other.#basePath === this.#basePath
63
+ );
64
+ }
65
+ };
66
+ var CFAssetsFileHandle = class _CFAssetsFileHandle {
67
+ kind;
68
+ name;
69
+ #assets;
70
+ #path;
71
+ constructor(assets, path, name) {
72
+ this.kind = "file";
73
+ this.#assets = assets;
74
+ this.#path = path;
75
+ this.name = name;
76
+ }
77
+ async getFile() {
78
+ const response = await this.#assets.fetch(
79
+ new Request("https://assets" + this.#path)
80
+ );
81
+ if (!response.ok) {
82
+ throw new DOMException(
83
+ `A requested file or directory could not be found: ${this.name}`,
84
+ "NotFoundError"
85
+ );
86
+ }
87
+ const blob = await response.blob();
88
+ const contentType = response.headers.get("content-type") || "application/octet-stream";
89
+ return new File([blob], this.name, { type: contentType });
90
+ }
91
+ async createWritable(_options) {
92
+ throw new DOMException("Assets are read-only", "NotAllowedError");
93
+ }
94
+ async createSyncAccessHandle() {
95
+ throw new DOMException("Sync access not supported", "NotSupportedError");
96
+ }
97
+ isSameEntry(other) {
98
+ return Promise.resolve(
99
+ other instanceof _CFAssetsFileHandle && other.#path === this.#path
100
+ );
101
+ }
102
+ };
103
+ export {
104
+ CFAssetsDirectoryHandle,
105
+ CFAssetsFileHandle
106
+ };
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// 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});\n\t\t\t\t\tlogger.error(\"Error stack\", {stack: error.stack});\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: 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
+ export default CloudflarePlatform;
package/src/index.js CHANGED
@@ -1,16 +1,411 @@
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;
12
+ #assetsMiniflare;
13
+ // Separate instance for ASSETS binding
14
+ #assetsBinding;
15
+ constructor(options = {}) {
16
+ super(options);
17
+ this.#miniflare = null;
18
+ this.#assetsMiniflare = null;
19
+ this.#assetsBinding = null;
20
+ this.name = "cloudflare";
21
+ this.#options = {
22
+ environment: "production",
23
+ assetsDirectory: void 0,
24
+ kvNamespaces: {},
25
+ r2Buckets: {},
26
+ d1Databases: {},
27
+ durableObjects: {},
28
+ ...options
29
+ };
30
+ }
31
+ /**
32
+ * Create "server" for Cloudflare Workers (which is really just the handler)
33
+ */
34
+ createServer(handler, _options = {}) {
35
+ return {
36
+ async listen() {
37
+ logger.info("Worker handler ready", {});
38
+ },
39
+ async close() {
40
+ logger.info("Worker handler stopped", {});
41
+ },
42
+ address: () => ({ port: 443, host: "cloudflare-workers" }),
43
+ get url() {
44
+ return "https://cloudflare-workers";
45
+ },
46
+ get ready() {
47
+ return true;
48
+ }
49
+ };
50
+ }
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
+ /**
83
+ * Load ServiceWorker using miniflare (workerd) for dev mode
84
+ */
85
+ async #loadServiceWorkerWithMiniflare(entrypoint) {
86
+ logger.info("Starting miniflare dev server", { entrypoint });
87
+ const { Miniflare } = await import("miniflare");
88
+ const miniflareOptions = {
89
+ modules: false,
90
+ // ServiceWorker format (not ES modules)
91
+ scriptPath: entrypoint,
92
+ // Enable CF-compatible APIs
93
+ compatibilityDate: "2024-09-23",
94
+ compatibilityFlags: ["nodejs_compat"]
95
+ };
96
+ this.#miniflare = new Miniflare(miniflareOptions);
97
+ await this.#miniflare.ready;
98
+ if (this.#options.assetsDirectory) {
99
+ logger.info("Setting up separate ASSETS binding", {
100
+ directory: this.#options.assetsDirectory
101
+ });
102
+ this.#assetsMiniflare = new Miniflare({
103
+ modules: true,
104
+ script: `export default { fetch() { return new Response("assets-only"); } }`,
105
+ assets: {
106
+ directory: this.#options.assetsDirectory,
107
+ binding: "ASSETS"
108
+ },
109
+ compatibilityDate: "2024-09-23"
110
+ });
111
+ const assetsEnv = await this.#assetsMiniflare.getBindings();
112
+ if (assetsEnv.ASSETS) {
113
+ this.#assetsBinding = assetsEnv.ASSETS;
114
+ logger.info("ASSETS binding available", {});
115
+ }
116
+ }
117
+ const mf = this.#miniflare;
118
+ const instance = {
119
+ runtime: mf,
120
+ handleRequest: async (request) => {
121
+ const cfResponse = await mf.dispatchFetch(request.url, {
122
+ method: request.method,
123
+ headers: request.headers,
124
+ body: request.body,
125
+ duplex: request.body ? "half" : void 0
126
+ });
127
+ return new Response(cfResponse.body, {
128
+ status: cfResponse.status,
129
+ statusText: cfResponse.statusText,
130
+ headers: cfResponse.headers
131
+ });
132
+ },
133
+ install: () => Promise.resolve(),
134
+ activate: () => Promise.resolve(),
135
+ get ready() {
136
+ return true;
137
+ },
138
+ dispose: async () => {
139
+ await mf.dispose();
140
+ }
141
+ };
142
+ logger.info("Miniflare dev server ready", {});
143
+ return instance;
144
+ }
145
+ /**
146
+ * Dispose of platform resources
147
+ */
148
+ async dispose() {
149
+ if (this.#miniflare) {
150
+ await this.#miniflare.dispose();
151
+ this.#miniflare = null;
152
+ }
153
+ if (this.#assetsMiniflare) {
154
+ await this.#assetsMiniflare.dispose();
155
+ this.#assetsMiniflare = null;
156
+ }
157
+ this.#assetsBinding = null;
158
+ }
159
+ };
160
+ function createOptionsFromEnv(env) {
161
+ return {
162
+ environment: env.ENVIRONMENT || "production",
163
+ kvNamespaces: extractKVNamespaces(env),
164
+ r2Buckets: extractR2Buckets(env),
165
+ d1Databases: extractD1Databases(env),
166
+ durableObjects: extractDurableObjects(env)
167
+ };
168
+ }
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
+ function generateWranglerConfig(options) {
206
+ const {
207
+ name,
208
+ entrypoint,
209
+ cacheAdapter: _cacheAdapter,
210
+ filesystemAdapter,
211
+ kvNamespaces = [],
212
+ r2Buckets = [],
213
+ d1Databases = []
214
+ } = options;
215
+ const autoKVNamespaces = [];
216
+ const autoR2Buckets = filesystemAdapter === "r2" ? ["STORAGE_R2"] : [];
217
+ const allKVNamespaces = [.../* @__PURE__ */ new Set([...kvNamespaces, ...autoKVNamespaces])];
218
+ const allR2Buckets = [.../* @__PURE__ */ new Set([...r2Buckets, ...autoR2Buckets])];
219
+ return `# Generated wrangler.toml for Shovel app
220
+ name = "${name}"
221
+ main = "${entrypoint}"
222
+ compatibility_date = "2024-09-23"
223
+ compatibility_flags = ["nodejs_compat"]
224
+
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]]
230
+ binding = "${kv}"
231
+ id = "your-kv-namespace-id"
232
+ preview_id = "your-preview-kv-namespace-id"`
233
+ ).join("\n\n") : ""}
234
+
235
+ # R2 bindings${allR2Buckets.length > 0 ? "\n" + allR2Buckets.map(
236
+ (bucket) => `[[r2_buckets]]
237
+ binding = "${bucket}"
238
+ bucket_name = "your-bucket-name"`
239
+ ).join("\n\n") : ""}
240
+
241
+ # D1 bindings
242
+ ${d1Databases.map(
243
+ (db) => `[[d1_databases]]
244
+ binding = "${db}"
245
+ database_name = "your-database-name"
246
+ database_id = "your-database-id"`
247
+ ).join("\n\n")}
248
+ `;
249
+ }
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});
369
+ logger.error("Error stack", {stack: error.stack});
370
+ return createErrorResponse(error);
371
+ }
372
+ }
373
+
374
+ return new Response('No ServiceWorker handler', { status: 404 });
375
+ } catch (topLevelError) {
376
+ logger.error("Top-level error", {error: topLevelError});
377
+ const isDev = typeof import.meta !== "undefined" && import.meta.env?.MODE !== "production";
378
+ if (isDev) {
379
+ const escapeHtml = (str) => String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
380
+ return new Response(\`<!DOCTYPE html>
381
+ <html>
382
+ <head>
383
+ <title>500 Internal Server Error</title>
384
+ <style>
385
+ body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
386
+ h1 { color: #c00; }
387
+ .message { font-size: 1.2em; color: #333; }
388
+ pre { background: #f5f5f5; padding: 1rem; overflow-x: auto; border-radius: 4px; }
389
+ </style>
390
+ </head>
391
+ <body>
392
+ <h1>500 Internal Server Error</h1>
393
+ <p class="message">\${escapeHtml(topLevelError.message)}</p>
394
+ <pre>\${escapeHtml(topLevelError.stack || "No stack trace available")}</pre>
395
+ </body>
396
+ </html>\`, { status: 500, headers: { "Content-Type": "text/html; charset=utf-8" } });
397
+ } else {
398
+ return new Response("Internal Server Error", { status: 500, headers: { "Content-Type": "text/plain" } });
399
+ }
400
+ }
401
+ }
402
+ };`;
403
+ var src_default = CloudflarePlatform;
9
404
  export {
10
405
  CloudflarePlatform,
11
406
  cloudflareWorkerBanner,
12
407
  cloudflareWorkerFooter,
13
- createCloudflarePlatform,
14
408
  createOptionsFromEnv,
409
+ src_default as default,
15
410
  generateWranglerConfig
16
411
  };
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,121 +0,0 @@
1
- /// <reference types="./platform.d.ts" />
2
- // src/platform.ts
3
- import {
4
- BasePlatform
5
- } from "@b9g/platform";
6
- import { FileSystemRegistry, getDirectoryHandle, MemoryBucket } from "@b9g/filesystem";
7
- var CloudflarePlatform = class extends BasePlatform {
8
- name = "cloudflare";
9
- options;
10
- constructor(options = {}) {
11
- super(options);
12
- this.options = {
13
- environment: "production",
14
- kvNamespaces: {},
15
- r2Buckets: {},
16
- d1Databases: {},
17
- durableObjects: {},
18
- ...options
19
- };
20
- FileSystemRegistry.register("memory", new MemoryBucket());
21
- }
22
- /**
23
- * Get filesystem directory handle (memory-only in Workers runtime)
24
- */
25
- async getDirectoryHandle(name) {
26
- const adapter = new MemoryBucket();
27
- return await adapter.getDirectoryHandle(name);
28
- }
29
- /**
30
- * Get platform-specific default cache configuration for Cloudflare Workers
31
- */
32
- getDefaultCacheConfig() {
33
- return {
34
- pages: { type: "cloudflare" },
35
- // Use Cloudflare's native Cache API
36
- api: { type: "cloudflare" },
37
- // Use Cloudflare's native Cache API
38
- static: { type: "cloudflare" }
39
- // Static files handled by CDN
40
- };
41
- }
42
- /**
43
- * Override cache creation to use bundled KV adapter
44
- */
45
- async createCaches(config) {
46
- return globalThis.caches;
47
- }
48
- /**
49
- * Create "server" for Cloudflare Workers (which is really just the handler)
50
- */
51
- createServer(handler, _options = {}) {
52
- return {
53
- async listen() {
54
- console.info("[Cloudflare] Worker handler ready");
55
- },
56
- async close() {
57
- console.info("[Cloudflare] Worker handler stopped");
58
- },
59
- address: () => ({ port: 443, host: "cloudflare-workers" }),
60
- get url() {
61
- return "https://cloudflare-workers";
62
- },
63
- get ready() {
64
- return true;
65
- }
66
- };
67
- }
68
- /**
69
- * Load ServiceWorker-style entrypoint in Cloudflare Workers
70
- *
71
- * Cloudflare Workers are already ServiceWorker-based, so we can use
72
- * the global environment directly in production
73
- */
74
- async loadServiceWorker(entrypoint, options = {}) {
75
- const isCloudflareWorker = typeof globalThis.addEventListener === "function" && typeof globalThis.caches !== "undefined" && typeof globalThis.FetchEvent !== "undefined";
76
- if (isCloudflareWorker) {
77
- console.info("[Cloudflare] Running in native ServiceWorker environment");
78
- const instance = {
79
- runtime: globalThis,
80
- handleRequest: async (request) => {
81
- const event = new FetchEvent("fetch", { request });
82
- globalThis.dispatchEvent(event);
83
- return new Response("Worker handler", { status: 200 });
84
- },
85
- install: () => Promise.resolve(),
86
- activate: () => Promise.resolve(),
87
- collectStaticRoutes: async () => [],
88
- // Not supported in Workers
89
- get ready() {
90
- return true;
91
- },
92
- dispose: async () => {
93
- }
94
- };
95
- await import(entrypoint);
96
- return instance;
97
- } else {
98
- throw new Error("Cloudflare platform development mode not yet implemented. Use Node platform for development.");
99
- }
100
- }
101
- /**
102
- * Get filesystem root for File System Access API
103
- */
104
- async getFileSystemRoot(name = "default") {
105
- return await getDirectoryHandle(name);
106
- }
107
- /**
108
- * Dispose of platform resources
109
- */
110
- async dispose() {
111
- }
112
- };
113
- function createCloudflarePlatform(options) {
114
- return new CloudflarePlatform(options);
115
- }
116
- var platform_default = createCloudflarePlatform;
117
- export {
118
- CloudflarePlatform,
119
- createCloudflarePlatform,
120
- platform_default as default
121
- };
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
- };