@takazudo/zfb-adapter-cloudflare 0.1.0-next.73 → 0.1.0-next.75
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 +96 -18
- package/bin/cli.mjs +9 -4
- package/dist/build.d.ts +17 -12
- package/dist/build.js +14 -9
- package/dist/build.js.map +1 -1
- package/dist/emit-worker.mjs +16 -5
- package/dist/index.d.ts +0 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/worker-wrapper.mjs +101 -22
- package/package.json +3 -2
- package/src/emit-worker.mjs +16 -5
- package/src/worker-wrapper.mjs +101 -22
- package/dist/build.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> Rust-built static-site engine for Astro and Next.js users — millisecond rebuilds, single binary.
|
|
4
4
|
|
|
5
|
-
The Cloudflare
|
|
6
|
-
|
|
7
|
-
`
|
|
8
|
-
`AsyncLocalStorage`.
|
|
5
|
+
The Cloudflare adapter for [zfb][zfb-site], targeting **Workers Static
|
|
6
|
+
Assets** (also deployable to Cloudflare Pages advanced mode). It wraps the
|
|
7
|
+
`@takazudo/zfb-runtime` page router into a Worker entry (`_worker.js`),
|
|
8
|
+
threading `(env, ctx)` through to user code via `AsyncLocalStorage`.
|
|
9
9
|
|
|
10
10
|
This package is the Cloudflare half of the SSR adapter contract. Other
|
|
11
11
|
targets (Node, Netlify, …) will land as sibling `@takazudo/zfb-adapter-*`
|
|
@@ -27,7 +27,7 @@ npm install --save-dev @takazudo/zfb-adapter-cloudflare
|
|
|
27
27
|
|
|
28
28
|
In `zfb.config.json`:
|
|
29
29
|
|
|
30
|
-
```
|
|
30
|
+
```json
|
|
31
31
|
{
|
|
32
32
|
"framework": "preact",
|
|
33
33
|
"adapter": "@takazudo/zfb-adapter-cloudflare"
|
|
@@ -68,30 +68,108 @@ lifecycle (`wrangler d1 create`, migrations, preview-vs-prod).
|
|
|
68
68
|
|
|
69
69
|
1. Render every SSG page (`prerender !== false`) into static HTML under
|
|
70
70
|
`dist/`.
|
|
71
|
-
2. Hand the SSR bundle to this adapter, which writes
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
2. Hand the SSR bundle to this adapter, which writes `dist/_worker.js`
|
|
72
|
+
(the wrapper), `dist/_zfb_inner.mjs` (the bundle), and
|
|
73
|
+
`dist/.assetsignore` (see below) — ready to deploy as a Worker with
|
|
74
|
+
Static Assets, or via Cloudflare Pages advanced mode.
|
|
75
|
+
|
|
76
|
+
## `wrangler.toml`
|
|
77
|
+
|
|
78
|
+
```toml
|
|
79
|
+
name = "my-site"
|
|
80
|
+
main = "./dist/_worker.js"
|
|
81
|
+
compatibility_date = "2024-09-23"
|
|
82
|
+
compatibility_flags = ["nodejs_compat"]
|
|
83
|
+
|
|
84
|
+
[assets]
|
|
85
|
+
directory = "./dist"
|
|
86
|
+
binding = "ASSETS" # optional — lets the Worker probe assets itself
|
|
87
|
+
not_found_handling = "404-page"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- `compatibility_flags = ["nodejs_compat"]` is **required**. The wrapper
|
|
91
|
+
imports `node:async_hooks` to thread `(env, ctx)` into your route
|
|
92
|
+
handlers; without this flag the Worker fails at runtime with
|
|
93
|
+
`No such module "node:async_hooks"`.
|
|
94
|
+
- `not_found_handling = "404-page"` is recommended. With it, an
|
|
95
|
+
unmatched path makes the asset layer serve your styled `dist/404.html`
|
|
96
|
+
(built from `pages/404.tsx`) with a `404` status. The `_worker.js`
|
|
97
|
+
wrapper still probes the inner Worker for genuinely dynamic
|
|
98
|
+
`prerender = false` routes, but its **404 precedence** is:
|
|
99
|
+
- inner returns a non-404 (a real dynamic route) → the inner response
|
|
100
|
+
wins;
|
|
101
|
+
- inner also 404s with only the framework default body (Hono's default
|
|
102
|
+
`text/plain` "404 Not Found", or a bare 404 with no content-type) →
|
|
103
|
+
the **styled `404.html` wins**, so visitors see your designed 404 page
|
|
104
|
+
instead of plain text;
|
|
105
|
+
- inner 404s with any other content-type → that **deliberate response is
|
|
106
|
+
preserved** and the styled page does not stomp it: a `text/html` 404 is
|
|
107
|
+
a rendered custom not-found page (e.g. a `prerender = false` route
|
|
108
|
+
SSRing its own 404), and an `application/json` 404 is an intentional
|
|
109
|
+
API error.
|
|
110
|
+
|
|
111
|
+
A bare `text/plain` API 404 is indistinguishable from the framework
|
|
112
|
+
default and yields to the styled page — use `application/json` for API
|
|
113
|
+
errors you want preserved.
|
|
114
|
+
|
|
115
|
+
Under `not_found_handling = "none"` the asset 404 has no styled body, so
|
|
116
|
+
the inner Worker's plain 404 is always shown. Avoid
|
|
117
|
+
`single-page-application`: it returns `index.html` for every unresolved
|
|
118
|
+
path _before_ the Worker ever sees the request, which breaks dynamic
|
|
119
|
+
routes like `pages/api/*.tsx`.
|
|
120
|
+
|
|
121
|
+
- `[assets] binding = "ASSETS"` is optional but recommended — it lets
|
|
122
|
+
the `_worker.js` wrapper probe `env.ASSETS.fetch()` directly for
|
|
123
|
+
GET/HEAD requests, matching the SSG-vs-SSR precedence described
|
|
124
|
+
below.
|
|
125
|
+
|
|
126
|
+
## `wrangler dev` / `wrangler deploy`
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
zfb build
|
|
130
|
+
wrangler dev # local Worker + assets simulation
|
|
131
|
+
wrangler deploy # ship it
|
|
132
|
+
```
|
|
74
133
|
|
|
75
|
-
##
|
|
134
|
+
## `.assetsignore`
|
|
76
135
|
|
|
77
|
-
The
|
|
78
|
-
`
|
|
136
|
+
The adapter emits `dist/.assetsignore` alongside `_worker.js` and
|
|
137
|
+
`_zfb_inner.mjs`:
|
|
79
138
|
|
|
80
139
|
```
|
|
81
|
-
|
|
140
|
+
_worker.js
|
|
141
|
+
_zfb_inner.mjs
|
|
82
142
|
```
|
|
83
143
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
144
|
+
This excludes the wrapper and the inner SSR bundle from the asset
|
|
145
|
+
upload, so they are reachable only through the Worker's own module
|
|
146
|
+
graph — never served as public static files. Without it, a request for
|
|
147
|
+
`/_worker.js` or `/_zfb_inner.mjs` would serve your server code as a
|
|
148
|
+
plain-text download.
|
|
149
|
+
|
|
150
|
+
**Precedence:** zfb copies your project's `public/` directory into
|
|
151
|
+
`dist/` _after_ running this adapter. If your `public/` directory
|
|
152
|
+
contains its own `.assetsignore`, it overrides the one this adapter
|
|
153
|
+
emits — the adapter's excludes are silently dropped. Only add a
|
|
154
|
+
`public/.assetsignore` if you have additional paths to exclude and
|
|
155
|
+
include the two lines above yourself.
|
|
156
|
+
|
|
157
|
+
## Cloudflare Pages compatibility
|
|
158
|
+
|
|
159
|
+
The same `dist/` output is still deployable to Cloudflare Pages
|
|
160
|
+
advanced mode (a `_worker.js` at the root of the Pages output directory
|
|
161
|
+
is Pages' equivalent convention). The `_worker.js` wrapper dispatches
|
|
162
|
+
requests identically on both platforms; the only platform-visible
|
|
163
|
+
difference is that trailing-slash asset redirects come back as `307`
|
|
164
|
+
on Workers Static Assets versus `308` on Pages.
|
|
87
165
|
|
|
88
|
-
## Why two files
|
|
166
|
+
## Why two bundle files instead of one
|
|
89
167
|
|
|
90
168
|
The wrapper imports the inner bundle by relative path
|
|
91
169
|
(`./_zfb_inner.mjs`) instead of inlining it, so the adapter package
|
|
92
170
|
itself does not need to ship an esbuild binary. Workerd's Module
|
|
93
|
-
loader resolves relative ESM imports inside
|
|
94
|
-
|
|
171
|
+
loader resolves relative ESM imports inside the `_worker.js` directory
|
|
172
|
+
layout.
|
|
95
173
|
|
|
96
174
|
## Why AsyncLocalStorage on a globalThis registry
|
|
97
175
|
|
package/bin/cli.mjs
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
//
|
|
7
7
|
// bundle <input> --outdir <dir>
|
|
8
8
|
//
|
|
9
|
-
// Wrap the input ESM bundle into a Cloudflare
|
|
10
|
-
// placed under <dir
|
|
9
|
+
// Wrap the input ESM bundle into a Cloudflare Workers Static Assets
|
|
10
|
+
// (Pages-compatible) `_worker.js` placed under <dir>, alongside a
|
|
11
|
+
// `.assetsignore` that excludes the wrapper and inner bundle from
|
|
12
|
+
// the asset upload. The input bundle is the file `zfb_build`'s
|
|
11
13
|
// bundler emits; <dir> is typically the project's `dist/`.
|
|
12
14
|
//
|
|
13
15
|
// The CLI is intentionally tiny and dependency-free. It imports the
|
|
@@ -50,7 +52,8 @@ function printUsage() {
|
|
|
50
52
|
zfb-adapter-cloudflare bundle <input> --outdir <dir>
|
|
51
53
|
|
|
52
54
|
Wrap an ESM bundle (the output of zfb-build's bundler) into a
|
|
53
|
-
Cloudflare
|
|
55
|
+
Cloudflare Workers Static Assets \`_worker.js\` placed under <dir>
|
|
56
|
+
(also deployable to Cloudflare Pages advanced mode).
|
|
54
57
|
|
|
55
58
|
Options:
|
|
56
59
|
--outdir <dir> Output directory. Required.
|
|
@@ -138,7 +141,9 @@ async function main() {
|
|
|
138
141
|
inputBundlePath: inputAbs,
|
|
139
142
|
outdir: outdirAbs,
|
|
140
143
|
});
|
|
141
|
-
process.stdout.write(
|
|
144
|
+
process.stdout.write(
|
|
145
|
+
`wrote ${out.workerPath}\nwrote ${out.innerBundlePath}\nwrote ${out.assetsIgnorePath}\n`,
|
|
146
|
+
);
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
main().catch((err) => {
|
package/dist/build.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ export interface EmitWorkerInput {
|
|
|
18
18
|
readonly inputBundlePath: string;
|
|
19
19
|
/**
|
|
20
20
|
* Absolute path to the output directory. The emitter creates it if
|
|
21
|
-
* missing and writes `_worker.js
|
|
22
|
-
*
|
|
21
|
+
* missing and writes `_worker.js`, `_zfb_inner.mjs` (the copied input
|
|
22
|
+
* bundle), and `.assetsignore` into it.
|
|
23
23
|
*/
|
|
24
24
|
readonly outdir: string;
|
|
25
25
|
}
|
|
@@ -30,24 +30,29 @@ export interface EmitWorkerInput {
|
|
|
30
30
|
export interface EmitWorkerOutput {
|
|
31
31
|
readonly workerPath: string;
|
|
32
32
|
readonly innerBundlePath: string;
|
|
33
|
+
readonly assetsIgnorePath: string;
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
|
-
* Emit a Cloudflare Pages `_worker.js`
|
|
36
|
+
* Emit a Cloudflare Workers Static Assets (Pages-compatible) `_worker.js`
|
|
37
|
+
* that wraps the zfb input bundle.
|
|
36
38
|
*
|
|
37
|
-
* Output shape (
|
|
39
|
+
* Output shape (three files in `outdir`):
|
|
38
40
|
*
|
|
39
|
-
* _worker.js — entry
|
|
41
|
+
* _worker.js — Worker entry point (`main` in wrangler.toml, or the
|
|
42
|
+
* Pages advanced-mode convention)
|
|
40
43
|
* _zfb_inner.mjs — the input bundle, copied verbatim
|
|
44
|
+
* .assetsignore — excludes the two files above from the asset upload
|
|
45
|
+
* so they are only reachable through the Worker's
|
|
46
|
+
* module graph, never served as a public static file
|
|
41
47
|
*
|
|
42
48
|
* The wrapper imports the inner bundle via the relative path
|
|
43
49
|
* `./_zfb_inner.mjs`. Workerd's Module loader resolves relative ESM
|
|
44
|
-
* imports inside
|
|
45
|
-
*
|
|
50
|
+
* imports inside the `_worker.js` directory, so this layout works
|
|
51
|
+
* without re-bundling.
|
|
46
52
|
*
|
|
47
|
-
* Why two files instead of one: re-bundling here would require a
|
|
48
|
-
* esbuild pass and would force the adapter to ship its own
|
|
49
|
-
* binary slot. The two-file layout keeps the adapter
|
|
50
|
-
* runtime — it is just `node:fs` glue.
|
|
53
|
+
* Why two bundle files instead of one: re-bundling here would require a
|
|
54
|
+
* second esbuild pass and would force the adapter to ship its own
|
|
55
|
+
* esbuild binary slot. The two-file layout keeps the adapter
|
|
56
|
+
* dependency-free at runtime — it is just `node:fs` glue.
|
|
51
57
|
*/
|
|
52
58
|
export declare function emitWorker(input: EmitWorkerInput): Promise<EmitWorkerOutput>;
|
|
53
|
-
//# sourceMappingURL=build.d.ts.map
|
package/dist/build.js
CHANGED
|
@@ -20,22 +20,27 @@ import { emitWorker as _emitWorker } from "./emit-worker.mjs";
|
|
|
20
20
|
*/
|
|
21
21
|
export const WORKER_WRAPPER_SOURCE = _wrapper;
|
|
22
22
|
/**
|
|
23
|
-
* Emit a Cloudflare Pages `_worker.js`
|
|
23
|
+
* Emit a Cloudflare Workers Static Assets (Pages-compatible) `_worker.js`
|
|
24
|
+
* that wraps the zfb input bundle.
|
|
24
25
|
*
|
|
25
|
-
* Output shape (
|
|
26
|
+
* Output shape (three files in `outdir`):
|
|
26
27
|
*
|
|
27
|
-
* _worker.js — entry
|
|
28
|
+
* _worker.js — Worker entry point (`main` in wrangler.toml, or the
|
|
29
|
+
* Pages advanced-mode convention)
|
|
28
30
|
* _zfb_inner.mjs — the input bundle, copied verbatim
|
|
31
|
+
* .assetsignore — excludes the two files above from the asset upload
|
|
32
|
+
* so they are only reachable through the Worker's
|
|
33
|
+
* module graph, never served as a public static file
|
|
29
34
|
*
|
|
30
35
|
* The wrapper imports the inner bundle via the relative path
|
|
31
36
|
* `./_zfb_inner.mjs`. Workerd's Module loader resolves relative ESM
|
|
32
|
-
* imports inside
|
|
33
|
-
*
|
|
37
|
+
* imports inside the `_worker.js` directory, so this layout works
|
|
38
|
+
* without re-bundling.
|
|
34
39
|
*
|
|
35
|
-
* Why two files instead of one: re-bundling here would require a
|
|
36
|
-
* esbuild pass and would force the adapter to ship its own
|
|
37
|
-
* binary slot. The two-file layout keeps the adapter
|
|
38
|
-
* runtime — it is just `node:fs` glue.
|
|
40
|
+
* Why two bundle files instead of one: re-bundling here would require a
|
|
41
|
+
* second esbuild pass and would force the adapter to ship its own
|
|
42
|
+
* esbuild binary slot. The two-file layout keeps the adapter
|
|
43
|
+
* dependency-free at runtime — it is just `node:fs` glue.
|
|
39
44
|
*/
|
|
40
45
|
export async function emitWorker(input) {
|
|
41
46
|
return _emitWorker({
|
package/dist/build.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,0EAA0E;AAC1E,kEAAkE;AAClE,sEAAsE;AACtE,+DAA+D;AAC/D,EAAE;AACF,yEAAyE;AACzE,mEAAmE;AAEnE,0EAA0E;AAC1E,sCAAsC;AACtC,OAAO,EAAE,qBAAqB,IAAI,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACzE,uEAAuE;AACvE,gEAAgE;AAChE,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE9D;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAW,QAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,0EAA0E;AAC1E,kEAAkE;AAClE,sEAAsE;AACtE,+DAA+D;AAC/D,EAAE;AACF,yEAAyE;AACzE,mEAAmE;AAEnE,0EAA0E;AAC1E,sCAAsC;AACtC,OAAO,EAAE,qBAAqB,IAAI,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACzE,uEAAuE;AACvE,gEAAgE;AAChE,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE9D;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAW,QAAkB,CAAC;AAgChE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,KAAsB;IACrD,OAAO,WAAW,CAAC;QACjB,eAAe,EAAE,KAAK,CAAC,eAAe;QACtC,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,mBAAmB,EAAE,qBAAqB;KAC3C,CAA8B,CAAC;AAClC,CAAC","sourcesContent":["// `@takazudo/zfb-adapter-cloudflare/build` — Node-only build helpers.\n//\n// This sub-entry is intentionally **not** imported by the Workers-runtime\n// entry (`./`). Code in this module may freely use Node built-ins\n// (`node:fs`, `node:path`, …) because it only ever runs in a Node 22+\n// build environment, never inside a Cloudflare Worker isolate.\n//\n// The `./` entry (`src/index.ts`) exports only Workers-runtime-safe code\n// (AsyncLocalStorage helpers) that can be bundled into the worker.\n\n// @ts-expect-error worker-wrapper.mjs has no declaration file; the export\n// shape is narrowed explicitly below.\nimport { WORKER_WRAPPER_SOURCE as _wrapper } from \"./worker-wrapper.mjs\";\n// @ts-expect-error emit-worker.mjs has no declaration file; the export\n// shape is narrowed via EmitWorkerInput/EmitWorkerOutput below.\nimport { emitWorker as _emitWorker } from \"./emit-worker.mjs\";\n\n/**\n * The wrapper source written to `_worker.js`. Imported from the single\n * canonical `.mjs` file so `src/build.ts` and `bin/cli.mjs` always stay\n * in sync without any duplication.\n */\nexport const WORKER_WRAPPER_SOURCE: string = _wrapper as string;\n\n/**\n * Inputs to [`emitWorker`].\n */\nexport interface EmitWorkerInput {\n /**\n * Absolute path to the input ESM bundle produced by `zfb-build`. The\n * bundle must export a Workers-shaped `default { fetch: (request) =>\n * Promise<Response> }` (this is the contract `zfb_build::bundler`\n * pins). The file is copied verbatim next to the emitted wrapper so\n * relative imports inside it keep resolving.\n */\n readonly inputBundlePath: string;\n /**\n * Absolute path to the output directory. The emitter creates it if\n * missing and writes `_worker.js`, `_zfb_inner.mjs` (the copied input\n * bundle), and `.assetsignore` into it.\n */\n readonly outdir: string;\n}\n\n/**\n * Output paths the emitter produced. Returned for callers that want to\n * log them (the Rust orchestrator surfaces them in build output).\n */\nexport interface EmitWorkerOutput {\n readonly workerPath: string;\n readonly innerBundlePath: string;\n readonly assetsIgnorePath: string;\n}\n\n/**\n * Emit a Cloudflare Workers Static Assets (Pages-compatible) `_worker.js`\n * that wraps the zfb input bundle.\n *\n * Output shape (three files in `outdir`):\n *\n * _worker.js — Worker entry point (`main` in wrangler.toml, or the\n * Pages advanced-mode convention)\n * _zfb_inner.mjs — the input bundle, copied verbatim\n * .assetsignore — excludes the two files above from the asset upload\n * so they are only reachable through the Worker's\n * module graph, never served as a public static file\n *\n * The wrapper imports the inner bundle via the relative path\n * `./_zfb_inner.mjs`. Workerd's Module loader resolves relative ESM\n * imports inside the `_worker.js` directory, so this layout works\n * without re-bundling.\n *\n * Why two bundle files instead of one: re-bundling here would require a\n * second esbuild pass and would force the adapter to ship its own\n * esbuild binary slot. The two-file layout keeps the adapter\n * dependency-free at runtime — it is just `node:fs` glue.\n */\nexport async function emitWorker(input: EmitWorkerInput): Promise<EmitWorkerOutput> {\n return _emitWorker({\n inputBundlePath: input.inputBundlePath,\n outdir: input.outdir,\n workerWrapperSource: WORKER_WRAPPER_SOURCE,\n }) as Promise<EmitWorkerOutput>;\n}\n"]}
|
package/dist/emit-worker.mjs
CHANGED
|
@@ -12,16 +12,24 @@
|
|
|
12
12
|
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
13
13
|
import { join, resolve } from "node:path";
|
|
14
14
|
|
|
15
|
+
// `.assetsignore` tells the Workers Static Assets uploader (and Pages'
|
|
16
|
+
// asset server) to skip these two files, so they are only ever reachable
|
|
17
|
+
// through the Worker's module graph, not served as public static assets.
|
|
18
|
+
const ASSETS_IGNORE_CONTENT = "_worker.js\n_zfb_inner.mjs\n";
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
|
-
* Emit a Cloudflare Pages `_worker.js`
|
|
21
|
+
* Emit a Cloudflare Workers Static Assets (Pages-compatible) `_worker.js`
|
|
22
|
+
* that wraps the zfb input bundle.
|
|
17
23
|
*
|
|
18
|
-
* Output shape (
|
|
24
|
+
* Output shape (three files in `outdir`):
|
|
19
25
|
*
|
|
20
|
-
* _worker.js — entry
|
|
26
|
+
* _worker.js — Worker entry point (`main` in wrangler.toml, or the
|
|
27
|
+
* Pages advanced-mode convention)
|
|
21
28
|
* _zfb_inner.mjs — the input bundle, copied verbatim
|
|
29
|
+
* .assetsignore — excludes the two files above from the asset upload
|
|
22
30
|
*
|
|
23
31
|
* @param {{ inputBundlePath: string; outdir: string; workerWrapperSource: string }} input
|
|
24
|
-
* @returns {Promise<{ workerPath: string; innerBundlePath: string }>}
|
|
32
|
+
* @returns {Promise<{ workerPath: string; innerBundlePath: string; assetsIgnorePath: string }>}
|
|
25
33
|
*/
|
|
26
34
|
export async function emitWorker({ inputBundlePath, outdir, workerWrapperSource }) {
|
|
27
35
|
const outdirAbs = resolve(outdir);
|
|
@@ -34,5 +42,8 @@ export async function emitWorker({ inputBundlePath, outdir, workerWrapperSource
|
|
|
34
42
|
const workerPath = join(outdirAbs, "_worker.js");
|
|
35
43
|
await writeFile(workerPath, workerWrapperSource, "utf8");
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
const assetsIgnorePath = join(outdirAbs, ".assetsignore");
|
|
46
|
+
await writeFile(assetsIgnorePath, ASSETS_IGNORE_CONTENT, "utf8");
|
|
47
|
+
|
|
48
|
+
return { workerPath, innerBundlePath, assetsIgnorePath };
|
|
38
49
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -39,4 +39,3 @@ export declare function runWithCloudflareContext<T>(context: CloudflareContext,
|
|
|
39
39
|
* recommended so TypeScript catches typos like `env.ANTRHOPIC_KEY`.
|
|
40
40
|
*/
|
|
41
41
|
export declare function getCloudflareContext<Env = unknown>(): CloudflareContext<Env>;
|
|
42
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// `@takazudo/zfb-adapter-cloudflare` — Cloudflare
|
|
2
|
-
// zfb framework
|
|
1
|
+
// `@takazudo/zfb-adapter-cloudflare` — Cloudflare Workers Static Assets
|
|
2
|
+
// adapter for the zfb framework (also deployable to Cloudflare Pages
|
|
3
|
+
// advanced mode).
|
|
3
4
|
//
|
|
4
5
|
// This entry (`./`) is the **Workers-runtime** surface. It is safe to
|
|
5
6
|
// bundle into a Cloudflare Worker and does not depend on any Node-only
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,qEAAqE;AACrE,kBAAkB;AAClB,EAAE;AACF,sEAAsE;AACtE,uEAAuE;AACvE,iEAAiE;AACjE,EAAE;AACF,wCAAwC;AACxC,EAAE;AACF,6EAA6E;AAC7E,EAAE;AACF,oCAAoC;AACpC,+CAA+C;AAC/C,kFAAkF;AAClF,yDAAyD;AACzD,MAAM;AACN,EAAE;AACF,wEAAwE;AACxE,qBAAqB;AACrB,EAAE;AACF,yEAAyE;AACzE,EAAE;AACF,oDAAoD;AACpD,EAAE;AACF,uEAAuE;AACvE,yEAAyE;AACzE,yEAAyE;AACzE,4DAA4D;AAC5D,EAAE;AACF,sEAAsE;AACtE,yEAAyE;AACzE,mEAAmE;AACnE,yEAAyE;AACzE,qEAAqE;AACrE,uEAAuE;AACvE,uEAAuE;AACvE,6CAA6C;AAE7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AA8BrD,uDAAuD;AACvD,MAAM,WAAW,GAAG,wBAAwB,CAAC;AAM7C;;;;;;GAMG;AACH,SAAS,UAAU;IACjB,MAAM,CAAC,GAAG,UAAuC,CAAC;IAClD,IAAI,GAAG,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC;IACzB,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,GAAG,GAAG,IAAI,iBAAiB,EAAqB,CAAC;QACjD,CAAC,CAAC,WAAW,CAAC,GAAG,GAAG,CAAC;IACvB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAI,OAA0B,EAAE,EAAW;IACjF,OAAO,UAAU,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,CAAC,GAAG,UAAU,EAAE,CAAC,QAAQ,EAAE,CAAC;IAClC,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,IAAI,KAAK,CACb,6FAA6F;YAC3F,yFAAyF;YACzF,gGAAgG,CACnG,CAAC;IACJ,CAAC;IACD,OAAO,CAA2B,CAAC;AACrC,CAAC","sourcesContent":["// `@takazudo/zfb-adapter-cloudflare` — Cloudflare Workers Static Assets\n// adapter for the zfb framework (also deployable to Cloudflare Pages\n// advanced mode).\n//\n// This entry (`./`) is the **Workers-runtime** surface. It is safe to\n// bundle into a Cloudflare Worker and does not depend on any Node-only\n// built-ins beyond `node:async_hooks` (which workerd polyfills).\n//\n// Usage in a `prerender = false` route:\n//\n// import { getCloudflareContext } from \"@takazudo/zfb-adapter-cloudflare\";\n//\n// export const prerender = false;\n// export default async function ApiRoute() {\n// const { env, ctx } = getCloudflareContext<{ ANTHROPIC_API_KEY: string }>();\n// // env.ANTHROPIC_API_KEY, ctx.waitUntil(...), etc.\n// }\n//\n// For Node-only build helpers (e.g. `emitWorker`), import the `./build`\n// sub-entry instead:\n//\n// import { emitWorker } from \"@takazudo/zfb-adapter-cloudflare/build\";\n//\n// ## Why AsyncLocalStorage on a globalThis registry\n//\n// Cloudflare Workers can process multiple requests concurrently in the\n// same isolate. A naïve `globalThis.__env = env` write would race across\n// requests. AsyncLocalStorage gives us a per-request scope that survives\n// `await` points without interfering with sibling requests.\n//\n// We register the storage instance on `globalThis` under a stable key\n// (`__zfb_cf_adapter_als__`) so the wrapper at `_worker.js` and the user\n// pages bundled together can share the same instance even when the\n// adapter module ends up duplicated in the final bundle graph (e.g. when\n// the wrapper file is emitted side-by-side with the inner bundle and\n// each pulls in its own copy of this module). Module-instance identity\n// is the property AsyncLocalStorage relies on; the registry pattern is\n// what makes it survive bundler duplication.\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Cloudflare execution context — minimal projection of the workerd\n * `ExecutionContext` interface. We do not depend on `@cloudflare/workers-types`\n * at the type level here because that would force every consumer of this\n * package to install it; instead we keep a minimal structural shape and\n * let users widen it via the generic on [`getCloudflareContext`] when\n * they need richer bindings.\n */\nexport interface CloudflareExecutionContext {\n /** Extends the lifetime of the request beyond the response. */\n waitUntil(promise: Promise<unknown>): void;\n /** Falls through to the static origin on uncaught exceptions. */\n passThroughOnException(): void;\n}\n\n/**\n * Per-request Cloudflare context. `Env` defaults to `unknown` so the\n * caller can narrow it at the call site (recommended) or leave it open.\n */\nexport interface CloudflareContext<Env = unknown> {\n /** CF env bindings (secrets, KV, D1, …) wired up via wrangler.toml. */\n readonly env: Env;\n /** ExecutionContext for waitUntil / passThroughOnException. */\n readonly ctx: CloudflareExecutionContext;\n /** The original Request, useful when handlers want headers / URL. */\n readonly request: Request;\n}\n\n/** Stable globalThis key the registry pattern uses. */\nconst STORAGE_KEY = \"__zfb_cf_adapter_als__\";\n\ninterface RegistryGlobal {\n [STORAGE_KEY]?: AsyncLocalStorage<CloudflareContext>;\n}\n\n/**\n * Acquire (or lazily create) the singleton AsyncLocalStorage instance.\n *\n * Stored on `globalThis` under a stable key so the wrapper at\n * `_worker.js` and the user bundle share state even if the adapter\n * module ends up duplicated in the final module graph.\n */\nfunction getStorage(): AsyncLocalStorage<CloudflareContext> {\n const g = globalThis as unknown as RegistryGlobal;\n let als = g[STORAGE_KEY];\n if (!als) {\n als = new AsyncLocalStorage<CloudflareContext>();\n g[STORAGE_KEY] = als;\n }\n return als;\n}\n\n/**\n * Establish a Cloudflare context for the duration of `fn`. Used by the\n * `_worker.js` wrapper; not normally called by user code.\n */\nexport function runWithCloudflareContext<T>(context: CloudflareContext, fn: () => T): T {\n return getStorage().run(context, fn);\n}\n\n/**\n * Read the current Cloudflare context. Throws if called outside a\n * Cloudflare request scope (e.g. from a build-time SSG render). Catch\n * the error and gate on `prerender = false` if you need a route to work\n * in both modes.\n *\n * The `Env` generic narrows the bindings shape — passing it is\n * recommended so TypeScript catches typos like `env.ANTRHOPIC_KEY`.\n */\nexport function getCloudflareContext<Env = unknown>(): CloudflareContext<Env> {\n const c = getStorage().getStore();\n if (!c) {\n throw new Error(\n \"[zfb-adapter-cloudflare] getCloudflareContext() called outside a Cloudflare request scope. \" +\n \"This usually means the route was rendered at build time (SSG) instead of dispatched by \" +\n \"the Worker. Add `export const prerender = false;` to the page if it needs Cloudflare bindings.\",\n );\n }\n return c as CloudflareContext<Env>;\n}\n"]}
|
package/dist/worker-wrapper.mjs
CHANGED
|
@@ -10,33 +10,59 @@
|
|
|
10
10
|
/** @type {string} */
|
|
11
11
|
export const WORKER_WRAPPER_SOURCE = `// AUTO-GENERATED by @takazudo/zfb-adapter-cloudflare. Do not edit.
|
|
12
12
|
//
|
|
13
|
-
// Cloudflare
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// Cloudflare Workers Static Assets entry (\`main\` in wrangler.toml,
|
|
14
|
+
// alongside an \`[assets]\` block; also deployable to Cloudflare Pages
|
|
15
|
+
// advanced mode). Forwards (request, env, ctx) to the inner zfb worker
|
|
16
|
+
// bundle, exposing env/ctx to user code via AsyncLocalStorage under a
|
|
17
|
+
// stable globalThis key.
|
|
16
18
|
//
|
|
17
19
|
// The same key is read by @takazudo/zfb-adapter-cloudflare's
|
|
18
20
|
// getCloudflareContext() inside the user bundle, so the two ends share
|
|
19
21
|
// state even though they live in separate ESM module instances.
|
|
20
22
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// is deliberately "ASSETS first, inner on 404":
|
|
23
|
+
// Dispatch order is deliberately "ASSETS first, inner on 404" — this
|
|
24
|
+
// holds across both deploy targets, but *why* the ASSETS probe fires
|
|
25
|
+
// differs:
|
|
25
26
|
//
|
|
26
|
-
// -
|
|
27
|
+
// - Workers Static Assets with \`run_worker_first = false\` (the zfb
|
|
28
|
+
// default): the platform itself serves asset hits before the
|
|
29
|
+
// Worker ever runs, so for those requests this in-Worker probe is
|
|
30
|
+
// normally bypassed — it never had a chance to run. The probe below
|
|
31
|
+
// is NOT dead code: it is still exercised for Pages (no
|
|
32
|
+
// \`run_worker_first\` concept; every request hits the Worker), for
|
|
33
|
+
// \`run_worker_first = true\` deployments, and for any request the
|
|
34
|
+
// platform's asset router itself does not resolve (which still
|
|
35
|
+
// reaches this Worker as a "miss").
|
|
36
|
+
// - GET/HEAD requests probe env.ASSETS first. The asset server
|
|
27
37
|
// handles the trailing-slash canonicalisation for SSG output (e.g.
|
|
28
|
-
// "/docs/foo" →
|
|
38
|
+
// "/docs/foo" → redirect → "/docs/foo/" → dist/docs/foo/index.html
|
|
39
|
+
// — 307 on Workers Static Assets, 308 on Cloudflare Pages), so
|
|
29
40
|
// prerendered routes get the build-time head-injected HTML
|
|
30
41
|
// (<link rel="stylesheet">, <script type="module" src="/assets/
|
|
31
42
|
// islands-…">). If we let the inner Hono router handle them first,
|
|
32
43
|
// it would dynamic-SSR the page WITHOUT the prod head injection
|
|
33
44
|
// (which is a build-time post-process, not a runtime concern), and
|
|
34
45
|
// islands would never hydrate.
|
|
35
|
-
// - On 404 from ASSETS, fall through to the inner zfb worker
|
|
36
|
-
// where genuinely dynamic routes (\`prerender = false\`, e.g.
|
|
37
|
-
// \`pages/api/*.tsx\`) are served.
|
|
46
|
+
// - On 404 from ASSETS, fall through to the inner zfb worker — this
|
|
47
|
+
// is where genuinely dynamic routes (\`prerender = false\`, e.g.
|
|
48
|
+
// \`pages/api/*.tsx\`) are served. EXCEPTION: when the asset 404
|
|
49
|
+
// carries a *styled* 404-page body (\`not_found_handling = "404-page"\`
|
|
50
|
+
// on Workers Static Assets, or the \`404.html\` convention on Pages —
|
|
51
|
+
// detected as an HTML document with a real Content-Length) AND the
|
|
52
|
+
// inner ALSO 404s with only the framework default body (Hono's
|
|
53
|
+
// default not-found, \`text/plain\` "404 Not Found", or a bare 404
|
|
54
|
+
// with no content-type), the earlier styled asset 404 is preferred
|
|
55
|
+
// over the inner's plain 404 — otherwise the styled \`404.html\` would
|
|
56
|
+
// be discarded and users would see the inner plain-text 404 (issue
|
|
57
|
+
// #1322). Any inner 404 that declares another content-type is a
|
|
58
|
+
// deliberate response and WINS: a \`text/html\` 404 is a rendered
|
|
59
|
+
// custom not-found page (a \`prerender = false\` route SSRing its own
|
|
60
|
+
// 404), and an \`application/json\` 404 is an intentional API error —
|
|
61
|
+
// both are returned unchanged. Under \`not_found_handling = "none"\`
|
|
62
|
+
// the asset 404 body is empty/non-HTML, so the inner always wins —
|
|
63
|
+
// the historical behavior is preserved.
|
|
38
64
|
// - Non-GET/HEAD requests skip ASSETS and go straight to the inner —
|
|
39
|
-
//
|
|
65
|
+
// assets are read-only by definition, and we want POSTs
|
|
40
66
|
// (\`/api/ai-chat\`, etc.) to reach the SSR handler without a probe
|
|
41
67
|
// that would always 405 / 404.
|
|
42
68
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -64,24 +90,77 @@ function isAssetProbeMethod(method) {
|
|
|
64
90
|
return method === "GET" || method === "HEAD";
|
|
65
91
|
}
|
|
66
92
|
|
|
93
|
+
function assetHasStyled404Body(response) {
|
|
94
|
+
// True iff an ASSETS 404 carries the site's *styled* 404 page.
|
|
95
|
+
// Platform fact: \`not_found_handling = "404-page"\` (Workers Static
|
|
96
|
+
// Assets) and the Pages \`404.html\` convention both serve that page as
|
|
97
|
+
// an HTML document — auto-detected \`content-type: text/html\` and a real
|
|
98
|
+
// \`Content-Length\` (it is a static file). \`not_found_handling = "none"\`
|
|
99
|
+
// and a bare Pages advanced-mode 404 send Cloudflare's default 404 with
|
|
100
|
+
// an empty / non-HTML body, so this returns false and the inner wins.
|
|
101
|
+
// Header-only (never reads the body) so it holds for HEAD too and leaves
|
|
102
|
+
// the one-shot asset stream intact for a verbatim return.
|
|
103
|
+
const contentType = (response.headers.get("content-type") || "").toLowerCase();
|
|
104
|
+
const contentLength = Number(response.headers.get("content-length") || "0");
|
|
105
|
+
return contentType.includes("text/html") && contentLength > 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function innerIsFrameworkDefault404(response) {
|
|
109
|
+
// True iff an inner 404 is the framework's *generic* not-found, which
|
|
110
|
+
// yields to the styled asset 404 page. The inner zfb router never
|
|
111
|
+
// overrides Hono's default handler, so a route miss is always Hono's
|
|
112
|
+
// default not-found — \`text/plain\` "404 Not Found" (or a bare 404 with no
|
|
113
|
+
// content-type). Any inner 404 that declares another content-type is a
|
|
114
|
+
// *deliberate* response and must WIN over the static styled asset page: a
|
|
115
|
+
// \`text/html\` 404 is a rendered custom not-found page (e.g. a
|
|
116
|
+
// \`prerender = false\` [slug] route SSRing its own 404), and an
|
|
117
|
+
// \`application/json\` 404 is an intentional machine-readable API error.
|
|
118
|
+
// Trade-off: a bare \`text/plain\` API 404 is indistinguishable from the
|
|
119
|
+
// framework default and will yield to the styled page — API errors should
|
|
120
|
+
// use \`application/json\` to be preserved.
|
|
121
|
+
const contentType = (response.headers.get("content-type") || "").toLowerCase();
|
|
122
|
+
if (contentType === "") return true;
|
|
123
|
+
return contentType.includes("text/plain");
|
|
124
|
+
}
|
|
125
|
+
|
|
67
126
|
export default {
|
|
68
127
|
async fetch(request, env, ctx) {
|
|
69
128
|
if (isAssetProbeMethod(request.method) && canDelegateToAssets(env)) {
|
|
70
129
|
// Trade-off: every GET/HEAD request that matches a prerendered route pays
|
|
71
130
|
// the cost of one env.ASSETS.fetch() round-trip before the inner worker
|
|
72
|
-
// sees it
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
131
|
+
// sees it (when this probe actually runs — see the header comment on
|
|
132
|
+
// run_worker_first). The upside is that the asset server handles
|
|
133
|
+
// trailing-slash canonicalisation (e.g. /docs/foo → redirect → /docs/foo/)
|
|
134
|
+
// and serves the build-time head-injected HTML with the hashed
|
|
135
|
+
// <link>/<script> tags. If we skipped this probe, prerendered routes
|
|
136
|
+
// would be dynamic-SSR'd by the inner Hono router without the prod head
|
|
137
|
+
// injection, and islands would never hydrate. For purely dynamic apps
|
|
138
|
+
// (prerender=false everywhere) the extra round-trip is pure overhead;
|
|
139
|
+
// splitting the wrapper into two variants is the accepted future escape
|
|
140
|
+
// hatch for that case.
|
|
80
141
|
const assetResponse = await env.ASSETS.fetch(request);
|
|
81
142
|
if (assetResponse.status !== 404) {
|
|
82
143
|
return assetResponse;
|
|
83
144
|
}
|
|
84
|
-
// Fall through to the inner worker for genuinely dynamic
|
|
145
|
+
// Asset 404. Fall through to the inner worker for genuinely dynamic
|
|
146
|
+
// routes, but first hold the asset response UNREAD if it carries a
|
|
147
|
+
// styled 404 page: if the inner also 404s with only the framework
|
|
148
|
+
// default body (text/plain or none), we return this styled page
|
|
149
|
+
// instead of the inner's plain 404 (issue #1322). An inner 404 that
|
|
150
|
+
// renders its own page (text/html) or a structured API error
|
|
151
|
+
// (application/json) wins. Returned verbatim — the one-shot body is
|
|
152
|
+
// untouched.
|
|
153
|
+
const styledAsset404 = assetHasStyled404Body(assetResponse) ? assetResponse : null;
|
|
154
|
+
const store = { env, ctx, request };
|
|
155
|
+
const innerResponse = await getStorage().run(store, () => inner.fetch(request));
|
|
156
|
+
if (
|
|
157
|
+
styledAsset404 !== null &&
|
|
158
|
+
innerResponse.status === 404 &&
|
|
159
|
+
innerIsFrameworkDefault404(innerResponse)
|
|
160
|
+
) {
|
|
161
|
+
return styledAsset404;
|
|
162
|
+
}
|
|
163
|
+
return innerResponse;
|
|
85
164
|
}
|
|
86
165
|
const store = { env, ctx, request };
|
|
87
166
|
return getStorage().run(store, () => inner.fetch(request));
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takazudo/zfb-adapter-cloudflare",
|
|
3
|
-
"version": "0.1.0-next.
|
|
3
|
+
"version": "0.1.0-next.75",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "Rust-built static-site engine for Astro and Next.js users — millisecond rebuilds, single binary. Cloudflare Pages
|
|
6
|
+
"description": "Rust-built static-site engine for Astro and Next.js users — millisecond rebuilds, single binary. Cloudflare adapter: Workers Static Assets (Pages-compatible).",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"zfb",
|
|
9
9
|
"zfb-adapter",
|
|
10
10
|
"cloudflare",
|
|
11
11
|
"cloudflare-pages",
|
|
12
12
|
"cloudflare-workers",
|
|
13
|
+
"workers-static-assets",
|
|
13
14
|
"ssg",
|
|
14
15
|
"static-site-generator",
|
|
15
16
|
"adapter"
|
package/src/emit-worker.mjs
CHANGED
|
@@ -12,16 +12,24 @@
|
|
|
12
12
|
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
13
13
|
import { join, resolve } from "node:path";
|
|
14
14
|
|
|
15
|
+
// `.assetsignore` tells the Workers Static Assets uploader (and Pages'
|
|
16
|
+
// asset server) to skip these two files, so they are only ever reachable
|
|
17
|
+
// through the Worker's module graph, not served as public static assets.
|
|
18
|
+
const ASSETS_IGNORE_CONTENT = "_worker.js\n_zfb_inner.mjs\n";
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
|
-
* Emit a Cloudflare Pages `_worker.js`
|
|
21
|
+
* Emit a Cloudflare Workers Static Assets (Pages-compatible) `_worker.js`
|
|
22
|
+
* that wraps the zfb input bundle.
|
|
17
23
|
*
|
|
18
|
-
* Output shape (
|
|
24
|
+
* Output shape (three files in `outdir`):
|
|
19
25
|
*
|
|
20
|
-
* _worker.js — entry
|
|
26
|
+
* _worker.js — Worker entry point (`main` in wrangler.toml, or the
|
|
27
|
+
* Pages advanced-mode convention)
|
|
21
28
|
* _zfb_inner.mjs — the input bundle, copied verbatim
|
|
29
|
+
* .assetsignore — excludes the two files above from the asset upload
|
|
22
30
|
*
|
|
23
31
|
* @param {{ inputBundlePath: string; outdir: string; workerWrapperSource: string }} input
|
|
24
|
-
* @returns {Promise<{ workerPath: string; innerBundlePath: string }>}
|
|
32
|
+
* @returns {Promise<{ workerPath: string; innerBundlePath: string; assetsIgnorePath: string }>}
|
|
25
33
|
*/
|
|
26
34
|
export async function emitWorker({ inputBundlePath, outdir, workerWrapperSource }) {
|
|
27
35
|
const outdirAbs = resolve(outdir);
|
|
@@ -34,5 +42,8 @@ export async function emitWorker({ inputBundlePath, outdir, workerWrapperSource
|
|
|
34
42
|
const workerPath = join(outdirAbs, "_worker.js");
|
|
35
43
|
await writeFile(workerPath, workerWrapperSource, "utf8");
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
const assetsIgnorePath = join(outdirAbs, ".assetsignore");
|
|
46
|
+
await writeFile(assetsIgnorePath, ASSETS_IGNORE_CONTENT, "utf8");
|
|
47
|
+
|
|
48
|
+
return { workerPath, innerBundlePath, assetsIgnorePath };
|
|
38
49
|
}
|
package/src/worker-wrapper.mjs
CHANGED
|
@@ -10,33 +10,59 @@
|
|
|
10
10
|
/** @type {string} */
|
|
11
11
|
export const WORKER_WRAPPER_SOURCE = `// AUTO-GENERATED by @takazudo/zfb-adapter-cloudflare. Do not edit.
|
|
12
12
|
//
|
|
13
|
-
// Cloudflare
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// Cloudflare Workers Static Assets entry (\`main\` in wrangler.toml,
|
|
14
|
+
// alongside an \`[assets]\` block; also deployable to Cloudflare Pages
|
|
15
|
+
// advanced mode). Forwards (request, env, ctx) to the inner zfb worker
|
|
16
|
+
// bundle, exposing env/ctx to user code via AsyncLocalStorage under a
|
|
17
|
+
// stable globalThis key.
|
|
16
18
|
//
|
|
17
19
|
// The same key is read by @takazudo/zfb-adapter-cloudflare's
|
|
18
20
|
// getCloudflareContext() inside the user bundle, so the two ends share
|
|
19
21
|
// state even though they live in separate ESM module instances.
|
|
20
22
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// is deliberately "ASSETS first, inner on 404":
|
|
23
|
+
// Dispatch order is deliberately "ASSETS first, inner on 404" — this
|
|
24
|
+
// holds across both deploy targets, but *why* the ASSETS probe fires
|
|
25
|
+
// differs:
|
|
25
26
|
//
|
|
26
|
-
// -
|
|
27
|
+
// - Workers Static Assets with \`run_worker_first = false\` (the zfb
|
|
28
|
+
// default): the platform itself serves asset hits before the
|
|
29
|
+
// Worker ever runs, so for those requests this in-Worker probe is
|
|
30
|
+
// normally bypassed — it never had a chance to run. The probe below
|
|
31
|
+
// is NOT dead code: it is still exercised for Pages (no
|
|
32
|
+
// \`run_worker_first\` concept; every request hits the Worker), for
|
|
33
|
+
// \`run_worker_first = true\` deployments, and for any request the
|
|
34
|
+
// platform's asset router itself does not resolve (which still
|
|
35
|
+
// reaches this Worker as a "miss").
|
|
36
|
+
// - GET/HEAD requests probe env.ASSETS first. The asset server
|
|
27
37
|
// handles the trailing-slash canonicalisation for SSG output (e.g.
|
|
28
|
-
// "/docs/foo" →
|
|
38
|
+
// "/docs/foo" → redirect → "/docs/foo/" → dist/docs/foo/index.html
|
|
39
|
+
// — 307 on Workers Static Assets, 308 on Cloudflare Pages), so
|
|
29
40
|
// prerendered routes get the build-time head-injected HTML
|
|
30
41
|
// (<link rel="stylesheet">, <script type="module" src="/assets/
|
|
31
42
|
// islands-…">). If we let the inner Hono router handle them first,
|
|
32
43
|
// it would dynamic-SSR the page WITHOUT the prod head injection
|
|
33
44
|
// (which is a build-time post-process, not a runtime concern), and
|
|
34
45
|
// islands would never hydrate.
|
|
35
|
-
// - On 404 from ASSETS, fall through to the inner zfb worker
|
|
36
|
-
// where genuinely dynamic routes (\`prerender = false\`, e.g.
|
|
37
|
-
// \`pages/api/*.tsx\`) are served.
|
|
46
|
+
// - On 404 from ASSETS, fall through to the inner zfb worker — this
|
|
47
|
+
// is where genuinely dynamic routes (\`prerender = false\`, e.g.
|
|
48
|
+
// \`pages/api/*.tsx\`) are served. EXCEPTION: when the asset 404
|
|
49
|
+
// carries a *styled* 404-page body (\`not_found_handling = "404-page"\`
|
|
50
|
+
// on Workers Static Assets, or the \`404.html\` convention on Pages —
|
|
51
|
+
// detected as an HTML document with a real Content-Length) AND the
|
|
52
|
+
// inner ALSO 404s with only the framework default body (Hono's
|
|
53
|
+
// default not-found, \`text/plain\` "404 Not Found", or a bare 404
|
|
54
|
+
// with no content-type), the earlier styled asset 404 is preferred
|
|
55
|
+
// over the inner's plain 404 — otherwise the styled \`404.html\` would
|
|
56
|
+
// be discarded and users would see the inner plain-text 404 (issue
|
|
57
|
+
// #1322). Any inner 404 that declares another content-type is a
|
|
58
|
+
// deliberate response and WINS: a \`text/html\` 404 is a rendered
|
|
59
|
+
// custom not-found page (a \`prerender = false\` route SSRing its own
|
|
60
|
+
// 404), and an \`application/json\` 404 is an intentional API error —
|
|
61
|
+
// both are returned unchanged. Under \`not_found_handling = "none"\`
|
|
62
|
+
// the asset 404 body is empty/non-HTML, so the inner always wins —
|
|
63
|
+
// the historical behavior is preserved.
|
|
38
64
|
// - Non-GET/HEAD requests skip ASSETS and go straight to the inner —
|
|
39
|
-
//
|
|
65
|
+
// assets are read-only by definition, and we want POSTs
|
|
40
66
|
// (\`/api/ai-chat\`, etc.) to reach the SSR handler without a probe
|
|
41
67
|
// that would always 405 / 404.
|
|
42
68
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -64,24 +90,77 @@ function isAssetProbeMethod(method) {
|
|
|
64
90
|
return method === "GET" || method === "HEAD";
|
|
65
91
|
}
|
|
66
92
|
|
|
93
|
+
function assetHasStyled404Body(response) {
|
|
94
|
+
// True iff an ASSETS 404 carries the site's *styled* 404 page.
|
|
95
|
+
// Platform fact: \`not_found_handling = "404-page"\` (Workers Static
|
|
96
|
+
// Assets) and the Pages \`404.html\` convention both serve that page as
|
|
97
|
+
// an HTML document — auto-detected \`content-type: text/html\` and a real
|
|
98
|
+
// \`Content-Length\` (it is a static file). \`not_found_handling = "none"\`
|
|
99
|
+
// and a bare Pages advanced-mode 404 send Cloudflare's default 404 with
|
|
100
|
+
// an empty / non-HTML body, so this returns false and the inner wins.
|
|
101
|
+
// Header-only (never reads the body) so it holds for HEAD too and leaves
|
|
102
|
+
// the one-shot asset stream intact for a verbatim return.
|
|
103
|
+
const contentType = (response.headers.get("content-type") || "").toLowerCase();
|
|
104
|
+
const contentLength = Number(response.headers.get("content-length") || "0");
|
|
105
|
+
return contentType.includes("text/html") && contentLength > 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function innerIsFrameworkDefault404(response) {
|
|
109
|
+
// True iff an inner 404 is the framework's *generic* not-found, which
|
|
110
|
+
// yields to the styled asset 404 page. The inner zfb router never
|
|
111
|
+
// overrides Hono's default handler, so a route miss is always Hono's
|
|
112
|
+
// default not-found — \`text/plain\` "404 Not Found" (or a bare 404 with no
|
|
113
|
+
// content-type). Any inner 404 that declares another content-type is a
|
|
114
|
+
// *deliberate* response and must WIN over the static styled asset page: a
|
|
115
|
+
// \`text/html\` 404 is a rendered custom not-found page (e.g. a
|
|
116
|
+
// \`prerender = false\` [slug] route SSRing its own 404), and an
|
|
117
|
+
// \`application/json\` 404 is an intentional machine-readable API error.
|
|
118
|
+
// Trade-off: a bare \`text/plain\` API 404 is indistinguishable from the
|
|
119
|
+
// framework default and will yield to the styled page — API errors should
|
|
120
|
+
// use \`application/json\` to be preserved.
|
|
121
|
+
const contentType = (response.headers.get("content-type") || "").toLowerCase();
|
|
122
|
+
if (contentType === "") return true;
|
|
123
|
+
return contentType.includes("text/plain");
|
|
124
|
+
}
|
|
125
|
+
|
|
67
126
|
export default {
|
|
68
127
|
async fetch(request, env, ctx) {
|
|
69
128
|
if (isAssetProbeMethod(request.method) && canDelegateToAssets(env)) {
|
|
70
129
|
// Trade-off: every GET/HEAD request that matches a prerendered route pays
|
|
71
130
|
// the cost of one env.ASSETS.fetch() round-trip before the inner worker
|
|
72
|
-
// sees it
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
131
|
+
// sees it (when this probe actually runs — see the header comment on
|
|
132
|
+
// run_worker_first). The upside is that the asset server handles
|
|
133
|
+
// trailing-slash canonicalisation (e.g. /docs/foo → redirect → /docs/foo/)
|
|
134
|
+
// and serves the build-time head-injected HTML with the hashed
|
|
135
|
+
// <link>/<script> tags. If we skipped this probe, prerendered routes
|
|
136
|
+
// would be dynamic-SSR'd by the inner Hono router without the prod head
|
|
137
|
+
// injection, and islands would never hydrate. For purely dynamic apps
|
|
138
|
+
// (prerender=false everywhere) the extra round-trip is pure overhead;
|
|
139
|
+
// splitting the wrapper into two variants is the accepted future escape
|
|
140
|
+
// hatch for that case.
|
|
80
141
|
const assetResponse = await env.ASSETS.fetch(request);
|
|
81
142
|
if (assetResponse.status !== 404) {
|
|
82
143
|
return assetResponse;
|
|
83
144
|
}
|
|
84
|
-
// Fall through to the inner worker for genuinely dynamic
|
|
145
|
+
// Asset 404. Fall through to the inner worker for genuinely dynamic
|
|
146
|
+
// routes, but first hold the asset response UNREAD if it carries a
|
|
147
|
+
// styled 404 page: if the inner also 404s with only the framework
|
|
148
|
+
// default body (text/plain or none), we return this styled page
|
|
149
|
+
// instead of the inner's plain 404 (issue #1322). An inner 404 that
|
|
150
|
+
// renders its own page (text/html) or a structured API error
|
|
151
|
+
// (application/json) wins. Returned verbatim — the one-shot body is
|
|
152
|
+
// untouched.
|
|
153
|
+
const styledAsset404 = assetHasStyled404Body(assetResponse) ? assetResponse : null;
|
|
154
|
+
const store = { env, ctx, request };
|
|
155
|
+
const innerResponse = await getStorage().run(store, () => inner.fetch(request));
|
|
156
|
+
if (
|
|
157
|
+
styledAsset404 !== null &&
|
|
158
|
+
innerResponse.status === 404 &&
|
|
159
|
+
innerIsFrameworkDefault404(innerResponse)
|
|
160
|
+
) {
|
|
161
|
+
return styledAsset404;
|
|
162
|
+
}
|
|
163
|
+
return innerResponse;
|
|
85
164
|
}
|
|
86
165
|
const store = { env, ctx, request };
|
|
87
166
|
return getStorage().run(store, () => inner.fetch(request));
|
package/dist/build.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAA2B,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAMlF"}
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwCA;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACzC,+DAA+D;IAC/D,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3C,iEAAiE;IACjE,sBAAsB,IAAI,IAAI,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB,CAAC,GAAG,GAAG,OAAO;IAC9C,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,EAAE,0BAA0B,CAAC;IACzC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AA0BD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEtF;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,GAAG,OAAO,KAAK,iBAAiB,CAAC,GAAG,CAAC,CAU5E"}
|