@fungi.computer/cloudflare-worker-bundler 0.0.0-stage → 0.2.3-fungi.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License Copyright (c) 2025 Cloudflare, Inc.
2
+
3
+ Permission is hereby granted, free of
4
+ charge, to any person obtaining a copy of this software and associated
5
+ documentation files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use, copy, modify, merge,
7
+ publish, distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice
12
+ (including the next paragraph) shall be included in all copies or substantial
13
+ portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
18
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
19
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,22 @@
1
+ @fungi.computer/cloudflare-worker-bundler
2
+ Copyright (c) 2025 Cloudflare, Inc.
3
+
4
+ This package is a fork of @cloudflare/worker-bundler 0.2.3
5
+ (https://github.com/cloudflare/agents/tree/%40cloudflare/worker-bundler%400.2.3/packages/worker-bundler,
6
+ commit d42494503efe836a073fbf911fae1fbd12253198), distributed under the MIT
7
+ License in LICENSE.
8
+
9
+ Changes from upstream, made by Fungi Computer:
10
+
11
+ - src/assets.ts: a new `./assets` entry that exports asset serving
12
+ (handleAssetRequest, buildAssetManifest, buildAssets, createMemoryStorage,
13
+ inferContentType, isTextContentType) without the esbuild/Sucrase bundler
14
+ graph. src/index.ts re-exports the same names from it.
15
+ - src/file-system.ts: imports DurableObjectStorage from
16
+ @cloudflare/workers-types, which is now a dependency, so the published
17
+ declarations type-check without ambient Workers types.
18
+ - scripts/build.ts: builds the `./assets` entry and drops the upstream
19
+ monorepo declaration-formatting step.
20
+ - src/index.ts, src/app.ts, src/asset-handler.ts: three expressions rewritten
21
+ without a redundant spread so the fork passes oxlint's correctness rules.
22
+ - Only the Node-runnable upstream tests (asset-handler, redos) are kept.
package/README.md CHANGED
@@ -1,3 +1,120 @@
1
- # Temporary Holding Version
1
+ # @fungi.computer/cloudflare-worker-bundler
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **Experimental**: this package is under active development and its API may
4
+ > change without notice. Not recommended for production use.
5
+
6
+ Bundles and serves full-stack applications on Cloudflare's
7
+ [Worker Loader binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/worker-loader/)
8
+ (closed beta). It dynamically generates Workers with real npm dependencies, or
9
+ builds complete apps with client-side bundles and static asset serving.
10
+
11
+ ## Runtime requirement
12
+
13
+ This package only runs **inside the Cloudflare Workers runtime (`workerd`)** —
14
+ both in production and in `wrangler dev`. It bundles via an `esbuild-wasm`
15
+ `WebAssembly.Module` import that only the Workers module loader can resolve.
16
+ It will **not** work under plain Node.js, including a default-pool
17
+ Vitest/Jest test; test code that depends on it needs
18
+ [`@cloudflare/vitest-pool-workers`](https://developers.cloudflare.com/workers/testing/vitest-integration/).
19
+
20
+ The `./assets` entry (see below) is the exception: it has no bundler
21
+ dependency and runs anywhere.
22
+
23
+ ## Fungi fork
24
+
25
+ This package is a Fungi fork of
26
+ [`@cloudflare/worker-bundler`](https://github.com/cloudflare/agents/tree/%40cloudflare/worker-bundler%400.2.3/packages/worker-bundler)
27
+ 0.2.3 (commit `d424945`), distributed under the MIT License. Changes from
28
+ upstream, made by Fungi Computer:
29
+
30
+ - `src/assets.ts`: a new `./assets` entry that exports asset serving
31
+ (`handleAssetRequest`, `buildAssetManifest`, `buildAssets`,
32
+ `createMemoryStorage`, `inferContentType`, `isTextContentType`) without the
33
+ esbuild/Sucrase bundler graph. `src/index.ts` re-exports the same names.
34
+ - `src/file-system.ts`: imports `DurableObjectStorage` from
35
+ `@cloudflare/workers-types`, now a dependency, so the published
36
+ declarations type-check without ambient Workers types.
37
+ - `scripts/build.ts`: builds the `./assets` entry and drops the upstream
38
+ monorepo declaration-formatting step.
39
+ - `src/index.ts`, `src/app.ts`, `src/asset-handler.ts`: three expressions
40
+ rewritten without a redundant spread so the fork passes oxlint's
41
+ correctness rules.
42
+ - Only the Node-runnable upstream tests (`asset-handler`, `redos`) are kept.
43
+
44
+ See [NOTICE](./NOTICE) for the full attribution text.
45
+
46
+ ## Install
47
+
48
+ ```sh
49
+ npm install @fungi.computer/cloudflare-worker-bundler
50
+ ```
51
+
52
+ ## Quick start
53
+
54
+ Bundle a Worker — provide source code and dependencies, the bundler handles
55
+ the rest:
56
+
57
+ ```ts
58
+ import { createWorker } from "@fungi.computer/cloudflare-worker-bundler";
59
+
60
+ const worker = env.LOADER.get("my-worker", async () => {
61
+ const { mainModule, modules } = await createWorker({
62
+ files: {
63
+ "src/index.ts": `
64
+ import { Hono } from 'hono';
65
+ const app = new Hono();
66
+ app.get('/', (c) => c.text('Hello from Hono!'));
67
+ export default app;
68
+ `,
69
+ "package.json": JSON.stringify({ dependencies: { hono: "^4.0.0" } })
70
+ }
71
+ });
72
+
73
+ return { mainModule, modules, compatibilityDate: "2026-01-01" };
74
+ });
75
+
76
+ await worker.getEntrypoint().fetch(request);
77
+ ```
78
+
79
+ Serve static assets from the standalone `./assets` entry (no bundler graph
80
+ required):
81
+
82
+ ```ts
83
+ import {
84
+ handleAssetRequest,
85
+ buildAssets
86
+ } from "@fungi.computer/cloudflare-worker-bundler/assets";
87
+
88
+ const { manifest, storage } = await buildAssets({
89
+ "/index.html": "<h1>Hello</h1>"
90
+ });
91
+
92
+ export default {
93
+ async fetch(request) {
94
+ const assetResponse = await handleAssetRequest(request, manifest, storage);
95
+ if (assetResponse) return assetResponse;
96
+ return new Response("API");
97
+ }
98
+ };
99
+ ```
100
+
101
+ `createApp` bundles a server Worker, client-side JavaScript, and static
102
+ assets together in one call; see the
103
+ [upstream README](https://github.com/cloudflare/agents/tree/%40cloudflare/worker-bundler%400.2.3/packages/worker-bundler#readme)
104
+ for `createApp`, the full `createWorker`/`createApp` option tables, the
105
+ `FileSystem` interface (`InMemoryFileSystem`, `DurableObjectKVFileSystem`,
106
+ `DurableObjectRawFileSystem`), the in-process TypeScript language service
107
+ (`./typescript` subpath), asset configuration (HTML/not-found handling,
108
+ redirects, headers, caching), and known limitations — none of that changed in
109
+ this fork beyond the `./assets` entry point described above.
110
+
111
+ ## Where it's used
112
+
113
+ [Oyster](https://github.com/fungi-computer/oyster) uses this fork to bundle
114
+ and serve Worker apps, and uses the `./assets` entry to serve static assets
115
+ without pulling in the bundler.
116
+
117
+ ## License
118
+
119
+ MIT. This package carries the upstream Cloudflare copyright and license
120
+ unchanged — see [LICENSE](./LICENSE) and [NOTICE](./NOTICE).
@@ -0,0 +1,464 @@
1
+ //#region src/mime.ts
2
+ /**
3
+ * MIME type inference from file extensions.
4
+ */
5
+ const MIME_TYPES = {
6
+ ".html": "text/html; charset=utf-8",
7
+ ".htm": "text/html; charset=utf-8",
8
+ ".js": "application/javascript; charset=utf-8",
9
+ ".mjs": "application/javascript; charset=utf-8",
10
+ ".css": "text/css; charset=utf-8",
11
+ ".json": "application/json; charset=utf-8",
12
+ ".png": "image/png",
13
+ ".jpg": "image/jpeg",
14
+ ".jpeg": "image/jpeg",
15
+ ".gif": "image/gif",
16
+ ".svg": "image/svg+xml",
17
+ ".ico": "image/x-icon",
18
+ ".webp": "image/webp",
19
+ ".avif": "image/avif",
20
+ ".woff": "font/woff",
21
+ ".woff2": "font/woff2",
22
+ ".ttf": "font/ttf",
23
+ ".otf": "font/otf",
24
+ ".eot": "application/vnd.ms-fontobject",
25
+ ".mp3": "audio/mpeg",
26
+ ".mp4": "video/mp4",
27
+ ".webm": "video/webm",
28
+ ".ogg": "audio/ogg",
29
+ ".wav": "audio/wav",
30
+ ".pdf": "application/pdf",
31
+ ".xml": "application/xml",
32
+ ".txt": "text/plain; charset=utf-8",
33
+ ".csv": "text/csv; charset=utf-8",
34
+ ".zip": "application/zip",
35
+ ".gz": "application/gzip",
36
+ ".tar": "application/x-tar",
37
+ ".wasm": "application/wasm",
38
+ ".webmanifest": "application/manifest+json",
39
+ ".map": "application/json"
40
+ };
41
+ /**
42
+ * Get the file extension from a path (including the dot).
43
+ */
44
+ function getExtension(path) {
45
+ const lastDot = path.lastIndexOf(".");
46
+ if (lastDot === -1) return "";
47
+ if (lastDot < path.lastIndexOf("/")) return "";
48
+ return path.slice(lastDot).toLowerCase();
49
+ }
50
+ /**
51
+ * Infer MIME type from a file path.
52
+ * Returns undefined if the type is unknown.
53
+ */
54
+ function inferContentType(path) {
55
+ const ext = getExtension(path);
56
+ return MIME_TYPES[ext];
57
+ }
58
+ /**
59
+ * Whether a content type represents a text-based format
60
+ * (used to decide text vs binary module storage).
61
+ */
62
+ function isTextContentType(contentType) {
63
+ return contentType.startsWith("text/") || contentType.includes("json") || contentType.includes("xml") || contentType.includes("javascript") || contentType.includes("svg");
64
+ }
65
+ //#endregion
66
+ //#region src/asset-handler.ts
67
+ /**
68
+ * Asset request handler for serving static assets.
69
+ *
70
+ * Key design: the manifest (routing metadata) is separated from the
71
+ * storage (content retrieval). This lets you plug in any backend —
72
+ * in-memory, KV, R2, Workspace, etc.
73
+ *
74
+ * Inspired by Cloudflare's Workers Static Assets behavior and
75
+ * cloudflare-asset-worker by Timo Wilhelm.
76
+ */
77
+ /**
78
+ * Create an in-memory storage backend from a pathname->content map.
79
+ * This is the zero-config default for small asset sets.
80
+ */
81
+ function createMemoryStorage(assets) {
82
+ const map = new Map(Object.entries(assets));
83
+ return { get(pathname) {
84
+ return Promise.resolve(map.get(pathname) ?? null);
85
+ } };
86
+ }
87
+ /**
88
+ * Normalize user config with defaults.
89
+ */
90
+ function normalizeConfig(config) {
91
+ const staticRedirects = {};
92
+ if (config?.redirects?.static) {
93
+ let lineNumber = 1;
94
+ for (const [path, rule] of Object.entries(config.redirects.static)) staticRedirects[path] = {
95
+ ...rule,
96
+ lineNumber: lineNumber++
97
+ };
98
+ }
99
+ return {
100
+ html_handling: config?.html_handling ?? "auto-trailing-slash",
101
+ not_found_handling: config?.not_found_handling ?? "none",
102
+ redirects: {
103
+ static: staticRedirects,
104
+ dynamic: config?.redirects?.dynamic ?? {}
105
+ },
106
+ headers: config?.headers ?? {}
107
+ };
108
+ }
109
+ /**
110
+ * Compute a simple hash for ETag generation.
111
+ * Uses a fast string hash (FNV-1a) for text, or SHA-256 for binary.
112
+ */
113
+ async function computeETag(content) {
114
+ if (typeof content === "string") {
115
+ let hash = 2166136261;
116
+ for (let i = 0; i < content.length; i++) {
117
+ hash ^= content.charCodeAt(i);
118
+ hash = hash * 16777619 >>> 0;
119
+ }
120
+ return hash.toString(16).padStart(8, "0");
121
+ }
122
+ const hashBuffer = await crypto.subtle.digest("SHA-256", content);
123
+ const hashArray = new Uint8Array(hashBuffer);
124
+ return Array.from(hashArray.subarray(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join("");
125
+ }
126
+ /**
127
+ * Build an AssetManifest from a pathname->content mapping.
128
+ * Only computes metadata (content types, ETags) — doesn't store content.
129
+ */
130
+ async function buildAssetManifest(assets) {
131
+ const manifest = /* @__PURE__ */ new Map();
132
+ const entries = Object.entries(assets);
133
+ await Promise.all(entries.map(async ([pathname, content]) => {
134
+ const contentType = inferContentType(pathname);
135
+ const etag = await computeETag(content);
136
+ manifest.set(pathname, {
137
+ contentType,
138
+ etag
139
+ });
140
+ }));
141
+ return manifest;
142
+ }
143
+ /**
144
+ * Convenience: build both a manifest and an in-memory storage from assets.
145
+ */
146
+ async function buildAssets(assets) {
147
+ return {
148
+ manifest: await buildAssetManifest(assets),
149
+ storage: createMemoryStorage(assets)
150
+ };
151
+ }
152
+ /**
153
+ * Check if a pathname exists in the manifest.
154
+ */
155
+ function exists(manifest, pathname) {
156
+ return manifest.get(pathname);
157
+ }
158
+ const ESCAPE_REGEX_CHARACTERS = /[-/\\^$*+?.()|[\]{}]/g;
159
+ const escapeRegex = (s) => s.replaceAll(ESCAPE_REGEX_CHARACTERS, String.raw`\$&`);
160
+ const PLACEHOLDER_REGEX = /:([A-Za-z]\w*)/g;
161
+ function replacer(str, replacements) {
162
+ for (const [key, value] of Object.entries(replacements)) str = str.replaceAll(`:${key}`, value);
163
+ return str;
164
+ }
165
+ function generateRuleRegExp(rule) {
166
+ rule = rule.split("*").map((s) => escapeRegex(s)).join("(?<splat>.*)");
167
+ const matches = rule.matchAll(PLACEHOLDER_REGEX);
168
+ for (const match of matches) rule = rule.split(match[0]).join(`(?<${match[1]}>[^/]+)`);
169
+ return new RegExp("^" + rule + "$");
170
+ }
171
+ function matchStaticRedirects(config, host, pathname) {
172
+ const withHost = config.redirects.static[`https://${host}${pathname}`];
173
+ const withoutHost = config.redirects.static[pathname];
174
+ if (withHost && withoutHost) return withHost.lineNumber < withoutHost.lineNumber ? withHost : withoutHost;
175
+ return withHost || withoutHost;
176
+ }
177
+ function matchDynamicRedirects(config, request) {
178
+ const { pathname } = new URL(request.url);
179
+ for (const [pattern, rule] of Object.entries(config.redirects.dynamic)) try {
180
+ const result = generateRuleRegExp(pattern).exec(pathname);
181
+ if (result) {
182
+ const target = replacer(rule.to, result.groups || {}).trim();
183
+ return {
184
+ status: rule.status,
185
+ to: target
186
+ };
187
+ }
188
+ } catch {}
189
+ }
190
+ function handleRedirects(request, config) {
191
+ const url = new URL(request.url);
192
+ const { search, host } = url;
193
+ let { pathname } = url;
194
+ const staticMatch = matchStaticRedirects(config, host, pathname);
195
+ const dynamicMatch = staticMatch ? void 0 : matchDynamicRedirects(config, request);
196
+ const match = staticMatch ?? dynamicMatch;
197
+ let proxied = false;
198
+ if (match) if (match.status === 200) {
199
+ pathname = new URL(match.to, request.url).pathname;
200
+ proxied = true;
201
+ } else {
202
+ const destination = new URL(match.to, request.url);
203
+ const location = destination.origin === url.origin ? `${destination.pathname}${destination.search || search}${destination.hash}` : `${destination.href}`;
204
+ return new Response(null, {
205
+ status: match.status,
206
+ headers: { Location: location }
207
+ });
208
+ }
209
+ return {
210
+ proxied,
211
+ pathname
212
+ };
213
+ }
214
+ function generateGlobRegExp(pattern) {
215
+ const escaped = pattern.split("*").map((s) => escapeRegex(s)).join(".*");
216
+ return new RegExp("^" + escaped + "$");
217
+ }
218
+ function attachCustomHeaders(request, response, config) {
219
+ if (Object.keys(config.headers).length === 0) return response;
220
+ const { pathname } = new URL(request.url);
221
+ const setMap = /* @__PURE__ */ new Set();
222
+ for (const [pattern, rules] of Object.entries(config.headers)) {
223
+ try {
224
+ if (!generateGlobRegExp(pattern).test(pathname)) continue;
225
+ } catch {
226
+ continue;
227
+ }
228
+ if (rules.unset) for (const key of rules.unset) response.headers.delete(key);
229
+ if (rules.set) for (const [key, value] of Object.entries(rules.set)) if (setMap.has(key.toLowerCase())) response.headers.append(key, value);
230
+ else {
231
+ response.headers.set(key, value);
232
+ setMap.add(key.toLowerCase());
233
+ }
234
+ }
235
+ return response;
236
+ }
237
+ function decodePath(pathname) {
238
+ return pathname.split("/").map((segment) => {
239
+ try {
240
+ return decodeURIComponent(segment);
241
+ } catch {
242
+ return segment;
243
+ }
244
+ }).join("/").replaceAll(/\/+/g, "/");
245
+ }
246
+ function encodePath(pathname) {
247
+ return pathname.split("/").map((segment) => {
248
+ try {
249
+ return encodeURIComponent(segment);
250
+ } catch {
251
+ return segment;
252
+ }
253
+ }).join("/");
254
+ }
255
+ function getIntent(pathname, manifest, config, skipRedirects = false, acceptsHtml = true) {
256
+ switch (config.html_handling) {
257
+ case "auto-trailing-slash": return htmlAutoTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml);
258
+ case "force-trailing-slash": return htmlForceTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml);
259
+ case "drop-trailing-slash": return htmlDropTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml);
260
+ case "none": return htmlNone(pathname, manifest, config, acceptsHtml);
261
+ }
262
+ }
263
+ function assetIntent(pathname, meta, status = 200) {
264
+ return {
265
+ type: "asset",
266
+ pathname,
267
+ meta,
268
+ status
269
+ };
270
+ }
271
+ function redirectIntent(to) {
272
+ return {
273
+ type: "redirect",
274
+ to
275
+ };
276
+ }
277
+ /**
278
+ * Safe redirect: only redirect if the file exists and the destination
279
+ * itself resolves to the same asset (avoids redirect loops).
280
+ */
281
+ function safeRedirect(file, destination, manifest, config, skip) {
282
+ if (skip) return void 0;
283
+ if (!exists(manifest, destination)) {
284
+ const intent = getIntent(destination, manifest, config, true);
285
+ if (intent?.type === "asset" && intent.meta.etag === exists(manifest, file)?.etag) return redirectIntent(destination);
286
+ }
287
+ }
288
+ function htmlAutoTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml) {
289
+ let meta;
290
+ let redirect;
291
+ const exactMeta = exists(manifest, pathname);
292
+ if (pathname.endsWith("/index")) {
293
+ if (exactMeta) return assetIntent(pathname, exactMeta);
294
+ if (redirect = safeRedirect(`${pathname}.html`, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
295
+ if (redirect = safeRedirect(`${pathname.slice(0, -6)}.html`, pathname.slice(0, -6), manifest, config, skipRedirects)) return redirect;
296
+ } else if (pathname.endsWith("/index.html")) {
297
+ if (redirect = safeRedirect(pathname, pathname.slice(0, -10), manifest, config, skipRedirects)) return redirect;
298
+ if (redirect = safeRedirect(`${pathname.slice(0, -11)}.html`, pathname.slice(0, -11), manifest, config, skipRedirects)) return redirect;
299
+ } else if (pathname.endsWith("/")) {
300
+ const indexPath = `${pathname}index.html`;
301
+ if (meta = exists(manifest, indexPath)) return assetIntent(indexPath, meta);
302
+ if (redirect = safeRedirect(`${pathname.slice(0, -1)}.html`, pathname.slice(0, -1), manifest, config, skipRedirects)) return redirect;
303
+ } else if (pathname.endsWith(".html")) {
304
+ if (redirect = safeRedirect(pathname, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
305
+ if (redirect = safeRedirect(`${pathname.slice(0, -5)}/index.html`, `${pathname.slice(0, -5)}/`, manifest, config, skipRedirects)) return redirect;
306
+ }
307
+ if (exactMeta) return assetIntent(pathname, exactMeta);
308
+ const htmlPath = `${pathname}.html`;
309
+ if (meta = exists(manifest, htmlPath)) return assetIntent(htmlPath, meta);
310
+ if (redirect = safeRedirect(`${pathname}/index.html`, `${pathname}/`, manifest, config, skipRedirects)) return redirect;
311
+ return notFound(pathname, manifest, config, acceptsHtml);
312
+ }
313
+ function htmlForceTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml) {
314
+ let meta;
315
+ let redirect;
316
+ const exactMeta = exists(manifest, pathname);
317
+ if (pathname.endsWith("/index")) {
318
+ if (exactMeta) return assetIntent(pathname, exactMeta);
319
+ if (redirect = safeRedirect(`${pathname}.html`, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
320
+ if (redirect = safeRedirect(`${pathname.slice(0, -6)}.html`, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
321
+ } else if (pathname.endsWith("/index.html")) {
322
+ if (redirect = safeRedirect(pathname, pathname.slice(0, -10), manifest, config, skipRedirects)) return redirect;
323
+ if (redirect = safeRedirect(`${pathname.slice(0, -11)}.html`, pathname.slice(0, -10), manifest, config, skipRedirects)) return redirect;
324
+ } else if (pathname.endsWith("/")) {
325
+ let p = `${pathname}index.html`;
326
+ if (meta = exists(manifest, p)) return assetIntent(p, meta);
327
+ p = `${pathname.slice(0, -1)}.html`;
328
+ if (meta = exists(manifest, p)) return assetIntent(p, meta);
329
+ } else if (pathname.endsWith(".html")) {
330
+ if (redirect = safeRedirect(pathname, `${pathname.slice(0, -5)}/`, manifest, config, skipRedirects)) return redirect;
331
+ if (exactMeta) return assetIntent(pathname, exactMeta);
332
+ if (redirect = safeRedirect(`${pathname.slice(0, -5)}/index.html`, `${pathname.slice(0, -5)}/`, manifest, config, skipRedirects)) return redirect;
333
+ }
334
+ if (exactMeta) return assetIntent(pathname, exactMeta);
335
+ if (redirect = safeRedirect(`${pathname}.html`, `${pathname}/`, manifest, config, skipRedirects)) return redirect;
336
+ if (redirect = safeRedirect(`${pathname}/index.html`, `${pathname}/`, manifest, config, skipRedirects)) return redirect;
337
+ return notFound(pathname, manifest, config, acceptsHtml);
338
+ }
339
+ function htmlDropTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml) {
340
+ let meta;
341
+ let redirect;
342
+ const exactMeta = exists(manifest, pathname);
343
+ if (pathname.endsWith("/index")) {
344
+ if (exactMeta) return assetIntent(pathname, exactMeta);
345
+ if (pathname === "/index") {
346
+ if (redirect = safeRedirect("/index.html", "/", manifest, config, skipRedirects)) return redirect;
347
+ } else {
348
+ if (redirect = safeRedirect(`${pathname.slice(0, -6)}.html`, pathname.slice(0, -6), manifest, config, skipRedirects)) return redirect;
349
+ if (redirect = safeRedirect(`${pathname}.html`, pathname.slice(0, -6), manifest, config, skipRedirects)) return redirect;
350
+ }
351
+ } else if (pathname.endsWith("/index.html")) if (pathname === "/index.html") {
352
+ if (redirect = safeRedirect("/index.html", "/", manifest, config, skipRedirects)) return redirect;
353
+ } else {
354
+ if (redirect = safeRedirect(pathname, pathname.slice(0, -11), manifest, config, skipRedirects)) return redirect;
355
+ if (exactMeta) return assetIntent(pathname, exactMeta);
356
+ if (redirect = safeRedirect(`${pathname.slice(0, -11)}.html`, pathname.slice(0, -11), manifest, config, skipRedirects)) return redirect;
357
+ }
358
+ else if (pathname.endsWith("/")) if (pathname === "/") {
359
+ if (meta = exists(manifest, "/index.html")) return assetIntent("/index.html", meta);
360
+ } else {
361
+ if (redirect = safeRedirect(`${pathname.slice(0, -1)}.html`, pathname.slice(0, -1), manifest, config, skipRedirects)) return redirect;
362
+ if (redirect = safeRedirect(`${pathname.slice(0, -1)}/index.html`, pathname.slice(0, -1), manifest, config, skipRedirects)) return redirect;
363
+ }
364
+ else if (pathname.endsWith(".html")) {
365
+ if (redirect = safeRedirect(pathname, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
366
+ if (redirect = safeRedirect(`${pathname.slice(0, -5)}/index.html`, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
367
+ }
368
+ if (exactMeta) return assetIntent(pathname, exactMeta);
369
+ let p = `${pathname}.html`;
370
+ if (meta = exists(manifest, p)) return assetIntent(p, meta);
371
+ p = `${pathname}/index.html`;
372
+ if (meta = exists(manifest, p)) return assetIntent(p, meta);
373
+ return notFound(pathname, manifest, config, acceptsHtml);
374
+ }
375
+ function htmlNone(pathname, manifest, config, acceptsHtml) {
376
+ const meta = exists(manifest, pathname);
377
+ return meta ? assetIntent(pathname, meta) : notFound(pathname, manifest, config, acceptsHtml);
378
+ }
379
+ function notFound(pathname, manifest, config, acceptsHtml = true) {
380
+ switch (config.not_found_handling) {
381
+ case "single-page-application": {
382
+ if (!acceptsHtml) return void 0;
383
+ const meta = exists(manifest, "/index.html");
384
+ if (meta) return assetIntent("/index.html", meta, 200);
385
+ return;
386
+ }
387
+ case "404-page": {
388
+ let cwd = pathname;
389
+ while (cwd) {
390
+ cwd = cwd.slice(0, cwd.lastIndexOf("/"));
391
+ const p = `${cwd}/404.html`;
392
+ const meta = exists(manifest, p);
393
+ if (meta) return assetIntent(p, meta, 404);
394
+ }
395
+ return;
396
+ }
397
+ default: return;
398
+ }
399
+ }
400
+ const CACHE_CONTROL_REVALIDATE = "public, max-age=0, must-revalidate";
401
+ const CACHE_CONTROL_IMMUTABLE = "public, max-age=31536000, immutable";
402
+ function getCacheControl(pathname) {
403
+ if (/\.[a-f0-9]{8,}\.\w+$/.test(pathname)) return CACHE_CONTROL_IMMUTABLE;
404
+ return CACHE_CONTROL_REVALIDATE;
405
+ }
406
+ /**
407
+ * Handle an asset request. Returns a Response if an asset matches,
408
+ * or null if the request should fall through to the user's Worker.
409
+ *
410
+ * @param request - The incoming HTTP request
411
+ * @param manifest - Asset manifest (pathname -> metadata)
412
+ * @param storage - Storage backend for fetching content
413
+ * @param config - Asset serving configuration
414
+ */
415
+ async function handleAssetRequest(request, manifest, storage, config) {
416
+ const normalized = normalizeConfig(config);
417
+ const method = request.method.toUpperCase();
418
+ if (!["GET", "HEAD"].includes(method)) return null;
419
+ const redirectResult = handleRedirects(request, normalized);
420
+ if (redirectResult instanceof Response) return attachCustomHeaders(request, redirectResult, normalized);
421
+ const { pathname } = redirectResult;
422
+ const decodedPathname = decodePath(pathname);
423
+ const intent = getIntent(decodedPathname, manifest, normalized, false, (request.headers.get("Accept") || "").includes("text/html"));
424
+ if (!intent) return null;
425
+ if (intent.type === "redirect") {
426
+ const url = new URL(request.url);
427
+ const encodedDest = encodePath(intent.to);
428
+ return attachCustomHeaders(request, new Response(null, {
429
+ status: 307,
430
+ headers: { Location: encodedDest + url.search }
431
+ }), normalized);
432
+ }
433
+ const encodedPathname = encodePath(decodedPathname);
434
+ if (encodedPathname !== pathname) {
435
+ const url = new URL(request.url);
436
+ return attachCustomHeaders(request, new Response(null, {
437
+ status: 307,
438
+ headers: { Location: encodedPathname + url.search }
439
+ }), normalized);
440
+ }
441
+ const { pathname: assetPath, meta, status } = intent;
442
+ const strongETag = `"${meta.etag}"`;
443
+ const weakETag = `W/${strongETag}`;
444
+ const ifNoneMatch = request.headers.get("If-None-Match") || "";
445
+ const eTags = new Set(ifNoneMatch.split(",").map((t) => t.trim()));
446
+ const headers = new Headers();
447
+ headers.set("ETag", strongETag);
448
+ if (meta.contentType) headers.set("Content-Type", meta.contentType);
449
+ headers.set("Cache-Control", getCacheControl(decodedPathname));
450
+ if (eTags.has(weakETag) || eTags.has(strongETag)) return attachCustomHeaders(request, new Response(null, {
451
+ status: 304,
452
+ headers
453
+ }), normalized);
454
+ let body = null;
455
+ if (method !== "HEAD") body = await storage.get(assetPath);
456
+ return attachCustomHeaders(request, new Response(body, {
457
+ status,
458
+ headers
459
+ }), normalized);
460
+ }
461
+ //#endregion
462
+ export { inferContentType as a, handleAssetRequest as i, buildAssets as n, isTextContentType as o, createMemoryStorage as r, buildAssetManifest as t };
463
+
464
+ //# sourceMappingURL=assets-BlVFmWBF.js.map