@b9g/platform-cloudflare 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -64
- package/package.json +38 -13
- package/src/caches.d.ts +24 -0
- package/src/caches.js +52 -0
- package/src/directories.d.ts +160 -0
- package/src/directories.js +344 -0
- package/src/index.d.ts +49 -42
- package/src/index.js +121 -244
- package/src/runtime.d.ts +47 -0
- package/src/runtime.js +65 -0
- package/src/variables.d.ts +15 -0
- package/src/variables.js +17 -0
- package/src/filesystem-assets.d.ts +0 -55
- package/src/filesystem-assets.js +0 -106
package/src/index.js
CHANGED
|
@@ -2,47 +2,68 @@
|
|
|
2
2
|
// src/index.ts
|
|
3
3
|
import {
|
|
4
4
|
BasePlatform,
|
|
5
|
-
|
|
6
|
-
createCacheFactory
|
|
5
|
+
CustomLoggerStorage
|
|
7
6
|
} from "@b9g/platform";
|
|
7
|
+
import { createCacheFactory } from "@b9g/platform/runtime";
|
|
8
8
|
import { CustomCacheStorage } from "@b9g/cache";
|
|
9
9
|
import { getLogger } from "@logtape/logtape";
|
|
10
|
-
|
|
10
|
+
import { CloudflareNativeCache } from "./caches.js";
|
|
11
|
+
var logger = getLogger(["shovel", "platform"]);
|
|
11
12
|
var CloudflarePlatform = class extends BasePlatform {
|
|
12
13
|
name;
|
|
13
14
|
#options;
|
|
14
15
|
#miniflare;
|
|
15
16
|
#assetsMiniflare;
|
|
16
|
-
// Separate instance for ASSETS binding
|
|
17
|
-
#config;
|
|
18
17
|
constructor(options = {}) {
|
|
19
18
|
super(options);
|
|
20
19
|
this.#miniflare = null;
|
|
21
20
|
this.#assetsMiniflare = null;
|
|
22
21
|
this.name = "cloudflare";
|
|
23
22
|
const cwd = options.cwd ?? ".";
|
|
24
|
-
this.#config = loadConfig(cwd);
|
|
25
23
|
this.#options = {
|
|
26
24
|
environment: options.environment ?? "production",
|
|
27
25
|
assetsDirectory: options.assetsDirectory,
|
|
28
|
-
cwd
|
|
26
|
+
cwd,
|
|
27
|
+
config: options.config
|
|
29
28
|
};
|
|
30
29
|
}
|
|
31
30
|
/**
|
|
32
|
-
* Create cache storage
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* Note: This is for the platform/test runner context. Inside actual
|
|
36
|
-
* Cloudflare Workers, native caches are available via globalThis.caches
|
|
37
|
-
* (captured by the banner as globalThis.__cloudflareCaches).
|
|
31
|
+
* Create cache storage using config from shovel.json
|
|
32
|
+
* Default: Cloudflare's native Cache API
|
|
33
|
+
* Merges with runtime defaults (actual class references) for fallback behavior
|
|
38
34
|
*/
|
|
39
35
|
async createCaches() {
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
const runtimeDefaults = {
|
|
37
|
+
default: { impl: CloudflareNativeCache }
|
|
38
|
+
};
|
|
39
|
+
const userCaches = this.#options.config?.caches ?? {};
|
|
40
|
+
const configs = {};
|
|
41
|
+
const allNames = /* @__PURE__ */ new Set([
|
|
42
|
+
...Object.keys(runtimeDefaults),
|
|
43
|
+
...Object.keys(userCaches)
|
|
44
|
+
]);
|
|
45
|
+
for (const name of allNames) {
|
|
46
|
+
configs[name] = { ...runtimeDefaults[name], ...userCaches[name] };
|
|
47
|
+
}
|
|
48
|
+
return new CustomCacheStorage(createCacheFactory({ configs }));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create directory storage for Cloudflare Workers
|
|
52
|
+
* Directories must be configured via shovel.json (no platform defaults)
|
|
53
|
+
*/
|
|
54
|
+
async createDirectories() {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Cloudflare Workers do not have default directories. Configure directories in shovel.json using Cloudflare directory classes."
|
|
42
57
|
);
|
|
43
58
|
}
|
|
44
59
|
/**
|
|
45
|
-
* Create
|
|
60
|
+
* Create logger storage for Cloudflare Workers
|
|
61
|
+
*/
|
|
62
|
+
async createLoggers() {
|
|
63
|
+
return new CustomLoggerStorage((categories) => getLogger(categories));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create "server" for Cloudflare Workers (stub for Platform interface)
|
|
46
67
|
*/
|
|
47
68
|
createServer(handler, _options = {}) {
|
|
48
69
|
return {
|
|
@@ -61,34 +82,22 @@ var CloudflarePlatform = class extends BasePlatform {
|
|
|
61
82
|
}
|
|
62
83
|
};
|
|
63
84
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Load ServiceWorker-style entrypoint using miniflare (workerd)
|
|
66
|
-
*
|
|
67
|
-
* Note: In production Cloudflare Workers, the banner/footer wrapper code
|
|
68
|
-
* handles request dispatch directly - loadServiceWorker is only used for
|
|
69
|
-
* local development with miniflare.
|
|
70
|
-
*/
|
|
71
|
-
async loadServiceWorker(entrypoint, _options = {}) {
|
|
72
|
-
return this.#loadServiceWorkerWithMiniflare(entrypoint);
|
|
73
|
-
}
|
|
74
85
|
/**
|
|
75
86
|
* Load ServiceWorker using miniflare (workerd) for dev mode
|
|
76
87
|
*/
|
|
77
|
-
async
|
|
88
|
+
async loadServiceWorker(entrypoint, _options = {}) {
|
|
78
89
|
logger.info("Starting miniflare dev server", { entrypoint });
|
|
79
90
|
const { Miniflare } = await import("miniflare");
|
|
80
91
|
const miniflareOptions = {
|
|
81
|
-
modules:
|
|
82
|
-
// ServiceWorker format (not ES modules)
|
|
92
|
+
modules: true,
|
|
83
93
|
scriptPath: entrypoint,
|
|
84
|
-
// Enable CF-compatible APIs
|
|
85
94
|
compatibilityDate: "2024-09-23",
|
|
86
95
|
compatibilityFlags: ["nodejs_compat"]
|
|
87
96
|
};
|
|
88
97
|
this.#miniflare = new Miniflare(miniflareOptions);
|
|
89
98
|
await this.#miniflare.ready;
|
|
90
99
|
if (this.#options.assetsDirectory) {
|
|
91
|
-
logger.info("Setting up
|
|
100
|
+
logger.info("Setting up ASSETS binding", {
|
|
92
101
|
directory: this.#options.assetsDirectory
|
|
93
102
|
});
|
|
94
103
|
this.#assetsMiniflare = new Miniflare({
|
|
@@ -101,9 +110,17 @@ var CloudflarePlatform = class extends BasePlatform {
|
|
|
101
110
|
compatibilityDate: "2024-09-23"
|
|
102
111
|
});
|
|
103
112
|
await this.#assetsMiniflare.ready;
|
|
104
|
-
logger.info("ASSETS binding available", {});
|
|
105
113
|
}
|
|
106
114
|
const mf = this.#miniflare;
|
|
115
|
+
const assetsMf = this.#assetsMiniflare;
|
|
116
|
+
const disposeInstance = async () => {
|
|
117
|
+
await mf.dispose();
|
|
118
|
+
this.#miniflare = null;
|
|
119
|
+
if (assetsMf) {
|
|
120
|
+
await assetsMf.dispose();
|
|
121
|
+
this.#assetsMiniflare = null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
107
124
|
const instance = {
|
|
108
125
|
runtime: mf,
|
|
109
126
|
handleRequest: async (request) => {
|
|
@@ -124,16 +141,11 @@ var CloudflarePlatform = class extends BasePlatform {
|
|
|
124
141
|
get ready() {
|
|
125
142
|
return true;
|
|
126
143
|
},
|
|
127
|
-
dispose:
|
|
128
|
-
await mf.dispose();
|
|
129
|
-
}
|
|
144
|
+
dispose: disposeInstance
|
|
130
145
|
};
|
|
131
146
|
logger.info("Miniflare dev server ready", {});
|
|
132
147
|
return instance;
|
|
133
148
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Dispose of platform resources
|
|
136
|
-
*/
|
|
137
149
|
async dispose() {
|
|
138
150
|
if (this.#miniflare) {
|
|
139
151
|
await this.#miniflare.dispose();
|
|
@@ -144,219 +156,84 @@ var CloudflarePlatform = class extends BasePlatform {
|
|
|
144
156
|
this.#assetsMiniflare = null;
|
|
145
157
|
}
|
|
146
158
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const autoR2Buckets = filesystemAdapter === "r2" ? ["STORAGE_R2"] : [];
|
|
165
|
-
const allKVNamespaces = [.../* @__PURE__ */ new Set([...kvNamespaces, ...autoKVNamespaces])];
|
|
166
|
-
const allR2Buckets = [.../* @__PURE__ */ new Set([...r2Buckets, ...autoR2Buckets])];
|
|
167
|
-
return `# Generated wrangler.toml for Shovel app
|
|
168
|
-
name = "${name}"
|
|
169
|
-
main = "${entrypoint}"
|
|
170
|
-
compatibility_date = "2024-09-23"
|
|
171
|
-
compatibility_flags = ["nodejs_compat"]
|
|
159
|
+
/**
|
|
160
|
+
* Get virtual entry wrapper for Cloudflare Workers
|
|
161
|
+
*
|
|
162
|
+
* Wraps user code with:
|
|
163
|
+
* 1. Config import (shovel:config virtual module)
|
|
164
|
+
* 2. Runtime initialization (ServiceWorkerGlobals)
|
|
165
|
+
* 3. User code import (registers fetch handlers)
|
|
166
|
+
* 4. ES module export for Cloudflare Workers format
|
|
167
|
+
*
|
|
168
|
+
* Note: Unlike Node/Bun, Cloudflare bundles user code inline, so the
|
|
169
|
+
* entryPath is embedded directly in the wrapper.
|
|
170
|
+
*/
|
|
171
|
+
getEntryWrapper(entryPath, _options) {
|
|
172
|
+
const safePath = JSON.stringify(entryPath);
|
|
173
|
+
return `// Cloudflare Worker Entry
|
|
174
|
+
import { config } from "shovel:config";
|
|
175
|
+
import { initializeRuntime, createFetchHandler } from "@b9g/platform-cloudflare/runtime";
|
|
172
176
|
|
|
173
|
-
|
|
174
|
-
usage_model = "bundled"
|
|
177
|
+
const registration = await initializeRuntime(config);
|
|
175
178
|
|
|
176
|
-
|
|
177
|
-
(kv) => `[[kv_namespaces]]
|
|
178
|
-
binding = "${kv}"
|
|
179
|
-
id = "your-kv-namespace-id"
|
|
180
|
-
preview_id = "your-preview-kv-namespace-id"`
|
|
181
|
-
).join("\n\n") : ""}
|
|
179
|
+
import ${safePath};
|
|
182
180
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
bucket_name = "your-bucket-name"`
|
|
187
|
-
).join("\n\n") : ""}
|
|
181
|
+
// Run ServiceWorker lifecycle (install/activate events for migrations, cache warmup, etc.)
|
|
182
|
+
await registration.install();
|
|
183
|
+
await registration.activate();
|
|
188
184
|
|
|
189
|
-
|
|
190
|
-
${d1Databases.map(
|
|
191
|
-
(db) => `[[d1_databases]]
|
|
192
|
-
binding = "${db}"
|
|
193
|
-
database_name = "your-database-name"
|
|
194
|
-
database_id = "your-database-id"`
|
|
195
|
-
).join("\n\n")}
|
|
185
|
+
export default { fetch: createFetchHandler(registration) };
|
|
196
186
|
`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get Cloudflare-specific esbuild configuration
|
|
190
|
+
*
|
|
191
|
+
* Note: Cloudflare Workers natively support import.meta.env, so no define alias
|
|
192
|
+
* is needed. The nodejs_compat flag enables node:* built-in modules at runtime,
|
|
193
|
+
* so we externalize them during bundling.
|
|
194
|
+
*/
|
|
195
|
+
getESBuildConfig() {
|
|
196
|
+
return {
|
|
197
|
+
platform: "browser",
|
|
198
|
+
conditions: ["worker", "browser"],
|
|
199
|
+
// Externalize node builtins - available at runtime via nodejs_compat flag
|
|
200
|
+
// Include both node:* prefix and bare module names for compatibility
|
|
201
|
+
external: [
|
|
202
|
+
"node:*",
|
|
203
|
+
"path",
|
|
204
|
+
"fs",
|
|
205
|
+
"fs/promises",
|
|
206
|
+
"crypto",
|
|
207
|
+
"util",
|
|
208
|
+
"stream",
|
|
209
|
+
"buffer",
|
|
210
|
+
"events"
|
|
211
|
+
],
|
|
212
|
+
// Cloudflare bundles user code inline via `import "user-entry"`
|
|
213
|
+
bundlesUserCodeInline: true
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get Cloudflare-specific defaults for config generation
|
|
218
|
+
*/
|
|
219
|
+
getDefaults() {
|
|
220
|
+
return {
|
|
221
|
+
caches: {
|
|
222
|
+
default: {
|
|
223
|
+
module: "@b9g/platform-cloudflare/caches"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
directories: {
|
|
227
|
+
public: {
|
|
228
|
+
module: "@b9g/platform-cloudflare/directories",
|
|
229
|
+
export: "CloudflareAssetsDirectory"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
221
234
|
};
|
|
222
|
-
|
|
223
|
-
// Create a promise-based FetchEvent that can be awaited
|
|
224
|
-
class FetchEvent {
|
|
225
|
-
constructor(type, init) {
|
|
226
|
-
this.type = type;
|
|
227
|
-
this.request = init.request;
|
|
228
|
-
this._response = null;
|
|
229
|
-
this._responsePromise = new Promise((resolve) => {
|
|
230
|
-
this._resolveResponse = resolve;
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
respondWith(response) {
|
|
235
|
-
this._response = response;
|
|
236
|
-
this._resolveResponse(response);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async waitUntil(promise) {
|
|
240
|
-
await promise;
|
|
241
|
-
}
|
|
242
|
-
}`;
|
|
243
|
-
var cloudflareWorkerFooter = `
|
|
244
|
-
// Export ES Module for Cloudflare Workers
|
|
245
|
-
export default {
|
|
246
|
-
async fetch(request, env, ctx) {
|
|
247
|
-
try {
|
|
248
|
-
// Set up ServiceWorker-like dirs API for bundled deployment
|
|
249
|
-
if (!globalThis.self.dirs) {
|
|
250
|
-
// For bundled deployment, assets are served via static middleware
|
|
251
|
-
// not through the dirs API
|
|
252
|
-
globalThis.self.dirs = {
|
|
253
|
-
async open(directoryName) {
|
|
254
|
-
if (directoryName === 'assets') {
|
|
255
|
-
// Return a minimal interface that indicates no files available
|
|
256
|
-
// The assets middleware will fall back to dev mode behavior
|
|
257
|
-
return {
|
|
258
|
-
async getFileHandle(fileName) {
|
|
259
|
-
throw new Error(\`NotFoundError: \${fileName} not found in bundled assets\`);
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
throw new Error(\`Directory \${directoryName} not available in bundled deployment\`);
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Set up caches API
|
|
269
|
-
if (!globalThis.self.caches) {
|
|
270
|
-
globalThis.self.caches = globalThis.caches;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Ensure request.url is a string
|
|
274
|
-
if (typeof request.url !== 'string') {
|
|
275
|
-
return new Response('Invalid request URL: ' + typeof request.url, { status: 500 });
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Create proper FetchEvent-like object
|
|
279
|
-
let responseReceived = null;
|
|
280
|
-
const event = {
|
|
281
|
-
request,
|
|
282
|
-
respondWith: (response) => { responseReceived = response; }
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
// Helper for error responses
|
|
286
|
-
const createErrorResponse = (err) => {
|
|
287
|
-
const isDev = typeof import.meta !== "undefined" && import.meta.env?.MODE !== "production";
|
|
288
|
-
if (isDev) {
|
|
289
|
-
const escapeHtml = (str) => str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
290
|
-
return new Response(\`<!DOCTYPE html>
|
|
291
|
-
<html>
|
|
292
|
-
<head>
|
|
293
|
-
<title>500 Internal Server Error</title>
|
|
294
|
-
<style>
|
|
295
|
-
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
|
296
|
-
h1 { color: #c00; }
|
|
297
|
-
.message { font-size: 1.2em; color: #333; }
|
|
298
|
-
pre { background: #f5f5f5; padding: 1rem; overflow-x: auto; border-radius: 4px; }
|
|
299
|
-
</style>
|
|
300
|
-
</head>
|
|
301
|
-
<body>
|
|
302
|
-
<h1>500 Internal Server Error</h1>
|
|
303
|
-
<p class="message">\${escapeHtml(err.message)}</p>
|
|
304
|
-
<pre>\${escapeHtml(err.stack || "No stack trace available")}</pre>
|
|
305
|
-
</body>
|
|
306
|
-
</html>\`, { status: 500, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
307
|
-
} else {
|
|
308
|
-
return new Response("Internal Server Error", { status: 500, headers: { "Content-Type": "text/plain" } });
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
// Dispatch to ServiceWorker fetch handlers
|
|
313
|
-
for (const handler of fetchHandlers) {
|
|
314
|
-
try {
|
|
315
|
-
await handler(event);
|
|
316
|
-
if (responseReceived) {
|
|
317
|
-
return responseReceived;
|
|
318
|
-
}
|
|
319
|
-
} catch (error) {
|
|
320
|
-
console.error("Handler error:", error);
|
|
321
|
-
return createErrorResponse(error);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return new Response('No ServiceWorker handler', { status: 404 });
|
|
326
|
-
} catch (topLevelError) {
|
|
327
|
-
console.error("Top-level error:", topLevelError);
|
|
328
|
-
const isDev = typeof import.meta !== "undefined" && import.meta.env?.MODE !== "production";
|
|
329
|
-
if (isDev) {
|
|
330
|
-
const escapeHtml = (str) => String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
331
|
-
return new Response(\`<!DOCTYPE html>
|
|
332
|
-
<html>
|
|
333
|
-
<head>
|
|
334
|
-
<title>500 Internal Server Error</title>
|
|
335
|
-
<style>
|
|
336
|
-
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
|
337
|
-
h1 { color: #c00; }
|
|
338
|
-
.message { font-size: 1.2em; color: #333; }
|
|
339
|
-
pre { background: #f5f5f5; padding: 1rem; overflow-x: auto; border-radius: 4px; }
|
|
340
|
-
</style>
|
|
341
|
-
</head>
|
|
342
|
-
<body>
|
|
343
|
-
<h1>500 Internal Server Error</h1>
|
|
344
|
-
<p class="message">\${escapeHtml(topLevelError.message)}</p>
|
|
345
|
-
<pre>\${escapeHtml(topLevelError.stack || "No stack trace available")}</pre>
|
|
346
|
-
</body>
|
|
347
|
-
</html>\`, { status: 500, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
348
|
-
} else {
|
|
349
|
-
return new Response("Internal Server Error", { status: 500, headers: { "Content-Type": "text/plain" } });
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
};`;
|
|
354
235
|
var src_default = CloudflarePlatform;
|
|
355
236
|
export {
|
|
356
237
|
CloudflarePlatform,
|
|
357
|
-
|
|
358
|
-
cloudflareWorkerFooter,
|
|
359
|
-
createOptionsFromEnv,
|
|
360
|
-
src_default as default,
|
|
361
|
-
generateWranglerConfig
|
|
238
|
+
src_default as default
|
|
362
239
|
};
|
package/src/runtime.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Worker Runtime
|
|
3
|
+
*
|
|
4
|
+
* This module provides runtime initialization for Cloudflare Workers.
|
|
5
|
+
* It is imported by the entry wrapper, not by user code.
|
|
6
|
+
*/
|
|
7
|
+
import { ShovelServiceWorkerRegistration, ShovelFetchEvent, type ShovelFetchEventInit, type ShovelConfig } from "@b9g/platform/runtime";
|
|
8
|
+
export type { ShovelConfig };
|
|
9
|
+
/**
|
|
10
|
+
* Cloudflare's ExecutionContext - passed to each request handler
|
|
11
|
+
*/
|
|
12
|
+
export interface ExecutionContext {
|
|
13
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
14
|
+
passThroughOnException(): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Options for CloudflareFetchEvent constructor
|
|
18
|
+
*/
|
|
19
|
+
export interface CloudflareFetchEventInit extends ShovelFetchEventInit {
|
|
20
|
+
/** Cloudflare environment bindings (KV, R2, D1, etc.) */
|
|
21
|
+
env: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Cloudflare-specific FetchEvent with env bindings.
|
|
25
|
+
*
|
|
26
|
+
* Extends ShovelFetchEvent to add the `env` property for accessing
|
|
27
|
+
* Cloudflare bindings (KV namespaces, R2 buckets, D1 databases, etc.)
|
|
28
|
+
*/
|
|
29
|
+
export declare class CloudflareFetchEvent extends ShovelFetchEvent {
|
|
30
|
+
/** Cloudflare environment bindings (KV, R2, D1, Durable Objects, etc.) */
|
|
31
|
+
readonly env: Record<string, unknown>;
|
|
32
|
+
constructor(request: Request, options: CloudflareFetchEventInit);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Initialize the Cloudflare runtime with ServiceWorkerGlobals
|
|
36
|
+
*
|
|
37
|
+
* @param config - Shovel configuration from shovel:config virtual module
|
|
38
|
+
* @returns The ServiceWorker registration for handling requests
|
|
39
|
+
*/
|
|
40
|
+
export declare function initializeRuntime(config: ShovelConfig): Promise<ShovelServiceWorkerRegistration>;
|
|
41
|
+
/**
|
|
42
|
+
* Create the ES module fetch handler for Cloudflare Workers
|
|
43
|
+
*
|
|
44
|
+
* Creates a CloudflareFetchEvent with env bindings and waitUntil hook,
|
|
45
|
+
* then delegates to registration.handleEvent()
|
|
46
|
+
*/
|
|
47
|
+
export declare function createFetchHandler(registration: ShovelServiceWorkerRegistration): (request: Request, env: unknown, ctx: ExecutionContext) => Promise<Response>;
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/// <reference types="./runtime.d.ts" />
|
|
2
|
+
// src/runtime.ts
|
|
3
|
+
import {
|
|
4
|
+
ServiceWorkerGlobals,
|
|
5
|
+
ShovelServiceWorkerRegistration,
|
|
6
|
+
ShovelFetchEvent,
|
|
7
|
+
CustomLoggerStorage,
|
|
8
|
+
configureLogging,
|
|
9
|
+
createCacheFactory,
|
|
10
|
+
createDirectoryFactory
|
|
11
|
+
} from "@b9g/platform/runtime";
|
|
12
|
+
import { CustomCacheStorage } from "@b9g/cache";
|
|
13
|
+
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
14
|
+
import { getLogger } from "@logtape/logtape";
|
|
15
|
+
import { envStorage } from "./variables.js";
|
|
16
|
+
var CloudflareFetchEvent = class extends ShovelFetchEvent {
|
|
17
|
+
/** Cloudflare environment bindings (KV, R2, D1, Durable Objects, etc.) */
|
|
18
|
+
env;
|
|
19
|
+
constructor(request, options) {
|
|
20
|
+
super(request, options);
|
|
21
|
+
this.env = options.env;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var _registration = null;
|
|
25
|
+
var _globals = null;
|
|
26
|
+
async function initializeRuntime(config) {
|
|
27
|
+
if (_registration) {
|
|
28
|
+
return _registration;
|
|
29
|
+
}
|
|
30
|
+
if (config.logging) {
|
|
31
|
+
await configureLogging(config.logging);
|
|
32
|
+
}
|
|
33
|
+
_registration = new ShovelServiceWorkerRegistration();
|
|
34
|
+
const caches = new CustomCacheStorage(
|
|
35
|
+
createCacheFactory({ configs: config.caches ?? {} })
|
|
36
|
+
);
|
|
37
|
+
const directories = new CustomDirectoryStorage(
|
|
38
|
+
createDirectoryFactory(config.directories ?? {})
|
|
39
|
+
);
|
|
40
|
+
_globals = new ServiceWorkerGlobals({
|
|
41
|
+
registration: _registration,
|
|
42
|
+
caches,
|
|
43
|
+
directories,
|
|
44
|
+
loggers: new CustomLoggerStorage((cats) => getLogger(cats))
|
|
45
|
+
});
|
|
46
|
+
_globals.install();
|
|
47
|
+
return _registration;
|
|
48
|
+
}
|
|
49
|
+
function createFetchHandler(registration) {
|
|
50
|
+
return async (request, env, ctx) => {
|
|
51
|
+
const event = new CloudflareFetchEvent(request, {
|
|
52
|
+
env,
|
|
53
|
+
platformWaitUntil: (promise) => ctx.waitUntil(promise)
|
|
54
|
+
});
|
|
55
|
+
return envStorage.run(
|
|
56
|
+
env,
|
|
57
|
+
() => registration.handleRequest(event)
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
CloudflareFetchEvent,
|
|
63
|
+
createFetchHandler,
|
|
64
|
+
initializeRuntime
|
|
65
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Environment Storage
|
|
3
|
+
*
|
|
4
|
+
* Provides per-request access to Cloudflare's env object (KV, R2, D1 bindings, etc.)
|
|
5
|
+
* via AsyncContext. Used by directory implementations to resolve bindings at runtime.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Per-request storage for Cloudflare's env object.
|
|
9
|
+
* Set by createFetchHandler() via envStorage.run().
|
|
10
|
+
*/
|
|
11
|
+
export declare const envStorage: any;
|
|
12
|
+
/**
|
|
13
|
+
* Get the current Cloudflare env or throw if not in request context.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getEnv(): Record<string, unknown>;
|
package/src/variables.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/// <reference types="./variables.d.ts" />
|
|
2
|
+
// src/variables.ts
|
|
3
|
+
import { AsyncContext } from "@b9g/async-context";
|
|
4
|
+
var envStorage = new AsyncContext.Variable();
|
|
5
|
+
function getEnv() {
|
|
6
|
+
const env = envStorage.get();
|
|
7
|
+
if (!env) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
"Cloudflare env not available. Are you accessing bindings outside of a request context?"
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
return env;
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
envStorage,
|
|
16
|
+
getEnv
|
|
17
|
+
};
|
|
@@ -1,55 +0,0 @@
|
|
|
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
|
-
}
|