@takazudo/zfb-adapter-cloudflare 0.1.0-next.80 → 0.1.0-next.82
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 +38 -25
- package/bin/cli.mjs +33 -10
- package/dist/build.d.ts +19 -9
- package/dist/build.js +10 -8
- package/dist/build.js.map +1 -1
- package/dist/emit-worker.mjs +122 -15
- package/package.json +1 -1
- package/src/emit-worker.mjs +122 -15
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 adapter for [zfb][zfb-site],
|
|
6
|
-
Assets
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
The Cloudflare adapter for [zfb][zfb-site], verified on **Workers Static
|
|
6
|
+
Assets**. It wraps the `@takazudo/zfb-runtime` page router into a Worker entry
|
|
7
|
+
(`_worker.js`), threading `(env, ctx)` through to user code via
|
|
8
|
+
`AsyncLocalStorage`. Cloudflare Pages advanced mode is unverified.
|
|
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-*`
|
|
@@ -69,9 +69,9 @@ lifecycle (`wrangler d1 create`, migrations, preview-vs-prod).
|
|
|
69
69
|
1. Render every SSG page (`prerender !== false`) into static HTML under
|
|
70
70
|
`dist/`.
|
|
71
71
|
2. Hand the SSR bundle to this adapter, which writes `dist/_worker.js`
|
|
72
|
-
(the wrapper), `dist/_zfb_inner.mjs` (the bundle),
|
|
73
|
-
`dist/.assetsignore` (see below) — ready
|
|
74
|
-
Static Assets
|
|
72
|
+
(the wrapper), `dist/_zfb_inner.mjs` (the bundle), copied
|
|
73
|
+
`x-<hash>.wasm` modules, and `dist/.assetsignore` (see below) — ready
|
|
74
|
+
for Workers Static Assets.
|
|
75
75
|
|
|
76
76
|
## `wrangler.toml`
|
|
77
77
|
|
|
@@ -134,34 +134,47 @@ wrangler deploy # ship it
|
|
|
134
134
|
## `.assetsignore`
|
|
135
135
|
|
|
136
136
|
The adapter emits `dist/.assetsignore` alongside `_worker.js` and
|
|
137
|
-
`_zfb_inner.mjs
|
|
137
|
+
`_zfb_inner.mjs`. When zfb passes one or more `--asset` Wasm modules, it
|
|
138
|
+
also adds every copied basename:
|
|
138
139
|
|
|
139
140
|
```
|
|
140
141
|
_worker.js
|
|
141
142
|
_zfb_inner.mjs
|
|
143
|
+
index_bg-a1b2c3d4.wasm
|
|
142
144
|
```
|
|
143
145
|
|
|
144
|
-
This excludes the wrapper
|
|
145
|
-
upload, so they are reachable only through the Worker's own module
|
|
146
|
+
This excludes the wrapper, inner SSR bundle, and compiled Wasm modules from
|
|
147
|
+
the asset upload, so they are reachable only through the Worker's own module
|
|
146
148
|
graph — never served as public static files. Without it, a request for
|
|
147
149
|
`/_worker.js` or `/_zfb_inner.mjs` would serve your server code as a
|
|
148
150
|
plain-text download.
|
|
149
151
|
|
|
150
|
-
**
|
|
151
|
-
`dist/` _after_ running this adapter. If
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
152
|
+
**Merge behavior:** zfb copies your project's `public/` directory into
|
|
153
|
+
`dist/` _after_ running this adapter. If `public/.assetsignore` adds entries,
|
|
154
|
+
zfb merges them with the generated entries; it does not drop the wrapper,
|
|
155
|
+
inner-bundle, or Wasm exclusions.
|
|
156
|
+
|
|
157
|
+
## CLI asset contract
|
|
158
|
+
|
|
159
|
+
zfb calls the package CLI with the SSR bundle plus one repeatable `--asset`
|
|
160
|
+
path for every bundle-relative Wasm module:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
zfb-adapter-cloudflare bundle ./bundle.mjs --outdir ./dist \
|
|
164
|
+
--asset index_bg-a1b2c3d4.wasm
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Each asset path must be relative to the input bundle directory. The CLI copies
|
|
168
|
+
it into `outdir` under its basename and records that basename in
|
|
169
|
+
`.assetsignore`; paths that escape the input directory or collide with emitted
|
|
170
|
+
output fail the build.
|
|
171
|
+
|
|
172
|
+
## Cloudflare Pages advanced mode
|
|
173
|
+
|
|
174
|
+
The root-level `_worker.js` follows the Cloudflare Pages advanced-mode
|
|
175
|
+
convention, but this adapter is only verified on Workers Static Assets.
|
|
176
|
+
Cloudflare Pages advanced mode remains unverified and should not be treated as
|
|
177
|
+
a supported deployment target until it has a dedicated smoke test.
|
|
165
178
|
|
|
166
179
|
## Why two bundle files instead of one
|
|
167
180
|
|
package/bin/cli.mjs
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Subcommands:
|
|
6
6
|
//
|
|
7
|
-
// bundle <input> --outdir <dir>
|
|
7
|
+
// bundle <input> --outdir <dir> [--asset <path>]...
|
|
8
8
|
//
|
|
9
9
|
// Wrap the input ESM bundle into a Cloudflare Workers Static Assets
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// the asset upload. The input bundle is the file `zfb_build`'s
|
|
13
|
-
// bundler emits; <dir> is typically the project's `dist/`.
|
|
10
|
+
// `_worker.js` placed under <dir>, alongside a `.assetsignore` that
|
|
11
|
+
// excludes the wrapper, inner bundle, and every copied --asset basename
|
|
12
|
+
// from the asset upload. The input bundle is the file `zfb_build`'s
|
|
13
|
+
// bundler emits; <dir> is typically the project's `dist/`. Cloudflare
|
|
14
|
+
// Pages advanced mode is unverified.
|
|
14
15
|
//
|
|
15
16
|
// The CLI is intentionally tiny and dependency-free. It imports the
|
|
16
17
|
// wrapper string from the canonical `src/worker-wrapper.mjs` (plain JS,
|
|
@@ -34,8 +35,13 @@ export { WORKER_WRAPPER_SOURCE };
|
|
|
34
35
|
|
|
35
36
|
import { emitWorker as _emitWorker } from "../src/emit-worker.mjs";
|
|
36
37
|
|
|
37
|
-
export async function emitWorker({ inputBundlePath, outdir }) {
|
|
38
|
-
return _emitWorker({
|
|
38
|
+
export async function emitWorker({ inputBundlePath, outdir, assets = [] }) {
|
|
39
|
+
return _emitWorker({
|
|
40
|
+
inputBundlePath,
|
|
41
|
+
outdir,
|
|
42
|
+
assets,
|
|
43
|
+
workerWrapperSource: WORKER_WRAPPER_SOURCE,
|
|
44
|
+
});
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
// ---------------------------------------------------------------------------
|
|
@@ -49,14 +55,15 @@ function fail(message) {
|
|
|
49
55
|
|
|
50
56
|
function printUsage() {
|
|
51
57
|
process.stdout.write(`Usage:
|
|
52
|
-
zfb-adapter-cloudflare bundle <input> --outdir <dir>
|
|
58
|
+
zfb-adapter-cloudflare bundle <input> --outdir <dir> [--asset <path>]...
|
|
53
59
|
|
|
54
60
|
Wrap an ESM bundle (the output of zfb-build's bundler) into a
|
|
55
61
|
Cloudflare Workers Static Assets \`_worker.js\` placed under <dir>
|
|
56
|
-
|
|
62
|
+
with a protected \`.assetsignore\`. Cloudflare Pages advanced mode is unverified.
|
|
57
63
|
|
|
58
64
|
Options:
|
|
59
65
|
--outdir <dir> Output directory. Required.
|
|
66
|
+
--asset <path> Bundle-relative Wasm asset to copy. Repeatable.
|
|
60
67
|
-h, --help Show this help.
|
|
61
68
|
`);
|
|
62
69
|
}
|
|
@@ -73,6 +80,7 @@ function parseArgs(argv) {
|
|
|
73
80
|
|
|
74
81
|
let input = null;
|
|
75
82
|
let outdir = null;
|
|
83
|
+
const assets = [];
|
|
76
84
|
let i = 1;
|
|
77
85
|
while (i < args.length) {
|
|
78
86
|
const arg = args[i];
|
|
@@ -88,6 +96,20 @@ function parseArgs(argv) {
|
|
|
88
96
|
i += 1;
|
|
89
97
|
continue;
|
|
90
98
|
}
|
|
99
|
+
if (arg === "--asset") {
|
|
100
|
+
const next = args[i + 1];
|
|
101
|
+
if (!next) fail("--asset requires a path argument");
|
|
102
|
+
assets.push(next);
|
|
103
|
+
i += 2;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (arg.startsWith("--asset=")) {
|
|
107
|
+
const asset = arg.slice("--asset=".length);
|
|
108
|
+
if (!asset) fail("--asset requires a path argument");
|
|
109
|
+
assets.push(asset);
|
|
110
|
+
i += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
91
113
|
if (arg.startsWith("--")) {
|
|
92
114
|
fail(`unknown option: ${arg}`);
|
|
93
115
|
}
|
|
@@ -102,7 +124,7 @@ function parseArgs(argv) {
|
|
|
102
124
|
if (!input) fail("missing required positional argument: <input>");
|
|
103
125
|
if (!outdir) fail("missing required option: --outdir <dir>");
|
|
104
126
|
|
|
105
|
-
return { command: "bundle", input, outdir };
|
|
127
|
+
return { command: "bundle", input, outdir, assets };
|
|
106
128
|
}
|
|
107
129
|
|
|
108
130
|
async function main() {
|
|
@@ -140,6 +162,7 @@ async function main() {
|
|
|
140
162
|
const out = await emitWorker({
|
|
141
163
|
inputBundlePath: inputAbs,
|
|
142
164
|
outdir: outdirAbs,
|
|
165
|
+
assets: parsed.assets,
|
|
143
166
|
});
|
|
144
167
|
process.stdout.write(
|
|
145
168
|
`wrote ${out.workerPath}\nwrote ${out.innerBundlePath}\nwrote ${out.assetsIgnorePath}\n`,
|
package/dist/build.d.ts
CHANGED
|
@@ -19,9 +19,18 @@ export interface EmitWorkerInput {
|
|
|
19
19
|
/**
|
|
20
20
|
* Absolute path to the output directory. The emitter creates it if
|
|
21
21
|
* missing and writes `_worker.js`, `_zfb_inner.mjs` (the copied input
|
|
22
|
-
* bundle), and `.assetsignore` into it.
|
|
22
|
+
* bundle), copied Wasm assets, and `.assetsignore` into it. The ignore
|
|
23
|
+
* file protects every generated JavaScript and Wasm basename from the
|
|
24
|
+
* public asset upload.
|
|
23
25
|
*/
|
|
24
26
|
readonly outdir: string;
|
|
27
|
+
/**
|
|
28
|
+
* Wasm modules emitted beside the input bundle. Relative paths resolve from
|
|
29
|
+
* the input bundle's directory and each module is copied into the Worker
|
|
30
|
+
* package under its basename, then added to `.assetsignore` so it remains a
|
|
31
|
+
* Worker module rather than a public static asset.
|
|
32
|
+
*/
|
|
33
|
+
readonly assets?: readonly string[];
|
|
25
34
|
}
|
|
26
35
|
/**
|
|
27
36
|
* Output paths the emitter produced. Returned for callers that want to
|
|
@@ -33,17 +42,18 @@ export interface EmitWorkerOutput {
|
|
|
33
42
|
readonly assetsIgnorePath: string;
|
|
34
43
|
}
|
|
35
44
|
/**
|
|
36
|
-
* Emit a Cloudflare Workers Static Assets
|
|
37
|
-
*
|
|
45
|
+
* Emit a Cloudflare Workers Static Assets `_worker.js` that wraps the zfb
|
|
46
|
+
* input bundle. Cloudflare Pages advanced mode is unverified.
|
|
38
47
|
*
|
|
39
|
-
* Output shape (
|
|
48
|
+
* Output shape (two generated JavaScript files, `.assetsignore`, and zero or
|
|
49
|
+
* more copied Wasm assets in `outdir`):
|
|
40
50
|
*
|
|
41
|
-
* _worker.js — Worker entry point (`main` in wrangler.toml
|
|
42
|
-
* Pages advanced-mode convention)
|
|
51
|
+
* _worker.js — Worker entry point (`main` in wrangler.toml)
|
|
43
52
|
* _zfb_inner.mjs — the input bundle, copied verbatim
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
53
|
+
* <asset>.wasm — each bundle-relative Wasm input, copied by basename
|
|
54
|
+
* .assetsignore — excludes every generated JavaScript and Wasm basename
|
|
55
|
+
* from the asset upload so they are only reachable
|
|
56
|
+
* through the Worker's module graph
|
|
47
57
|
*
|
|
48
58
|
* The wrapper imports the inner bundle via the relative path
|
|
49
59
|
* `./_zfb_inner.mjs`. Workerd's Module loader resolves relative ESM
|
package/dist/build.js
CHANGED
|
@@ -20,17 +20,18 @@ import { emitWorker as _emitWorker } from "./emit-worker.mjs";
|
|
|
20
20
|
*/
|
|
21
21
|
export const WORKER_WRAPPER_SOURCE = _wrapper;
|
|
22
22
|
/**
|
|
23
|
-
* Emit a Cloudflare Workers Static Assets
|
|
24
|
-
*
|
|
23
|
+
* Emit a Cloudflare Workers Static Assets `_worker.js` that wraps the zfb
|
|
24
|
+
* input bundle. Cloudflare Pages advanced mode is unverified.
|
|
25
25
|
*
|
|
26
|
-
* Output shape (
|
|
26
|
+
* Output shape (two generated JavaScript files, `.assetsignore`, and zero or
|
|
27
|
+
* more copied Wasm assets in `outdir`):
|
|
27
28
|
*
|
|
28
|
-
* _worker.js — Worker entry point (`main` in wrangler.toml
|
|
29
|
-
* Pages advanced-mode convention)
|
|
29
|
+
* _worker.js — Worker entry point (`main` in wrangler.toml)
|
|
30
30
|
* _zfb_inner.mjs — the input bundle, copied verbatim
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* <asset>.wasm — each bundle-relative Wasm input, copied by basename
|
|
32
|
+
* .assetsignore — excludes every generated JavaScript and Wasm basename
|
|
33
|
+
* from the asset upload so they are only reachable
|
|
34
|
+
* through the Worker's module graph
|
|
34
35
|
*
|
|
35
36
|
* The wrapper imports the inner bundle via the relative path
|
|
36
37
|
* `./_zfb_inner.mjs`. Workerd's Module loader resolves relative ESM
|
|
@@ -46,6 +47,7 @@ export async function emitWorker(input) {
|
|
|
46
47
|
return _emitWorker({
|
|
47
48
|
inputBundlePath: input.inputBundlePath,
|
|
48
49
|
outdir: input.outdir,
|
|
50
|
+
assets: input.assets,
|
|
49
51
|
workerWrapperSource: WORKER_WRAPPER_SOURCE,
|
|
50
52
|
});
|
|
51
53
|
}
|
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;AAyChE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;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,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), copied Wasm assets, and `.assetsignore` into it. The ignore\n * file protects every generated JavaScript and Wasm basename from the\n * public asset upload.\n */\n readonly outdir: string;\n /**\n * Wasm modules emitted beside the input bundle. Relative paths resolve from\n * the input bundle's directory and each module is copied into the Worker\n * package under its basename, then added to `.assetsignore` so it remains a\n * Worker module rather than a public static asset.\n */\n readonly assets?: readonly 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 `_worker.js` that wraps the zfb\n * input bundle. Cloudflare Pages advanced mode is unverified.\n *\n * Output shape (two generated JavaScript files, `.assetsignore`, and zero or\n * more copied Wasm assets in `outdir`):\n *\n * _worker.js — Worker entry point (`main` in wrangler.toml)\n * _zfb_inner.mjs — the input bundle, copied verbatim\n * <asset>.wasm — each bundle-relative Wasm input, copied by basename\n * .assetsignore — excludes every generated JavaScript and Wasm basename\n * from the asset upload so they are only reachable\n * through the Worker's module graph\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 assets: input.assets,\n workerWrapperSource: WORKER_WRAPPER_SOURCE,\n }) as Promise<EmitWorkerOutput>;\n}\n"]}
|
package/dist/emit-worker.mjs
CHANGED
|
@@ -9,33 +9,132 @@
|
|
|
9
9
|
//
|
|
10
10
|
// invariant: no runtime npm deps — see SECURITY-DEPS.md
|
|
11
11
|
|
|
12
|
-
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
13
|
-
import { join, resolve } from "node:path";
|
|
12
|
+
import { copyFile, lstat, mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
13
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
14
|
|
|
15
|
-
// `.assetsignore` tells the Workers Static Assets uploader
|
|
16
|
-
//
|
|
17
|
-
// through the Worker's module graph
|
|
18
|
-
|
|
15
|
+
// `.assetsignore` tells the Workers Static Assets uploader to skip the
|
|
16
|
+
// generated JavaScript files. Every copied Wasm basename is appended later,
|
|
17
|
+
// so it too is reachable only through the Worker's module graph and never
|
|
18
|
+
// served as a public static asset.
|
|
19
|
+
const ASSETS_IGNORE_BASE_ENTRIES = ["_worker.js", "_zfb_inner.mjs"];
|
|
20
|
+
const RESERVED_OUTPUT_NAMES = new Set([...ASSETS_IGNORE_BASE_ENTRIES, ".assetsignore"]);
|
|
21
|
+
|
|
22
|
+
function isPathWithin(parent, candidate) {
|
|
23
|
+
const pathFromParent = relative(parent, candidate);
|
|
24
|
+
return (
|
|
25
|
+
pathFromParent === "" ||
|
|
26
|
+
(!pathFromParent.startsWith(`..${sep}`) &&
|
|
27
|
+
pathFromParent !== ".." &&
|
|
28
|
+
!isAbsolute(pathFromParent))
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function resolveAssets(inputBundlePath, assetPaths) {
|
|
33
|
+
const inputDir = dirname(inputBundlePath);
|
|
34
|
+
const canonicalInputDir = await realpath(inputDir);
|
|
35
|
+
const names = new Set();
|
|
36
|
+
const resolvedAssets = [];
|
|
37
|
+
|
|
38
|
+
for (const assetPath of assetPaths) {
|
|
39
|
+
if (typeof assetPath !== "string" || isAbsolute(assetPath)) {
|
|
40
|
+
throw new Error("asset path must be bundle-relative: " + String(assetPath));
|
|
41
|
+
}
|
|
42
|
+
const sourcePath = resolve(inputDir, assetPath);
|
|
43
|
+
if (!isPathWithin(inputDir, sourcePath)) {
|
|
44
|
+
throw new Error("asset path escapes the input bundle directory: " + assetPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sourceInfo = await stat(sourcePath);
|
|
48
|
+
if (!sourceInfo.isFile()) {
|
|
49
|
+
throw new Error("asset is not a file: " + sourcePath);
|
|
50
|
+
}
|
|
51
|
+
const canonicalSourcePath = await realpath(sourcePath);
|
|
52
|
+
if (!isPathWithin(canonicalInputDir, canonicalSourcePath)) {
|
|
53
|
+
throw new Error("asset path resolves outside the input bundle directory: " + assetPath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const outputName = basename(sourcePath);
|
|
57
|
+
if (!outputName || outputName === "." || outputName === "..") {
|
|
58
|
+
throw new Error("asset path has no valid basename: " + assetPath);
|
|
59
|
+
}
|
|
60
|
+
if (RESERVED_OUTPUT_NAMES.has(outputName)) {
|
|
61
|
+
throw new Error("asset basename collides with generated adapter output: " + outputName);
|
|
62
|
+
}
|
|
63
|
+
if (names.has(outputName)) {
|
|
64
|
+
throw new Error("asset basename collision: " + outputName);
|
|
65
|
+
}
|
|
66
|
+
names.add(outputName);
|
|
67
|
+
resolvedAssets.push({ sourcePath: canonicalSourcePath, outputName });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return resolvedAssets;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function pathExists(path) {
|
|
74
|
+
try {
|
|
75
|
+
await lstat(path);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function readExistingAssetsIgnore(path) {
|
|
86
|
+
try {
|
|
87
|
+
return await readFile(path, "utf8");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mergeAssetsIgnore(existing, requiredEntries) {
|
|
97
|
+
const listed = new Set(existing.split(/\r?\n/));
|
|
98
|
+
const missing = requiredEntries.filter((entry) => !listed.has(entry));
|
|
99
|
+
if (missing.length === 0) {
|
|
100
|
+
return existing;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? existing + "\n" : existing;
|
|
104
|
+
return prefix + missing.map((entry) => entry + "\n").join("");
|
|
105
|
+
}
|
|
19
106
|
|
|
20
107
|
/**
|
|
21
|
-
* Emit a Cloudflare Workers Static Assets
|
|
22
|
-
*
|
|
108
|
+
* Emit a Cloudflare Workers Static Assets `_worker.js` that wraps the zfb
|
|
109
|
+
* input bundle. Cloudflare Pages advanced mode is unverified.
|
|
23
110
|
*
|
|
24
|
-
* Output shape (
|
|
111
|
+
* Output shape (two generated JavaScript files, `.assetsignore`, and zero or
|
|
112
|
+
* more copied Wasm assets in `outdir`):
|
|
25
113
|
*
|
|
26
|
-
* _worker.js — Worker entry point (`main` in wrangler.toml
|
|
27
|
-
* Pages advanced-mode convention)
|
|
114
|
+
* _worker.js — Worker entry point (`main` in wrangler.toml)
|
|
28
115
|
* _zfb_inner.mjs — the input bundle, copied verbatim
|
|
29
|
-
*
|
|
116
|
+
* <asset>.wasm — each bundle-relative Wasm input, copied by basename
|
|
117
|
+
* .assetsignore — excludes every generated JavaScript and Wasm basename
|
|
118
|
+
* from the asset upload
|
|
30
119
|
*
|
|
31
|
-
* @param {{ inputBundlePath: string; outdir: string; workerWrapperSource: string }} input
|
|
120
|
+
* @param {{ inputBundlePath: string; outdir: string; assets?: readonly string[]; workerWrapperSource: string }} input
|
|
32
121
|
* @returns {Promise<{ workerPath: string; innerBundlePath: string; assetsIgnorePath: string }>}
|
|
33
122
|
*/
|
|
34
|
-
export async function emitWorker({ inputBundlePath, outdir, workerWrapperSource }) {
|
|
123
|
+
export async function emitWorker({ inputBundlePath, outdir, assets = [], workerWrapperSource }) {
|
|
35
124
|
const outdirAbs = resolve(outdir);
|
|
36
125
|
const inputAbs = resolve(inputBundlePath);
|
|
126
|
+
const resolvedAssets = await resolveAssets(inputAbs, assets);
|
|
37
127
|
|
|
38
128
|
await mkdir(outdirAbs, { recursive: true });
|
|
129
|
+
for (const asset of resolvedAssets) {
|
|
130
|
+
const destination = join(outdirAbs, asset.outputName);
|
|
131
|
+
if (await pathExists(destination)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"asset basename collision: " + asset.outputName + " would overwrite " + destination,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
39
138
|
const innerBundlePath = join(outdirAbs, "_zfb_inner.mjs");
|
|
40
139
|
await copyFile(inputAbs, innerBundlePath);
|
|
41
140
|
|
|
@@ -43,7 +142,15 @@ export async function emitWorker({ inputBundlePath, outdir, workerWrapperSource
|
|
|
43
142
|
await writeFile(workerPath, workerWrapperSource, "utf8");
|
|
44
143
|
|
|
45
144
|
const assetsIgnorePath = join(outdirAbs, ".assetsignore");
|
|
46
|
-
await
|
|
145
|
+
const existingAssetsIgnore = await readExistingAssetsIgnore(assetsIgnorePath);
|
|
146
|
+
for (const asset of resolvedAssets) {
|
|
147
|
+
await copyFile(asset.sourcePath, join(outdirAbs, asset.outputName));
|
|
148
|
+
}
|
|
149
|
+
const assetsIgnore = mergeAssetsIgnore(existingAssetsIgnore, [
|
|
150
|
+
...ASSETS_IGNORE_BASE_ENTRIES,
|
|
151
|
+
...resolvedAssets.map((asset) => asset.outputName),
|
|
152
|
+
]);
|
|
153
|
+
await writeFile(assetsIgnorePath, assetsIgnore, "utf8");
|
|
47
154
|
|
|
48
155
|
return { workerPath, innerBundlePath, assetsIgnorePath };
|
|
49
156
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takazudo/zfb-adapter-cloudflare",
|
|
3
|
-
"version": "0.1.0-next.
|
|
3
|
+
"version": "0.1.0-next.82",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Rust-built static-site engine for Astro and Next.js users — millisecond rebuilds, single binary. Cloudflare adapter: Workers Static Assets (Pages-compatible).",
|
package/src/emit-worker.mjs
CHANGED
|
@@ -9,33 +9,132 @@
|
|
|
9
9
|
//
|
|
10
10
|
// invariant: no runtime npm deps — see SECURITY-DEPS.md
|
|
11
11
|
|
|
12
|
-
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
13
|
-
import { join, resolve } from "node:path";
|
|
12
|
+
import { copyFile, lstat, mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
13
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
14
|
|
|
15
|
-
// `.assetsignore` tells the Workers Static Assets uploader
|
|
16
|
-
//
|
|
17
|
-
// through the Worker's module graph
|
|
18
|
-
|
|
15
|
+
// `.assetsignore` tells the Workers Static Assets uploader to skip the
|
|
16
|
+
// generated JavaScript files. Every copied Wasm basename is appended later,
|
|
17
|
+
// so it too is reachable only through the Worker's module graph and never
|
|
18
|
+
// served as a public static asset.
|
|
19
|
+
const ASSETS_IGNORE_BASE_ENTRIES = ["_worker.js", "_zfb_inner.mjs"];
|
|
20
|
+
const RESERVED_OUTPUT_NAMES = new Set([...ASSETS_IGNORE_BASE_ENTRIES, ".assetsignore"]);
|
|
21
|
+
|
|
22
|
+
function isPathWithin(parent, candidate) {
|
|
23
|
+
const pathFromParent = relative(parent, candidate);
|
|
24
|
+
return (
|
|
25
|
+
pathFromParent === "" ||
|
|
26
|
+
(!pathFromParent.startsWith(`..${sep}`) &&
|
|
27
|
+
pathFromParent !== ".." &&
|
|
28
|
+
!isAbsolute(pathFromParent))
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function resolveAssets(inputBundlePath, assetPaths) {
|
|
33
|
+
const inputDir = dirname(inputBundlePath);
|
|
34
|
+
const canonicalInputDir = await realpath(inputDir);
|
|
35
|
+
const names = new Set();
|
|
36
|
+
const resolvedAssets = [];
|
|
37
|
+
|
|
38
|
+
for (const assetPath of assetPaths) {
|
|
39
|
+
if (typeof assetPath !== "string" || isAbsolute(assetPath)) {
|
|
40
|
+
throw new Error("asset path must be bundle-relative: " + String(assetPath));
|
|
41
|
+
}
|
|
42
|
+
const sourcePath = resolve(inputDir, assetPath);
|
|
43
|
+
if (!isPathWithin(inputDir, sourcePath)) {
|
|
44
|
+
throw new Error("asset path escapes the input bundle directory: " + assetPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sourceInfo = await stat(sourcePath);
|
|
48
|
+
if (!sourceInfo.isFile()) {
|
|
49
|
+
throw new Error("asset is not a file: " + sourcePath);
|
|
50
|
+
}
|
|
51
|
+
const canonicalSourcePath = await realpath(sourcePath);
|
|
52
|
+
if (!isPathWithin(canonicalInputDir, canonicalSourcePath)) {
|
|
53
|
+
throw new Error("asset path resolves outside the input bundle directory: " + assetPath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const outputName = basename(sourcePath);
|
|
57
|
+
if (!outputName || outputName === "." || outputName === "..") {
|
|
58
|
+
throw new Error("asset path has no valid basename: " + assetPath);
|
|
59
|
+
}
|
|
60
|
+
if (RESERVED_OUTPUT_NAMES.has(outputName)) {
|
|
61
|
+
throw new Error("asset basename collides with generated adapter output: " + outputName);
|
|
62
|
+
}
|
|
63
|
+
if (names.has(outputName)) {
|
|
64
|
+
throw new Error("asset basename collision: " + outputName);
|
|
65
|
+
}
|
|
66
|
+
names.add(outputName);
|
|
67
|
+
resolvedAssets.push({ sourcePath: canonicalSourcePath, outputName });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return resolvedAssets;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function pathExists(path) {
|
|
74
|
+
try {
|
|
75
|
+
await lstat(path);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function readExistingAssetsIgnore(path) {
|
|
86
|
+
try {
|
|
87
|
+
return await readFile(path, "utf8");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mergeAssetsIgnore(existing, requiredEntries) {
|
|
97
|
+
const listed = new Set(existing.split(/\r?\n/));
|
|
98
|
+
const missing = requiredEntries.filter((entry) => !listed.has(entry));
|
|
99
|
+
if (missing.length === 0) {
|
|
100
|
+
return existing;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? existing + "\n" : existing;
|
|
104
|
+
return prefix + missing.map((entry) => entry + "\n").join("");
|
|
105
|
+
}
|
|
19
106
|
|
|
20
107
|
/**
|
|
21
|
-
* Emit a Cloudflare Workers Static Assets
|
|
22
|
-
*
|
|
108
|
+
* Emit a Cloudflare Workers Static Assets `_worker.js` that wraps the zfb
|
|
109
|
+
* input bundle. Cloudflare Pages advanced mode is unverified.
|
|
23
110
|
*
|
|
24
|
-
* Output shape (
|
|
111
|
+
* Output shape (two generated JavaScript files, `.assetsignore`, and zero or
|
|
112
|
+
* more copied Wasm assets in `outdir`):
|
|
25
113
|
*
|
|
26
|
-
* _worker.js — Worker entry point (`main` in wrangler.toml
|
|
27
|
-
* Pages advanced-mode convention)
|
|
114
|
+
* _worker.js — Worker entry point (`main` in wrangler.toml)
|
|
28
115
|
* _zfb_inner.mjs — the input bundle, copied verbatim
|
|
29
|
-
*
|
|
116
|
+
* <asset>.wasm — each bundle-relative Wasm input, copied by basename
|
|
117
|
+
* .assetsignore — excludes every generated JavaScript and Wasm basename
|
|
118
|
+
* from the asset upload
|
|
30
119
|
*
|
|
31
|
-
* @param {{ inputBundlePath: string; outdir: string; workerWrapperSource: string }} input
|
|
120
|
+
* @param {{ inputBundlePath: string; outdir: string; assets?: readonly string[]; workerWrapperSource: string }} input
|
|
32
121
|
* @returns {Promise<{ workerPath: string; innerBundlePath: string; assetsIgnorePath: string }>}
|
|
33
122
|
*/
|
|
34
|
-
export async function emitWorker({ inputBundlePath, outdir, workerWrapperSource }) {
|
|
123
|
+
export async function emitWorker({ inputBundlePath, outdir, assets = [], workerWrapperSource }) {
|
|
35
124
|
const outdirAbs = resolve(outdir);
|
|
36
125
|
const inputAbs = resolve(inputBundlePath);
|
|
126
|
+
const resolvedAssets = await resolveAssets(inputAbs, assets);
|
|
37
127
|
|
|
38
128
|
await mkdir(outdirAbs, { recursive: true });
|
|
129
|
+
for (const asset of resolvedAssets) {
|
|
130
|
+
const destination = join(outdirAbs, asset.outputName);
|
|
131
|
+
if (await pathExists(destination)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"asset basename collision: " + asset.outputName + " would overwrite " + destination,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
39
138
|
const innerBundlePath = join(outdirAbs, "_zfb_inner.mjs");
|
|
40
139
|
await copyFile(inputAbs, innerBundlePath);
|
|
41
140
|
|
|
@@ -43,7 +142,15 @@ export async function emitWorker({ inputBundlePath, outdir, workerWrapperSource
|
|
|
43
142
|
await writeFile(workerPath, workerWrapperSource, "utf8");
|
|
44
143
|
|
|
45
144
|
const assetsIgnorePath = join(outdirAbs, ".assetsignore");
|
|
46
|
-
await
|
|
145
|
+
const existingAssetsIgnore = await readExistingAssetsIgnore(assetsIgnorePath);
|
|
146
|
+
for (const asset of resolvedAssets) {
|
|
147
|
+
await copyFile(asset.sourcePath, join(outdirAbs, asset.outputName));
|
|
148
|
+
}
|
|
149
|
+
const assetsIgnore = mergeAssetsIgnore(existingAssetsIgnore, [
|
|
150
|
+
...ASSETS_IGNORE_BASE_ENTRIES,
|
|
151
|
+
...resolvedAssets.map((asset) => asset.outputName),
|
|
152
|
+
]);
|
|
153
|
+
await writeFile(assetsIgnorePath, assetsIgnore, "utf8");
|
|
47
154
|
|
|
48
155
|
return { workerPath, innerBundlePath, assetsIgnorePath };
|
|
49
156
|
}
|