@jxsuite/compiler 0.28.0 → 0.28.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/compiler",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
4
4
  "description": "Jx static HTML compiler, island detector, and site builder",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,9 +42,9 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@apidevtools/json-schema-ref-parser": "^15.3.5",
45
- "@jxsuite/parser": "^0.28.0",
46
- "@jxsuite/runtime": "^0.28.0",
47
- "@jxsuite/schema": "^0.28.0",
45
+ "@jxsuite/parser": "^0.28.2",
46
+ "@jxsuite/runtime": "^0.28.2",
47
+ "@jxsuite/schema": "^0.28.2",
48
48
  "sharp": "^0.34.5"
49
49
  },
50
50
  "devDependencies": {
package/src/compiler.ts CHANGED
@@ -22,11 +22,7 @@ import {
22
22
  DEFAULT_REACTIVITY_SRC,
23
23
  DEFAULT_LIT_HTML_SRC,
24
24
  } from "./shared.ts";
25
- import {
26
- compileServer,
27
- compileSiteServer,
28
- compilePagesFunctions,
29
- } from "./targets/compile-server.ts";
25
+ import { compileServer, compileSiteServer } from "./targets/compile-server.ts";
30
26
 
31
27
  import {
32
28
  compileElement,
@@ -42,7 +38,6 @@ export {
42
38
  isDynamic,
43
39
  compileServer,
44
40
  compileSiteServer,
45
- compilePagesFunctions,
46
41
  compileElement,
47
42
  compileElementPage,
48
43
  compileClient,
@@ -31,11 +31,10 @@ export interface ImageConfig {
31
31
  sizes: string;
32
32
  lazyLoad: boolean;
33
33
  service?: "build" | "cloudflare";
34
- binding?: string;
35
34
  /**
36
35
  * Hostnames whose remote (https) images are routed through the optimization pipeline. Cloudflare
37
- * service only — the generated /_jx/image endpoint fetches and transforms allowlisted remote
38
- * sources; everything else is rejected to prevent open-proxy use.
36
+ * service only — allowlisted remote sources get /cdn-cgi/image transform srcsets (the zone must
37
+ * permit resizing from the remote origin); everything else is left untouched.
39
38
  */
40
39
  remoteDomains?: string[];
41
40
  }
@@ -7,7 +7,6 @@
7
7
  */
8
8
 
9
9
  import { existsSync } from "node:fs";
10
- import { createHash } from "node:crypto";
11
10
  import { resolve, extname, basename } from "node:path";
12
11
  import {
13
12
  processImage,
@@ -49,20 +48,35 @@ async function resolveCfMeta(absoluteSrc: string, metaCache: ImageMetaCache) {
49
48
  }
50
49
 
51
50
  /**
52
- * Build a srcset of `/_jx/image` endpoint URLs for the configured widths that fit within the
53
- * original image width. The `v` param carries the content hash for cache busting.
51
+ * Build a srcset of Cloudflare `/cdn-cgi/image/` transform-via-URL entries for the configured
52
+ * widths that fit within the original image width. `format=auto` lets Cloudflare negotiate
53
+ * AVIF/WebP per browser; `fit=scale-down` guards against upscaling. Requires Image Transformations
54
+ * to be enabled on the serving zone (does not work on *.pages.dev).
54
55
  *
55
- * @param {string} src - Original site-relative src (e.g. "/images/hero.png")
56
- * @param {number[]} widths
56
+ * @param {string} src - Site-relative src (e.g. "/images/hero.png") or allowlisted https URL
57
+ * @param {ImageConfig} config
57
58
  * @param {number} originalWidth
58
- * @param {string} hash - 8-char content hash
59
+ * @param {string | null} hash - 8-char content hash for cache busting (null for remote sources,
60
+ * whose bytes aren't available at build time)
59
61
  * @returns {string}
60
62
  */
61
- function buildCloudflareSrcset(src: string, widths: number[], originalWidth: number, hash: string) {
62
- return widths
63
+ function buildCloudflareSrcset(
64
+ src: string,
65
+ config: ImageConfig,
66
+ originalWidth: number,
67
+ hash: string | null,
68
+ ) {
69
+ const quality = config.quality?.webp ?? 80;
70
+ const separator = src.startsWith("/") ? "" : "/";
71
+ const suffix = hash ? `?v=${hash}` : "";
72
+ return config.widths
63
73
  .filter((w) => w <= originalWidth)
64
74
  .sort((a, b) => a - b)
65
- .map((w) => `/_jx/image?src=${encodeURIComponent(src)}&w=${w}&v=${hash} ${w}w`)
75
+ .map(
76
+ (w) =>
77
+ `/cdn-cgi/image/width=${w},quality=${quality},fit=scale-down,format=auto` +
78
+ `${separator}${src}${suffix} ${w}w`,
79
+ )
66
80
  .join(", ");
67
81
  }
68
82
 
@@ -113,8 +127,9 @@ function shouldSkip(src: string) {
113
127
  /**
114
128
  * Check if a remote src is eligible for cloudflare-service optimization: an https URL whose
115
129
  * hostname is in the project's `images.remoteDomains` allowlist. Content data (e.g. CSV columns)
116
- * routinely references externally hosted images — allowlisted hosts flow through the /_jx/image
117
- * endpoint like local assets; everything else passes through untouched.
130
+ * routinely references externally hosted images — allowlisted hosts flow through the /cdn-cgi/image
131
+ * transform URL like local assets; everything else passes through untouched. The zone must permit
132
+ * resizing from the remote origin (Images → Transformations → Sources).
118
133
  *
119
134
  * @param {string} src
120
135
  * @param {ImageConfig} config
@@ -134,11 +149,6 @@ function isAllowedRemote(src: string, config: ImageConfig) {
134
149
  }
135
150
  }
136
151
 
137
- /** 8-char cache-busting hash for a remote URL (original bytes aren't available at build time). */
138
- function urlHash(src: string) {
139
- return createHash("sha256").update(src).digest("hex").slice(0, 8);
140
- }
141
-
142
152
  /**
143
153
  * Resolve a src path to an absolute filesystem path.
144
154
  *
@@ -258,11 +268,11 @@ async function transformImgNode(
258
268
 
259
269
  if (remote) {
260
270
  // Original dimensions are unknown without fetching — emit every configured width and let
261
- // the endpoint's scale-down fit avoid upscaling past the source size.
262
- srcset = buildCloudflareSrcset(src, config.widths, Infinity, urlHash(src));
271
+ // fit=scale-down avoid upscaling past the source size.
272
+ srcset = buildCloudflareSrcset(src, config, Infinity, null);
263
273
  } else if (config.service === "cloudflare") {
264
274
  const meta = await resolveCfMeta(absoluteSrc as string, metaCache);
265
- srcset = buildCloudflareSrcset(src, config.widths, meta.width, meta.hash);
275
+ srcset = buildCloudflareSrcset(src, config, meta.width, meta.hash);
266
276
  original = meta;
267
277
  } else {
268
278
  let manifest = imageRefs.get(absoluteSrc as string);
@@ -344,10 +354,10 @@ async function transformInnerHtmlImages(
344
354
  let original: { width: number; height: number } | undefined;
345
355
 
346
356
  if (remote) {
347
- srcset = buildCloudflareSrcset(src, config.widths, Infinity, urlHash(src));
357
+ srcset = buildCloudflareSrcset(src, config, Infinity, null);
348
358
  } else if (config.service === "cloudflare") {
349
359
  const meta = await resolveCfMeta(absoluteSrc as string, metaCache);
350
- srcset = buildCloudflareSrcset(src, config.widths, meta.width, meta.hash);
360
+ srcset = buildCloudflareSrcset(src, config, meta.width, meta.hash);
351
361
  original = meta;
352
362
  } else {
353
363
  let manifest = imageRefs.get(absoluteSrc as string);
@@ -25,7 +25,7 @@ import type { FormatRegistry } from "@jxsuite/schema/format-registry";
25
25
  import { resolveLayout } from "./layout-resolver.ts";
26
26
  import { mergeHead, renderHead } from "./head-merger.ts";
27
27
  import { injectContext } from "./context-injection.ts";
28
- import { compile, compileServer, compileSiteServer, compilePagesFunctions } from "../compiler.ts";
28
+ import { compile, compileServer, compileSiteServer } from "../compiler.ts";
29
29
  import { compileElement } from "../targets/compile-element.ts";
30
30
  import {
31
31
  buildInitialScope,
@@ -45,7 +45,6 @@ import { loadContentTypes, loadContentConfig, resolveContentTypeRefs } from "./c
45
45
  import { resolvePrototypes } from "./prototype-resolver.ts";
46
46
  import { transformImageNodes } from "./image-transform.ts";
47
47
  import { loadCache, saveCache, getImageCacheDir } from "./image-cache.ts";
48
- import { compilePagesImageFunction } from "../targets/compile-image-endpoint.ts";
49
48
  import type { ImageConfig } from "./image-optimizer.ts";
50
49
  import type { ImageMetaCache } from "./image-transform.ts";
51
50
  import type {
@@ -205,11 +204,9 @@ export async function buildSite(
205
204
  const imageMetaCache = cfImages ? new Map() : null;
206
205
  if (cfImages) {
207
206
  console.log(
208
- `images.service is "cloudflare" — ensure your wrangler config declares the Images binding ` +
209
- `("images": { "binding": "${projectConfig.images.binding ?? "IMAGES"}" })` +
210
- (projectConfig.build.adapter === "cloudflare-workers"
211
- ? ` and an assets binding ("assets": { "binding": "ASSETS" }).`
212
- : `.`),
207
+ `images.service is "cloudflare" — srcsets use /cdn-cgi/image transform URLs. ` +
208
+ `Ensure Image Transformations are enabled for your zone (Cloudflare dashboard → ` +
209
+ `Images → Transformations); these URLs do not work on *.pages.dev / *.workers.dev previews.`,
213
210
  );
214
211
  }
215
212
 
@@ -321,24 +318,29 @@ export async function buildSite(
321
318
  if (!deduped.has(entry.exportName)) deduped.set(entry.exportName, entry);
322
319
  }
323
320
 
324
- if (adapter === "cloudflare-pages") {
325
- const functions = compilePagesFunctions([...deduped.values()]);
321
+ // Cloudflare Pages uses advanced mode (_worker.js inside the build output) — the
322
+ // functions/ directory convention only works from the project root, not from dist/.
323
+ // A static-only Pages site needs no worker at all.
324
+ const skipWorker = adapter === "cloudflare-pages" && deduped.size === 0;
325
+ const workerSource = skipWorker ? null : compileSiteServer([...deduped.values()], { adapter });
326
326
 
327
- if (cfImages) {
328
- functions.set(
329
- "functions/_jx/image.js",
330
- compilePagesImageFunction(projectConfig.images as ImageConfig),
327
+ if (workerSource) {
328
+ const workerName = adapter === "cloudflare-pages" ? "_worker.js" : "worker.js";
329
+ writeFileSync(resolve(outDir, workerName), workerSource, "utf8");
330
+ fileCount++;
331
+ log(` Generated dist/${workerName} (${deduped.size} server function(s))`);
332
+
333
+ if (adapter === "cloudflare-pages") {
334
+ // Only invoke the worker for server routes; everything else stays static.
335
+ writeFileSync(
336
+ resolve(outDir, "_routes.json"),
337
+ JSON.stringify({ version: 1, include: ["/_jx/*"], exclude: [] }, null, 2) + "\n",
338
+ "utf8",
331
339
  );
332
- }
333
-
334
- for (const [filePath, source] of functions) {
335
- const fullPath = resolve(outDir, filePath);
336
- mkdirSync(resolve(fullPath, ".."), { recursive: true });
337
- writeFileSync(fullPath, source, "utf8");
338
340
  fileCount++;
339
341
  }
340
342
 
341
- // Copy server source files so function imports resolve
343
+ // Copy server source files into dist/components/ so worker imports resolve
342
344
  const distComponentsDir = resolve(outDir, "components");
343
345
  mkdirSync(distComponentsDir, { recursive: true });
344
346
  for (const { src } of deduped.values()) {
@@ -348,35 +350,6 @@ export async function buildSite(
348
350
  copyFileSync(srcFile, destFile);
349
351
  }
350
352
  }
351
-
352
- if (functions.size > 0) {
353
- log(` Generated ${functions.size} Pages function(s) in dist/functions/`);
354
- }
355
- } else {
356
- const workerSource = compileSiteServer([...deduped.values()], {
357
- adapter,
358
- images:
359
- cfImages && adapter === "cloudflare-workers"
360
- ? (projectConfig.images as ImageConfig)
361
- : null,
362
- });
363
-
364
- if (workerSource) {
365
- const workerPath = resolve(outDir, "worker.js");
366
- writeFileSync(workerPath, workerSource, "utf8");
367
- fileCount++;
368
- log(` Generated dist/worker.js (${deduped.size} server function(s))`);
369
-
370
- // Copy server source files into dist/components/ so worker imports resolve
371
- const distComponentsDir = resolve(outDir, "components");
372
- for (const { src } of deduped.values()) {
373
- const srcFile = resolve(projectRoot, src.replace(/^\.\//, ""));
374
- const destFile = resolve(distComponentsDir, src.replace(/^\.\/components\//, ""));
375
- if (existsSync(srcFile)) {
376
- copyFileSync(srcFile, destFile);
377
- }
378
- }
379
- }
380
353
  }
381
354
  }
382
355
 
@@ -35,7 +35,6 @@ const DEFAULTS = {
35
35
  sizes: "(max-width: 768px) 100vw, 50vw",
36
36
  lazyLoad: true,
37
37
  service: "build",
38
- binding: "IMAGES",
39
38
  },
40
39
  build: {
41
40
  outDir: "./dist",
@@ -98,7 +97,8 @@ export function loadProjectConfig(projectRoot: string) {
98
97
  );
99
98
  }
100
99
 
101
- // Validate image service
100
+ // Validate image service. The "cloudflare" service emits /cdn-cgi/image transform URLs,
101
+ // which work on any zone served through Cloudflare regardless of the build adapter.
102
102
  const VALID_IMAGE_SERVICES = ["build", "cloudflare"];
103
103
  if (!VALID_IMAGE_SERVICES.includes(config.images.service)) {
104
104
  throw new Error(
@@ -106,14 +106,6 @@ export function loadProjectConfig(projectRoot: string) {
106
106
  `Valid services: ${VALID_IMAGE_SERVICES.join(", ")}`,
107
107
  );
108
108
  }
109
- const CF_ADAPTERS = ["cloudflare-workers", "cloudflare-pages"];
110
- if (config.images.service === "cloudflare" && !CF_ADAPTERS.includes(config.build.adapter)) {
111
- console.warn(
112
- `images.service "cloudflare" requires a Cloudflare adapter ` +
113
- `(${CF_ADAPTERS.join(", ")}) — falling back to "build"`,
114
- );
115
- config.images.service = "build";
116
- }
117
109
 
118
110
  return {
119
111
  config,
@@ -7,9 +7,7 @@
7
7
 
8
8
  import $RefParser from "@apidevtools/json-schema-ref-parser";
9
9
  import { collectServerEntries } from "../shared.ts";
10
- import { buildImageHandlerSource, buildImageWorkerRoute } from "./compile-image-endpoint.ts";
11
10
  import type { JxElement } from "@jxsuite/schema/types";
12
- import type { ImageConfig } from "../site/image-optimizer.ts";
13
11
 
14
12
  /**
15
13
  * Compile a Jx document to a Hono server handler file. The handler exposes each `timing: "server"`
@@ -63,9 +61,8 @@ export default app
63
61
  * @param {{ exportName: string; src: string }[]} entries - Resolved server entries
64
62
  * @param {object} [opts]
65
63
  * @param {string} [opts.baseUrl] - Base path for server endpoints. Default is `'/_jx/server'`
66
- * @param {string | null} [opts.adapter] - Deployment adapter: `"cloudflare"`, `"node"`, `"bun"`
67
- * @param {ImageConfig | null} [opts.images] - When set, embed the Cloudflare Images binding
68
- * endpoint at `/_jx/image` (images.service: "cloudflare")
64
+ * @param {string | null} [opts.adapter] - Deployment adapter: `"cloudflare-workers"`,
65
+ * `"cloudflare-pages"`, `"node"`, `"bun"`
69
66
  * @returns {string | null} Complete worker/server source string, or null
70
67
  */
71
68
  export function compileSiteServer(
@@ -73,10 +70,9 @@ export function compileSiteServer(
73
70
  opts: {
74
71
  baseUrl?: string;
75
72
  adapter?: string | null;
76
- images?: ImageConfig | null;
77
73
  } = {},
78
74
  ) {
79
- const { baseUrl = "/_jx/server", adapter = null, images = null } = opts;
75
+ const { baseUrl = "/_jx/server", adapter = null } = opts;
80
76
  if (entries.length === 0 && !adapter) return null;
81
77
 
82
78
  const imports = entries
@@ -85,21 +81,19 @@ export function compileSiteServer(
85
81
 
86
82
  const routes = entries.map(({ exportName }) => buildRoute(exportName, baseUrl)).join("\n");
87
83
 
88
- const imageHandler = images ? `\n${buildImageHandlerSource(images)}\n` : "";
89
- // The image route must register before the cloudflare-workers ASSETS catch-all below.
90
- const imageRoute = images ? buildImageWorkerRoute() : "";
91
-
84
+ // Workers static assets and Pages advanced mode (_worker.js) both route every request
85
+ // through the worker — unmatched paths must fall through to the ASSETS binding.
92
86
  const adapterBlock =
93
- adapter === "cloudflare-workers"
87
+ adapter === "cloudflare-workers" || adapter === "cloudflare-pages"
94
88
  ? "\napp.all('*', (c) => c.env.ASSETS.fetch(c.req.raw))\n"
95
89
  : "\n";
96
90
 
97
91
  return `// Generated by @jxsuite/compiler — do not edit manually
98
92
  import { Hono } from 'hono'
99
93
  ${imports}
100
- ${imageHandler}
94
+
101
95
  const app = new Hono()
102
- ${routes}${imageRoute}
96
+ ${routes}
103
97
  ${adapterBlock}
104
98
  export default app
105
99
  `;
@@ -117,47 +111,3 @@ app.post('${baseUrl}/${exportName}', async (c) => {
117
111
  }
118
112
  })`;
119
113
  }
120
-
121
- /**
122
- * Generate Cloudflare Pages Functions from pre-collected server entries. Returns a Map of relative
123
- * file paths (under `functions/`) to source strings.
124
- *
125
- * @param {{ exportName: string; src: string }[]} entries - Resolved server entries
126
- * @param {object} [opts]
127
- * @param {string} [opts.baseUrl] - Base path prefix. Default is `'/_jx/server'`
128
- * @returns {Map<string, string>} Map of output path → source (e.g.
129
- * `"functions/_jx/server/sendEmail.js"` → source)
130
- */
131
- export function compilePagesFunctions(
132
- entries: { exportName: string; src: string }[],
133
- opts: {
134
- baseUrl?: string;
135
- } = {},
136
- ) {
137
- const { baseUrl = "/_jx/server" } = opts;
138
- const files = new Map();
139
-
140
- for (const { exportName, src } of entries) {
141
- const relativeSrc = src.startsWith("./") ? src : `./${src}`;
142
- const funcPath = `functions${baseUrl}/${exportName}.js`;
143
- const depth = funcPath.split("/").length - 1;
144
- const importPath = "../".repeat(depth) + relativeSrc.replace(/^\.\//, "");
145
-
146
- const source = `// Generated by @jxsuite/compiler — do not edit manually
147
- import { ${exportName} } from '${importPath}'
148
-
149
- export async function onRequestPost(context) {
150
- const args = await context.request.json().catch(() => ({}))
151
- try {
152
- const result = await ${exportName}(args, context.env)
153
- return Response.json(result)
154
- } catch (e) {
155
- return Response.json({ ok: false, error: e?.message ?? 'Server error' }, { status: 500 })
156
- }
157
- }
158
- `;
159
- files.set(funcPath, source);
160
- }
161
-
162
- return files;
163
- }
@@ -1,138 +0,0 @@
1
- /**
2
- * Compile-image-endpoint.js — Cloudflare Images binding endpoint codegen
3
- *
4
- * Generates the `/_jx/image` runtime image-transformation handler used when `images.service:
5
- * "cloudflare"` is set in project.json. The handler fetches the original static asset via the
6
- * ASSETS binding and pipes it through the Cloudflare Images binding, negotiating the output format
7
- * from the request's Accept header.
8
- */
9
-
10
- import type { ImageConfig } from "../site/image-optimizer.ts";
11
-
12
- const MIME: Record<string, string> = {
13
- avif: "image/avif",
14
- webp: "image/webp",
15
- jpeg: "image/jpeg",
16
- png: "image/png",
17
- };
18
-
19
- /**
20
- * Generate the shared `jxHandleImage(request, env, waitUntil)` handler source. Widths, format
21
- * preference order (avif first, mirroring the build-time pipeline), per-format quality, and the
22
- * Images binding name are baked in at codegen time.
23
- *
24
- * @param {ImageConfig} images - The project's images config
25
- * @returns {string} JavaScript source defining `jxHandleImage`
26
- */
27
- export function buildImageHandlerSource(images: ImageConfig) {
28
- const binding = images.binding ?? "IMAGES";
29
- const formats = [...(images.formats ?? [])]
30
- .filter((f) => f in MIME)
31
- .sort((a, b) => Number(b === "avif") - Number(a === "avif"));
32
- const tuples = formats.map((f) => [
33
- MIME[f],
34
- images.quality?.[f as keyof ImageConfig["quality"]] ?? 80,
35
- ]);
36
-
37
- return `const JX_IMG_WIDTHS = new Set(${JSON.stringify(images.widths ?? [])})
38
- const JX_IMG_FORMATS = ${JSON.stringify(tuples)} // [mime, quality] in preference order
39
- const JX_IMG_BINDING = ${JSON.stringify(binding)}
40
- const JX_IMG_REMOTE_DOMAINS = new Set(${JSON.stringify(images.remoteDomains ?? [])})
41
-
42
- async function jxHandleImage(request, env, waitUntil) {
43
- const url = new URL(request.url)
44
- const src = url.searchParams.get('src') ?? ''
45
- const w = Number(url.searchParams.get('w') ?? '')
46
-
47
- // Same-origin asset paths, or https sources from allowlisted remote domains —
48
- // never act as an open proxy.
49
- let remoteSrc = null
50
- if (src.startsWith('https://')) {
51
- try {
52
- const remote = new URL(src)
53
- if (JX_IMG_REMOTE_DOMAINS.has(remote.hostname)) remoteSrc = remote
54
- } catch {}
55
- if (!remoteSrc) return new Response('Invalid src', { status: 400 })
56
- } else if (!src.startsWith('/') || src.startsWith('//') || src.includes('..') ||
57
- src.includes(':') || src.includes('\\\\') || src.includes('?') || src.includes('#')) {
58
- return new Response('Invalid src', { status: 400 })
59
- }
60
- if (!JX_IMG_WIDTHS.has(w)) return new Response('Invalid width', { status: 400 })
61
-
62
- const accept = request.headers.get('accept') ?? ''
63
- let target = null
64
- for (const [mime, quality] of JX_IMG_FORMATS) {
65
- if (accept.includes(mime)) { target = { mime, quality }; break }
66
- }
67
-
68
- const v = url.searchParams.get('v') ?? ''
69
- const cacheKey = new Request(url.origin + '/_jx/image?src=' + encodeURIComponent(src) +
70
- '&w=' + w + '&f=' + (target ? target.mime : 'orig') + '&v=' + encodeURIComponent(v))
71
- const cache = caches.default
72
- const hit = await cache.match(cacheKey)
73
- if (hit) {
74
- const out = new Response(hit.body, hit)
75
- out.headers.set('Vary', 'Accept')
76
- return out
77
- }
78
-
79
- const fetchSource = () =>
80
- remoteSrc
81
- ? fetch(remoteSrc.href, { redirect: 'follow' })
82
- : env.ASSETS.fetch(new Request(new URL(src, url.origin)))
83
-
84
- const asset = await fetchSource()
85
- if (!asset.ok || !asset.body) return new Response('Not found', { status: 404 })
86
-
87
- let response
88
- if (target && env[JX_IMG_BINDING]) {
89
- try {
90
- const result = await env[JX_IMG_BINDING]
91
- .input(asset.body)
92
- .transform({ width: w, fit: 'scale-down' })
93
- .output({ format: target.mime, quality: target.quality })
94
- response = result.response()
95
- } catch (e) {
96
- // Transform failed — serve the original source untouched.
97
- const retry = await fetchSource()
98
- response = new Response(retry.body, retry)
99
- }
100
- } else {
101
- response = new Response(asset.body, asset)
102
- }
103
-
104
- response = new Response(response.body, response)
105
- response.headers.set('Cache-Control', 'public, max-age=31536000, immutable')
106
- if (waitUntil) waitUntil(cache.put(cacheKey, response.clone()))
107
- response.headers.set('Vary', 'Accept')
108
- return response
109
- }`;
110
- }
111
-
112
- /**
113
- * Generate the Hono route registering `jxHandleImage` at `/_jx/image`. Must be injected before the
114
- * cloudflare-workers `app.all('*')` ASSETS catch-all.
115
- *
116
- * @returns {string}
117
- */
118
- export function buildImageWorkerRoute() {
119
- return `
120
- app.get('/_jx/image', (c) => jxHandleImage(c.req.raw, c.env, (p) => c.executionCtx?.waitUntil?.(p)))`;
121
- }
122
-
123
- /**
124
- * Generate a complete Cloudflare Pages Function (`functions/_jx/image.js`) wrapping `jxHandleImage`
125
- * in an `onRequestGet` handler.
126
- *
127
- * @param {ImageConfig} images - The project's images config
128
- * @returns {string}
129
- */
130
- export function compilePagesImageFunction(images: ImageConfig) {
131
- return `// Generated by @jxsuite/compiler — do not edit manually
132
- ${buildImageHandlerSource(images)}
133
-
134
- export async function onRequestGet(context) {
135
- return jxHandleImage(context.request, context.env, (p) => context.waitUntil(p))
136
- }
137
- `;
138
- }