@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 +111 -0
- package/package.json +13 -28
- package/src/filesystem-assets.d.ts +55 -0
- package/src/filesystem-assets.js +101 -0
- package/src/index.d.ts +67 -4
- package/src/index.js +356 -6
- package/src/platform.d.ts +0 -66
- package/src/platform.js +0 -137
- package/src/wrangler.d.ts +0 -20
- package/src/wrangler.js +0 -95
- package/src/wrapper.d.ts +0 -12
- package/src/wrapper.js +0 -127
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.
|
|
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/
|
|
15
|
-
"@b9g/cache": "^0.1.
|
|
16
|
-
"@b9g/
|
|
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.
|
|
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
|
-
"./
|
|
53
|
-
"types": "./src/
|
|
54
|
-
"import": "./src/
|
|
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
|
-
"./
|
|
61
|
-
"types": "./src/
|
|
62
|
-
"import": "./src/
|
|
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
|
-
|
|
7
|
-
export {
|
|
8
|
-
export
|
|
9
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
} from "
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
};
|