@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 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], 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`.
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), and
73
- `dist/.assetsignore` (see below) — ready to deploy as a Worker with
74
- Static Assets, or via Cloudflare Pages advanced mode.
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 and the inner SSR bundle from the asset
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
- **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.
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
- // (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
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({ inputBundlePath, outdir, workerWrapperSource: WORKER_WRAPPER_SOURCE });
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
- (also deployable to Cloudflare Pages advanced mode).
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 (Pages-compatible) `_worker.js`
37
- * that wraps the zfb input bundle.
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 (three files in `outdir`):
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, or the
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
- * .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
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 (Pages-compatible) `_worker.js`
24
- * that wraps the zfb input bundle.
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 (three files in `outdir`):
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, or the
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
- * .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
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;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"]}
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"]}
@@ -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 (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";
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 (Pages-compatible) `_worker.js`
22
- * that wraps the zfb input bundle.
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 (three files in `outdir`):
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, or the
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
- * .assetsignore — excludes the two files above from the asset upload
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 writeFile(assetsIgnorePath, ASSETS_IGNORE_CONTENT, "utf8");
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.80",
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).",
@@ -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 (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";
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 (Pages-compatible) `_worker.js`
22
- * that wraps the zfb input bundle.
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 (three files in `outdir`):
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, or the
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
- * .assetsignore — excludes the two files above from the asset upload
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 writeFile(assetsIgnorePath, ASSETS_IGNORE_CONTENT, "utf8");
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
  }