@takazudo/zfb-adapter-cloudflare 0.1.0-next.2
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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/bin/cli.mjs +152 -0
- package/dist/build.d.ts +53 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +49 -0
- package/dist/build.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/worker-wrapper.mjs +81 -0
- package/package.json +69 -0
- package/src/worker-wrapper.mjs +81 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Takeshi Takatsudo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @takazudo/zfb-adapter-cloudflare
|
|
2
|
+
|
|
3
|
+
> Rust-built static-site engine for Astro and Next.js users — millisecond rebuilds, single binary.
|
|
4
|
+
|
|
5
|
+
The Cloudflare Pages adapter for [zfb][zfb-site]. Wraps the
|
|
6
|
+
`@takazudo/zfb-runtime` page router into a Cloudflare Pages advanced-mode
|
|
7
|
+
`_worker.js` entry, threading `(env, ctx)` through to user code via
|
|
8
|
+
`AsyncLocalStorage`.
|
|
9
|
+
|
|
10
|
+
This package is the Cloudflare half of the SSR adapter contract. Other
|
|
11
|
+
targets (Node, Netlify, …) will land as sibling `@takazudo/zfb-adapter-*`
|
|
12
|
+
packages with the same shape.
|
|
13
|
+
|
|
14
|
+
Full documentation: <https://takazudomodular.com/pj/zudo-front-builder/>.
|
|
15
|
+
Source: <https://github.com/Takazudo/zudo-front-builder>.
|
|
16
|
+
|
|
17
|
+
[zfb-site]: https://takazudomodular.com/pj/zudo-front-builder/
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install --save-dev @takazudo/zfb-adapter-cloudflare
|
|
23
|
+
# or: pnpm add -D @takazudo/zfb-adapter-cloudflare
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
In `zfb.config.json`:
|
|
29
|
+
|
|
30
|
+
```jsonc
|
|
31
|
+
{
|
|
32
|
+
"framework": "preact",
|
|
33
|
+
"adapter": "@takazudo/zfb-adapter-cloudflare"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then in any page that needs Cloudflare bindings — secrets, KV, or a
|
|
38
|
+
D1 database:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// pages/api/products.tsx
|
|
42
|
+
import { getCloudflareContext } from "@takazudo/zfb-adapter-cloudflare";
|
|
43
|
+
|
|
44
|
+
export const prerender = false; // opt out of build-time SSG
|
|
45
|
+
|
|
46
|
+
interface Env {
|
|
47
|
+
ANTHROPIC_API_KEY: string;
|
|
48
|
+
DB: D1Database; // a `wrangler.toml` D1 binding named "DB"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default async function Products() {
|
|
52
|
+
const { env, ctx } = getCloudflareContext<Env>();
|
|
53
|
+
ctx.waitUntil(reportToAnalytics());
|
|
54
|
+
// A D1 binding is just-another-object on `env` — query it directly.
|
|
55
|
+
const { results } = await env.DB.prepare("SELECT * FROM products").all();
|
|
56
|
+
return new Response(JSON.stringify(results), {
|
|
57
|
+
headers: { "content-type": "application/json" },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See the [SSR and Cloudflare Bindings guide][ssr-guide] for the full D1
|
|
63
|
+
lifecycle (`wrangler d1 create`, migrations, preview-vs-prod).
|
|
64
|
+
|
|
65
|
+
[ssr-guide]: https://takazudomodular.com/pj/zudo-front-builder/guides/ssr-and-cloudflare-bindings/
|
|
66
|
+
|
|
67
|
+
`zfb build` will:
|
|
68
|
+
|
|
69
|
+
1. Render every SSG page (`prerender !== false`) into static HTML under
|
|
70
|
+
`dist/`.
|
|
71
|
+
2. Hand the SSR bundle to this adapter, which writes
|
|
72
|
+
`dist/_worker.js` (the wrapper) and `dist/_zfb_inner.mjs` (the
|
|
73
|
+
bundle) ready to be deployed via Cloudflare Pages advanced mode.
|
|
74
|
+
|
|
75
|
+
## CLI
|
|
76
|
+
|
|
77
|
+
The package ships a `zfb-adapter-cloudflare` bin invoked by
|
|
78
|
+
`zfb-build`:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
zfb-adapter-cloudflare bundle <input.mjs> --outdir dist/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`<input.mjs>` is the ESM bundle `zfb-build`'s bundler emits. The
|
|
85
|
+
output is a single Workers-shaped entry plus a sidecar copy of the
|
|
86
|
+
inner bundle.
|
|
87
|
+
|
|
88
|
+
## Why two files
|
|
89
|
+
|
|
90
|
+
The wrapper imports the inner bundle by relative path
|
|
91
|
+
(`./_zfb_inner.mjs`) instead of inlining it, so the adapter package
|
|
92
|
+
itself does not need to ship an esbuild binary. Workerd's Module
|
|
93
|
+
loader resolves relative ESM imports inside an advanced-mode
|
|
94
|
+
`_worker.js` directory layout.
|
|
95
|
+
|
|
96
|
+
## Why AsyncLocalStorage on a globalThis registry
|
|
97
|
+
|
|
98
|
+
Cloudflare Workers can dispatch multiple requests concurrently in the
|
|
99
|
+
same isolate, so reading `env` from `globalThis` would race across
|
|
100
|
+
requests. AsyncLocalStorage gives each request its own scope.
|
|
101
|
+
|
|
102
|
+
We register the storage instance on `globalThis` under a stable key so
|
|
103
|
+
the wrapper at `_worker.js` and the user pages bundled together share
|
|
104
|
+
the same instance even when this module ends up duplicated in the
|
|
105
|
+
final module graph (the wrapper and the user bundle are separate ESM
|
|
106
|
+
graphs by design).
|
|
107
|
+
|
|
108
|
+
## Acceptance test
|
|
109
|
+
|
|
110
|
+
`src/__tests__/cli.test.ts` imports the produced `_worker.js`
|
|
111
|
+
directly into vitest, builds a synthetic `Request + env + ctx`, and
|
|
112
|
+
asserts that user code reading `env.ANTHROPIC_API_KEY` sees the value
|
|
113
|
+
the wrapper passed in. This stands in for a full `wrangler dev` smoke
|
|
114
|
+
test, which would require port binding and is out of scope for the
|
|
115
|
+
worktree-side test loop.
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// `zfb-adapter-cloudflare` CLI.
|
|
4
|
+
//
|
|
5
|
+
// Subcommands:
|
|
6
|
+
//
|
|
7
|
+
// bundle <input> --outdir <dir>
|
|
8
|
+
//
|
|
9
|
+
// Wrap the input ESM bundle into a Cloudflare Pages `_worker.js`
|
|
10
|
+
// placed under <dir>. The input bundle is the file `zfb_build`'s
|
|
11
|
+
// bundler emits; <dir> is typically the project's `dist/`.
|
|
12
|
+
//
|
|
13
|
+
// The CLI is intentionally tiny and dependency-free. It imports the
|
|
14
|
+
// wrapper string from the canonical `src/worker-wrapper.mjs` (plain JS,
|
|
15
|
+
// no TypeScript loader required) so there is a single source of truth.
|
|
16
|
+
// invariant: no runtime npm deps — see SECURITY-DEPS.md
|
|
17
|
+
|
|
18
|
+
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
19
|
+
import { realpathSync } from "node:fs";
|
|
20
|
+
import { join, resolve } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Wrapper source — imported from the single canonical source.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
import { WORKER_WRAPPER_SOURCE } from "../src/worker-wrapper.mjs";
|
|
28
|
+
export { WORKER_WRAPPER_SOURCE };
|
|
29
|
+
|
|
30
|
+
export async function emitWorker({ inputBundlePath, outdir }) {
|
|
31
|
+
const outdirAbs = resolve(outdir);
|
|
32
|
+
const inputAbs = resolve(inputBundlePath);
|
|
33
|
+
|
|
34
|
+
await mkdir(outdirAbs, { recursive: true });
|
|
35
|
+
const innerBundlePath = join(outdirAbs, "_zfb_inner.mjs");
|
|
36
|
+
await copyFile(inputAbs, innerBundlePath);
|
|
37
|
+
|
|
38
|
+
const workerPath = join(outdirAbs, "_worker.js");
|
|
39
|
+
await writeFile(workerPath, WORKER_WRAPPER_SOURCE, "utf8");
|
|
40
|
+
|
|
41
|
+
return { workerPath, innerBundlePath };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// CLI
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
function fail(message) {
|
|
49
|
+
process.stderr.write(`zfb-adapter-cloudflare: ${message}\n`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function printUsage() {
|
|
54
|
+
process.stdout.write(`Usage:
|
|
55
|
+
zfb-adapter-cloudflare bundle <input> --outdir <dir>
|
|
56
|
+
|
|
57
|
+
Wrap an ESM bundle (the output of zfb-build's bundler) into a
|
|
58
|
+
Cloudflare Pages \`_worker.js\` placed under <dir>.
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--outdir <dir> Output directory. Required.
|
|
62
|
+
-h, --help Show this help.
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseArgs(argv) {
|
|
67
|
+
const args = argv.slice(2);
|
|
68
|
+
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
|
|
69
|
+
return { command: "help" };
|
|
70
|
+
}
|
|
71
|
+
const command = args[0];
|
|
72
|
+
if (command !== "bundle") {
|
|
73
|
+
fail(`unknown subcommand: ${command}\nRun \`zfb-adapter-cloudflare --help\` for usage.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let input = null;
|
|
77
|
+
let outdir = null;
|
|
78
|
+
let i = 1;
|
|
79
|
+
while (i < args.length) {
|
|
80
|
+
const arg = args[i];
|
|
81
|
+
if (arg === "--outdir") {
|
|
82
|
+
const next = args[i + 1];
|
|
83
|
+
if (!next) fail("--outdir requires a directory argument");
|
|
84
|
+
outdir = next;
|
|
85
|
+
i += 2;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg.startsWith("--outdir=")) {
|
|
89
|
+
outdir = arg.slice("--outdir=".length);
|
|
90
|
+
i += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (arg.startsWith("--")) {
|
|
94
|
+
fail(`unknown option: ${arg}`);
|
|
95
|
+
}
|
|
96
|
+
if (input === null) {
|
|
97
|
+
input = arg;
|
|
98
|
+
i += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
fail(`unexpected positional argument: ${arg}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!input) fail("missing required positional argument: <input>");
|
|
105
|
+
if (!outdir) fail("missing required option: --outdir <dir>");
|
|
106
|
+
|
|
107
|
+
return { command: "bundle", input, outdir };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function main() {
|
|
111
|
+
// Skip when imported (e.g. by the vitest that snapshots
|
|
112
|
+
// WORKER_WRAPPER_SOURCE). When this file is run directly the process
|
|
113
|
+
// entry resolves (after symlinks) to this file's realpath.
|
|
114
|
+
//
|
|
115
|
+
// pnpm's `.bin` shim invokes `node node_modules/.bin/../<pkg>/bin/cli.mjs`,
|
|
116
|
+
// so `process.argv[1]` lexically resolves to the symlinked
|
|
117
|
+
// `node_modules/<pkg>/bin/cli.mjs` path — but Node's ESM loader follows
|
|
118
|
+
// symlinks by default, so `import.meta.url` points at the realpath under
|
|
119
|
+
// `.pnpm/`. A lexical compare therefore mismatches under pnpm exec and
|
|
120
|
+
// the CLI silently no-ops. Compare realpaths so direct `node bin/cli.mjs`,
|
|
121
|
+
// pnpm exec, npm bin shims, and Yarn PnP all agree.
|
|
122
|
+
const entry = process.argv[1];
|
|
123
|
+
if (!entry) return;
|
|
124
|
+
let entryReal;
|
|
125
|
+
let selfReal;
|
|
126
|
+
try {
|
|
127
|
+
entryReal = realpathSync(entry);
|
|
128
|
+
selfReal = realpathSync(fileURLToPath(import.meta.url));
|
|
129
|
+
} catch {
|
|
130
|
+
return; // can't resolve either side — treat as imported, not run
|
|
131
|
+
}
|
|
132
|
+
if (entryReal !== selfReal) return;
|
|
133
|
+
|
|
134
|
+
const parsed = parseArgs(process.argv);
|
|
135
|
+
if (parsed.command === "help") {
|
|
136
|
+
printUsage();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const inputAbs = resolve(process.cwd(), parsed.input);
|
|
141
|
+
const outdirAbs = resolve(process.cwd(), parsed.outdir);
|
|
142
|
+
const out = await emitWorker({
|
|
143
|
+
inputBundlePath: inputAbs,
|
|
144
|
+
outdir: outdirAbs,
|
|
145
|
+
});
|
|
146
|
+
process.stdout.write(`wrote ${out.workerPath}\nwrote ${out.innerBundlePath}\n`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
main().catch((err) => {
|
|
150
|
+
const msg = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
151
|
+
fail(msg);
|
|
152
|
+
});
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wrapper source written to `_worker.js`. Imported from the single
|
|
3
|
+
* canonical `.mjs` file so `src/build.ts` and `bin/cli.mjs` always stay
|
|
4
|
+
* in sync without any duplication.
|
|
5
|
+
*/
|
|
6
|
+
export declare const WORKER_WRAPPER_SOURCE: string;
|
|
7
|
+
/**
|
|
8
|
+
* Inputs to [`emitWorker`].
|
|
9
|
+
*/
|
|
10
|
+
export interface EmitWorkerInput {
|
|
11
|
+
/**
|
|
12
|
+
* Absolute path to the input ESM bundle produced by `zfb-build`. The
|
|
13
|
+
* bundle must export a Workers-shaped `default { fetch: (request) =>
|
|
14
|
+
* Promise<Response> }` (this is the contract `zfb_build::bundler`
|
|
15
|
+
* pins). The file is copied verbatim next to the emitted wrapper so
|
|
16
|
+
* relative imports inside it keep resolving.
|
|
17
|
+
*/
|
|
18
|
+
readonly inputBundlePath: string;
|
|
19
|
+
/**
|
|
20
|
+
* Absolute path to the output directory. The emitter creates it if
|
|
21
|
+
* missing and writes `_worker.js` plus `_zfb_inner.mjs` (the copied
|
|
22
|
+
* input bundle) into it.
|
|
23
|
+
*/
|
|
24
|
+
readonly outdir: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Output paths the emitter produced. Returned for callers that want to
|
|
28
|
+
* log them (the Rust orchestrator surfaces them in build output).
|
|
29
|
+
*/
|
|
30
|
+
export interface EmitWorkerOutput {
|
|
31
|
+
readonly workerPath: string;
|
|
32
|
+
readonly innerBundlePath: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Emit a Cloudflare Pages `_worker.js` that wraps the zfb input bundle.
|
|
36
|
+
*
|
|
37
|
+
* Output shape (two files in `outdir`):
|
|
38
|
+
*
|
|
39
|
+
* _worker.js — entry imported by Cloudflare Pages advanced mode
|
|
40
|
+
* _zfb_inner.mjs — the input bundle, copied verbatim
|
|
41
|
+
*
|
|
42
|
+
* The wrapper imports the inner bundle via the relative path
|
|
43
|
+
* `./_zfb_inner.mjs`. Workerd's Module loader resolves relative ESM
|
|
44
|
+
* imports inside an advanced-mode `_worker.js` directory, so this layout
|
|
45
|
+
* works without re-bundling.
|
|
46
|
+
*
|
|
47
|
+
* Why two files instead of one: re-bundling here would require a second
|
|
48
|
+
* esbuild pass and would force the adapter to ship its own esbuild
|
|
49
|
+
* binary slot. The two-file layout keeps the adapter dependency-free at
|
|
50
|
+
* runtime — it is just `node:fs` glue.
|
|
51
|
+
*/
|
|
52
|
+
export declare function emitWorker(input: EmitWorkerInput): Promise<EmitWorkerOutput>;
|
|
53
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAcA;;;;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,CAelF"}
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// `@takazudo/zfb-adapter-cloudflare/build` — Node-only build helpers.
|
|
2
|
+
//
|
|
3
|
+
// This sub-entry is intentionally **not** imported by the Workers-runtime
|
|
4
|
+
// entry (`./`). Code in this module may freely use Node built-ins
|
|
5
|
+
// (`node:fs`, `node:path`, …) because it only ever runs in a Node 22+
|
|
6
|
+
// build environment, never inside a Cloudflare Worker isolate.
|
|
7
|
+
//
|
|
8
|
+
// The `./` entry (`src/index.ts`) exports only Workers-runtime-safe code
|
|
9
|
+
// (AsyncLocalStorage helpers) that can be bundled into the worker.
|
|
10
|
+
// @ts-expect-error worker-wrapper.mjs has no declaration file; the export
|
|
11
|
+
// shape is narrowed explicitly below.
|
|
12
|
+
import { WORKER_WRAPPER_SOURCE as _wrapper } from "./worker-wrapper.mjs";
|
|
13
|
+
/**
|
|
14
|
+
* The wrapper source written to `_worker.js`. Imported from the single
|
|
15
|
+
* canonical `.mjs` file so `src/build.ts` and `bin/cli.mjs` always stay
|
|
16
|
+
* in sync without any duplication.
|
|
17
|
+
*/
|
|
18
|
+
export const WORKER_WRAPPER_SOURCE = _wrapper;
|
|
19
|
+
/**
|
|
20
|
+
* Emit a Cloudflare Pages `_worker.js` that wraps the zfb input bundle.
|
|
21
|
+
*
|
|
22
|
+
* Output shape (two files in `outdir`):
|
|
23
|
+
*
|
|
24
|
+
* _worker.js — entry imported by Cloudflare Pages advanced mode
|
|
25
|
+
* _zfb_inner.mjs — the input bundle, copied verbatim
|
|
26
|
+
*
|
|
27
|
+
* The wrapper imports the inner bundle via the relative path
|
|
28
|
+
* `./_zfb_inner.mjs`. Workerd's Module loader resolves relative ESM
|
|
29
|
+
* imports inside an advanced-mode `_worker.js` directory, so this layout
|
|
30
|
+
* works without re-bundling.
|
|
31
|
+
*
|
|
32
|
+
* Why two files instead of one: re-bundling here would require a second
|
|
33
|
+
* esbuild pass and would force the adapter to ship its own esbuild
|
|
34
|
+
* binary slot. The two-file layout keeps the adapter dependency-free at
|
|
35
|
+
* runtime — it is just `node:fs` glue.
|
|
36
|
+
*/
|
|
37
|
+
export async function emitWorker(input) {
|
|
38
|
+
const { mkdir, copyFile, writeFile } = await import("node:fs/promises");
|
|
39
|
+
const { resolve, join } = await import("node:path");
|
|
40
|
+
const outdir = resolve(input.outdir);
|
|
41
|
+
const inputBundle = resolve(input.inputBundlePath);
|
|
42
|
+
await mkdir(outdir, { recursive: true });
|
|
43
|
+
const innerBundlePath = join(outdir, "_zfb_inner.mjs");
|
|
44
|
+
await copyFile(inputBundle, innerBundlePath);
|
|
45
|
+
const workerPath = join(outdir, "_worker.js");
|
|
46
|
+
await writeFile(workerPath, WORKER_WRAPPER_SOURCE, "utf8");
|
|
47
|
+
return { workerPath, innerBundlePath };
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +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;AAEzE;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAW,QAAkB,CAAC;AA+BhE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,KAAsB;IACrD,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;IACxE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;IAEpD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACrC,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAEnD,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACvD,MAAM,QAAQ,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;IAE7C,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC9C,MAAM,SAAS,CAAC,UAAU,EAAE,qBAAqB,EAAE,MAAM,CAAC,CAAC;IAE3D,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;AACzC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare execution context — minimal projection of the workerd
|
|
3
|
+
* `ExecutionContext` interface. We do not depend on `@cloudflare/workers-types`
|
|
4
|
+
* at the type level here because that would force every consumer of this
|
|
5
|
+
* package to install it; instead we keep a minimal structural shape and
|
|
6
|
+
* let users widen it via the generic on [`getCloudflareContext`] when
|
|
7
|
+
* they need richer bindings.
|
|
8
|
+
*/
|
|
9
|
+
export interface CloudflareExecutionContext {
|
|
10
|
+
/** Extends the lifetime of the request beyond the response. */
|
|
11
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
12
|
+
/** Falls through to the static origin on uncaught exceptions. */
|
|
13
|
+
passThroughOnException(): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Per-request Cloudflare context. `Env` defaults to `unknown` so the
|
|
17
|
+
* caller can narrow it at the call site (recommended) or leave it open.
|
|
18
|
+
*/
|
|
19
|
+
export interface CloudflareContext<Env = unknown> {
|
|
20
|
+
/** CF env bindings (secrets, KV, D1, …) wired up via wrangler.toml. */
|
|
21
|
+
readonly env: Env;
|
|
22
|
+
/** ExecutionContext for waitUntil / passThroughOnException. */
|
|
23
|
+
readonly ctx: CloudflareExecutionContext;
|
|
24
|
+
/** The original Request, useful when handlers want headers / URL. */
|
|
25
|
+
readonly request: Request;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Establish a Cloudflare context for the duration of `fn`. Used by the
|
|
29
|
+
* `_worker.js` wrapper; not normally called by user code.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runWithCloudflareContext<T>(context: CloudflareContext, fn: () => T): T;
|
|
32
|
+
/**
|
|
33
|
+
* Read the current Cloudflare context. Throws if called outside a
|
|
34
|
+
* Cloudflare request scope (e.g. from a build-time SSG render). Catch
|
|
35
|
+
* the error and gate on `prerender = false` if you need a route to work
|
|
36
|
+
* in both modes.
|
|
37
|
+
*
|
|
38
|
+
* The `Env` generic narrows the bindings shape — passing it is
|
|
39
|
+
* recommended so TypeScript catches typos like `env.ANTRHOPIC_KEY`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getCloudflareContext<Env = unknown>(): CloudflareContext<Env>;
|
|
42
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// `@takazudo/zfb-adapter-cloudflare` — Cloudflare Pages adapter for the
|
|
2
|
+
// zfb framework.
|
|
3
|
+
//
|
|
4
|
+
// This entry (`./`) is the **Workers-runtime** surface. It is safe to
|
|
5
|
+
// bundle into a Cloudflare Worker and does not depend on any Node-only
|
|
6
|
+
// built-ins beyond `node:async_hooks` (which workerd polyfills).
|
|
7
|
+
//
|
|
8
|
+
// Usage in a `prerender = false` route:
|
|
9
|
+
//
|
|
10
|
+
// import { getCloudflareContext } from "@takazudo/zfb-adapter-cloudflare";
|
|
11
|
+
//
|
|
12
|
+
// export const prerender = false;
|
|
13
|
+
// export default async function ApiRoute() {
|
|
14
|
+
// const { env, ctx } = getCloudflareContext<{ ANTHROPIC_API_KEY: string }>();
|
|
15
|
+
// // env.ANTHROPIC_API_KEY, ctx.waitUntil(...), etc.
|
|
16
|
+
// }
|
|
17
|
+
//
|
|
18
|
+
// For Node-only build helpers (e.g. `emitWorker`), import the `./build`
|
|
19
|
+
// sub-entry instead:
|
|
20
|
+
//
|
|
21
|
+
// import { emitWorker } from "@takazudo/zfb-adapter-cloudflare/build";
|
|
22
|
+
//
|
|
23
|
+
// ## Why AsyncLocalStorage on a globalThis registry
|
|
24
|
+
//
|
|
25
|
+
// Cloudflare Workers can process multiple requests concurrently in the
|
|
26
|
+
// same isolate. A naïve `globalThis.__env = env` write would race across
|
|
27
|
+
// requests. AsyncLocalStorage gives us a per-request scope that survives
|
|
28
|
+
// `await` points without interfering with sibling requests.
|
|
29
|
+
//
|
|
30
|
+
// We register the storage instance on `globalThis` under a stable key
|
|
31
|
+
// (`__zfb_cf_adapter_als__`) so the wrapper at `_worker.js` and the user
|
|
32
|
+
// pages bundled together can share the same instance even when the
|
|
33
|
+
// adapter module ends up duplicated in the final bundle graph (e.g. when
|
|
34
|
+
// the wrapper file is emitted side-by-side with the inner bundle and
|
|
35
|
+
// each pulls in its own copy of this module). Module-instance identity
|
|
36
|
+
// is the property AsyncLocalStorage relies on; the registry pattern is
|
|
37
|
+
// what makes it survive bundler duplication.
|
|
38
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
39
|
+
/** Stable globalThis key the registry pattern uses. */
|
|
40
|
+
const STORAGE_KEY = "__zfb_cf_adapter_als__";
|
|
41
|
+
/**
|
|
42
|
+
* Acquire (or lazily create) the singleton AsyncLocalStorage instance.
|
|
43
|
+
*
|
|
44
|
+
* Stored on `globalThis` under a stable key so the wrapper at
|
|
45
|
+
* `_worker.js` and the user bundle share state even if the adapter
|
|
46
|
+
* module ends up duplicated in the final module graph.
|
|
47
|
+
*/
|
|
48
|
+
function getStorage() {
|
|
49
|
+
const g = globalThis;
|
|
50
|
+
let als = g[STORAGE_KEY];
|
|
51
|
+
if (!als) {
|
|
52
|
+
als = new AsyncLocalStorage();
|
|
53
|
+
g[STORAGE_KEY] = als;
|
|
54
|
+
}
|
|
55
|
+
return als;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Establish a Cloudflare context for the duration of `fn`. Used by the
|
|
59
|
+
* `_worker.js` wrapper; not normally called by user code.
|
|
60
|
+
*/
|
|
61
|
+
export function runWithCloudflareContext(context, fn) {
|
|
62
|
+
return getStorage().run(context, fn);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Read the current Cloudflare context. Throws if called outside a
|
|
66
|
+
* Cloudflare request scope (e.g. from a build-time SSG render). Catch
|
|
67
|
+
* the error and gate on `prerender = false` if you need a route to work
|
|
68
|
+
* in both modes.
|
|
69
|
+
*
|
|
70
|
+
* The `Env` generic narrows the bindings shape — passing it is
|
|
71
|
+
* recommended so TypeScript catches typos like `env.ANTRHOPIC_KEY`.
|
|
72
|
+
*/
|
|
73
|
+
export function getCloudflareContext() {
|
|
74
|
+
const c = getStorage().getStore();
|
|
75
|
+
if (!c) {
|
|
76
|
+
throw new Error("[zfb-adapter-cloudflare] getCloudflareContext() called outside a Cloudflare request scope. " +
|
|
77
|
+
"This usually means the route was rendered at build time (SSG) instead of dispatched by " +
|
|
78
|
+
"the Worker. Add `export const prerender = false;` to the page if it needs Cloudflare bindings.");
|
|
79
|
+
}
|
|
80
|
+
return c;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,iBAAiB;AACjB,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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Single source of truth for the `_worker.js` wrapper string.
|
|
2
|
+
//
|
|
3
|
+
// Kept as a plain `.mjs` module (no TypeScript) so it can be imported from
|
|
4
|
+
// both the typed TypeScript surface (`src/build.ts`) and the dependency-free
|
|
5
|
+
// CLI binary (`bin/cli.mjs`) without needing a TypeScript loader.
|
|
6
|
+
//
|
|
7
|
+
// Do NOT inline this string in any other file. Edit it here and the two
|
|
8
|
+
// consumers automatically see the update.
|
|
9
|
+
|
|
10
|
+
/** @type {string} */
|
|
11
|
+
export const WORKER_WRAPPER_SOURCE = `// AUTO-GENERATED by @takazudo/zfb-adapter-cloudflare. Do not edit.
|
|
12
|
+
//
|
|
13
|
+
// Cloudflare Pages advanced mode entry. Forwards (request, env, ctx) to
|
|
14
|
+
// the inner zfb worker bundle, exposing env/ctx to user code via
|
|
15
|
+
// AsyncLocalStorage under a stable globalThis key.
|
|
16
|
+
//
|
|
17
|
+
// The same key is read by @takazudo/zfb-adapter-cloudflare's
|
|
18
|
+
// getCloudflareContext() inside the user bundle, so the two ends share
|
|
19
|
+
// state even though they live in separate ESM module instances.
|
|
20
|
+
//
|
|
21
|
+
// CF Pages advanced-mode contract: when _worker.js is present, every
|
|
22
|
+
// request hits the worker — static asset routing is OFF unless the
|
|
23
|
+
// worker explicitly delegates to env.ASSETS. The dispatch order below
|
|
24
|
+
// is deliberately "ASSETS first, inner on 404":
|
|
25
|
+
//
|
|
26
|
+
// - GET/HEAD requests probe env.ASSETS first. CF Pages' asset server
|
|
27
|
+
// handles the trailing-slash canonicalisation for SSG output (e.g.
|
|
28
|
+
// "/docs/foo" → 308 → "/docs/foo/" → dist/docs/foo/index.html), so
|
|
29
|
+
// prerendered routes get the build-time head-injected HTML
|
|
30
|
+
// (<link rel="stylesheet">, <script type="module" src="/assets/
|
|
31
|
+
// islands-…">). If we let the inner Hono router handle them first,
|
|
32
|
+
// it would dynamic-SSR the page WITHOUT the prod head injection
|
|
33
|
+
// (which is a build-time post-process, not a runtime concern), and
|
|
34
|
+
// islands would never hydrate. See zfb#XXX (filed by Wave 10 of
|
|
35
|
+
// the zudo-doc#1355 zfb-pipeline-gaps epic).
|
|
36
|
+
// - On 404 from ASSETS, fall through to the inner zfb worker. This is
|
|
37
|
+
// where genuinely dynamic routes (\`prerender = false\`, e.g.
|
|
38
|
+
// \`pages/api/*.tsx\`) are served.
|
|
39
|
+
// - Non-GET/HEAD requests skip ASSETS and go straight to the inner —
|
|
40
|
+
// CF Pages assets are read-only by definition, and we want POSTs
|
|
41
|
+
// (\`/api/ai-chat\`, etc.) to reach the SSR handler without a probe
|
|
42
|
+
// that would always 405 / 404.
|
|
43
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
44
|
+
import inner from "./_zfb_inner.mjs";
|
|
45
|
+
|
|
46
|
+
const STORAGE_KEY = "__zfb_cf_adapter_als__";
|
|
47
|
+
|
|
48
|
+
function getStorage() {
|
|
49
|
+
const g = globalThis;
|
|
50
|
+
let als = g[STORAGE_KEY];
|
|
51
|
+
if (!als) {
|
|
52
|
+
als = new AsyncLocalStorage();
|
|
53
|
+
g[STORAGE_KEY] = als;
|
|
54
|
+
}
|
|
55
|
+
return als;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function canDelegateToAssets(env) {
|
|
59
|
+
return Boolean(env && env.ASSETS && typeof env.ASSETS.fetch === "function");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isAssetProbeMethod(method) {
|
|
63
|
+
// Only safe, side-effect-free methods probe the asset server. POST/
|
|
64
|
+
// PUT/PATCH/DELETE go straight to the inner SSR worker.
|
|
65
|
+
return method === "GET" || method === "HEAD";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default {
|
|
69
|
+
async fetch(request, env, ctx) {
|
|
70
|
+
if (isAssetProbeMethod(request.method) && canDelegateToAssets(env)) {
|
|
71
|
+
const assetResponse = await env.ASSETS.fetch(request);
|
|
72
|
+
if (assetResponse.status !== 404) {
|
|
73
|
+
return assetResponse;
|
|
74
|
+
}
|
|
75
|
+
// Fall through to the inner worker for genuinely dynamic routes.
|
|
76
|
+
}
|
|
77
|
+
const store = { env, ctx, request };
|
|
78
|
+
return getStorage().run(store, () => inner.fetch(request));
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
`;
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@takazudo/zfb-adapter-cloudflare",
|
|
3
|
+
"version": "0.1.0-next.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Rust-built static-site engine for Astro and Next.js users — millisecond rebuilds, single binary. Cloudflare Pages adapter.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"zfb",
|
|
9
|
+
"zfb-adapter",
|
|
10
|
+
"cloudflare",
|
|
11
|
+
"cloudflare-pages",
|
|
12
|
+
"cloudflare-workers",
|
|
13
|
+
"ssg",
|
|
14
|
+
"static-site-generator",
|
|
15
|
+
"adapter"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Takeshi Takatsudo <takazudo@gmail.com> (https://github.com/Takazudo)",
|
|
19
|
+
"homepage": "https://takazudomodular.com/pj/zudo-front-builder/",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Takazudo/zudo-front-builder.git",
|
|
23
|
+
"directory": "packages/zfb-adapter-cloudflare"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Takazudo/zudo-front-builder/issues"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./build": {
|
|
36
|
+
"types": "./dist/build.d.ts",
|
|
37
|
+
"default": "./dist/build.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"bin": {
|
|
41
|
+
"zfb-adapter-cloudflare": "./bin/cli.mjs"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"bin",
|
|
46
|
+
"src/worker-wrapper.mjs",
|
|
47
|
+
"README.md",
|
|
48
|
+
"CHANGELOG.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
],
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22.0.0",
|
|
56
|
+
"pnpm": ">=10.0.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^22.0.0",
|
|
60
|
+
"typescript": "^5.9.0",
|
|
61
|
+
"vitest": "^2.1.9"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsc && cp src/worker-wrapper.mjs dist/worker-wrapper.mjs",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"test:watch": "vitest",
|
|
67
|
+
"typecheck": "tsc --noEmit"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Single source of truth for the `_worker.js` wrapper string.
|
|
2
|
+
//
|
|
3
|
+
// Kept as a plain `.mjs` module (no TypeScript) so it can be imported from
|
|
4
|
+
// both the typed TypeScript surface (`src/build.ts`) and the dependency-free
|
|
5
|
+
// CLI binary (`bin/cli.mjs`) without needing a TypeScript loader.
|
|
6
|
+
//
|
|
7
|
+
// Do NOT inline this string in any other file. Edit it here and the two
|
|
8
|
+
// consumers automatically see the update.
|
|
9
|
+
|
|
10
|
+
/** @type {string} */
|
|
11
|
+
export const WORKER_WRAPPER_SOURCE = `// AUTO-GENERATED by @takazudo/zfb-adapter-cloudflare. Do not edit.
|
|
12
|
+
//
|
|
13
|
+
// Cloudflare Pages advanced mode entry. Forwards (request, env, ctx) to
|
|
14
|
+
// the inner zfb worker bundle, exposing env/ctx to user code via
|
|
15
|
+
// AsyncLocalStorage under a stable globalThis key.
|
|
16
|
+
//
|
|
17
|
+
// The same key is read by @takazudo/zfb-adapter-cloudflare's
|
|
18
|
+
// getCloudflareContext() inside the user bundle, so the two ends share
|
|
19
|
+
// state even though they live in separate ESM module instances.
|
|
20
|
+
//
|
|
21
|
+
// CF Pages advanced-mode contract: when _worker.js is present, every
|
|
22
|
+
// request hits the worker — static asset routing is OFF unless the
|
|
23
|
+
// worker explicitly delegates to env.ASSETS. The dispatch order below
|
|
24
|
+
// is deliberately "ASSETS first, inner on 404":
|
|
25
|
+
//
|
|
26
|
+
// - GET/HEAD requests probe env.ASSETS first. CF Pages' asset server
|
|
27
|
+
// handles the trailing-slash canonicalisation for SSG output (e.g.
|
|
28
|
+
// "/docs/foo" → 308 → "/docs/foo/" → dist/docs/foo/index.html), so
|
|
29
|
+
// prerendered routes get the build-time head-injected HTML
|
|
30
|
+
// (<link rel="stylesheet">, <script type="module" src="/assets/
|
|
31
|
+
// islands-…">). If we let the inner Hono router handle them first,
|
|
32
|
+
// it would dynamic-SSR the page WITHOUT the prod head injection
|
|
33
|
+
// (which is a build-time post-process, not a runtime concern), and
|
|
34
|
+
// islands would never hydrate. See zfb#XXX (filed by Wave 10 of
|
|
35
|
+
// the zudo-doc#1355 zfb-pipeline-gaps epic).
|
|
36
|
+
// - On 404 from ASSETS, fall through to the inner zfb worker. This is
|
|
37
|
+
// where genuinely dynamic routes (\`prerender = false\`, e.g.
|
|
38
|
+
// \`pages/api/*.tsx\`) are served.
|
|
39
|
+
// - Non-GET/HEAD requests skip ASSETS and go straight to the inner —
|
|
40
|
+
// CF Pages assets are read-only by definition, and we want POSTs
|
|
41
|
+
// (\`/api/ai-chat\`, etc.) to reach the SSR handler without a probe
|
|
42
|
+
// that would always 405 / 404.
|
|
43
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
44
|
+
import inner from "./_zfb_inner.mjs";
|
|
45
|
+
|
|
46
|
+
const STORAGE_KEY = "__zfb_cf_adapter_als__";
|
|
47
|
+
|
|
48
|
+
function getStorage() {
|
|
49
|
+
const g = globalThis;
|
|
50
|
+
let als = g[STORAGE_KEY];
|
|
51
|
+
if (!als) {
|
|
52
|
+
als = new AsyncLocalStorage();
|
|
53
|
+
g[STORAGE_KEY] = als;
|
|
54
|
+
}
|
|
55
|
+
return als;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function canDelegateToAssets(env) {
|
|
59
|
+
return Boolean(env && env.ASSETS && typeof env.ASSETS.fetch === "function");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isAssetProbeMethod(method) {
|
|
63
|
+
// Only safe, side-effect-free methods probe the asset server. POST/
|
|
64
|
+
// PUT/PATCH/DELETE go straight to the inner SSR worker.
|
|
65
|
+
return method === "GET" || method === "HEAD";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default {
|
|
69
|
+
async fetch(request, env, ctx) {
|
|
70
|
+
if (isAssetProbeMethod(request.method) && canDelegateToAssets(env)) {
|
|
71
|
+
const assetResponse = await env.ASSETS.fetch(request);
|
|
72
|
+
if (assetResponse.status !== 404) {
|
|
73
|
+
return assetResponse;
|
|
74
|
+
}
|
|
75
|
+
// Fall through to the inner worker for genuinely dynamic routes.
|
|
76
|
+
}
|
|
77
|
+
const store = { env, ctx, request };
|
|
78
|
+
return getStorage().run(store, () => inner.fetch(request));
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
`;
|