From fe5ccd420608f1fab6c85ed7b0458a2eae0cc6a7 Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 12 Jun 2025 17:00:06 +0200 Subject: [PATCH] refactor and move cloudflare image transformation plugin --- app/src/adapter/cloudflare/index.ts | 4 + .../plugins/image-optimization.plugin.ts | 86 +++++++++++++++++++ .../cloudflare/image-optimization-plugin.ts | 83 ------------------ 3 files changed, 90 insertions(+), 83 deletions(-) create mode 100644 app/src/adapter/cloudflare/plugins/image-optimization.plugin.ts delete mode 100644 app/src/plugins/cloudflare/image-optimization-plugin.ts diff --git a/app/src/adapter/cloudflare/index.ts b/app/src/adapter/cloudflare/index.ts index 60e6a77..57daed1 100644 --- a/app/src/adapter/cloudflare/index.ts +++ b/app/src/adapter/cloudflare/index.ts @@ -1,4 +1,5 @@ import { D1Connection, type D1ConnectionConfig } from "./connection/D1Connection"; +import { ImageOptimizationPlugin } from "./plugins/image-optimization.plugin"; export * from "./cloudflare-workers.adapter"; export { makeApp, getFresh } from "./modes/fresh"; @@ -13,6 +14,9 @@ export { type BindingMap, } from "./bindings"; export { constants } from "./config"; +export const plugins = { + imageOptimization: ImageOptimizationPlugin, +}; export function d1(config: D1ConnectionConfig) { return new D1Connection(config); diff --git a/app/src/adapter/cloudflare/plugins/image-optimization.plugin.ts b/app/src/adapter/cloudflare/plugins/image-optimization.plugin.ts new file mode 100644 index 0000000..db9130a --- /dev/null +++ b/app/src/adapter/cloudflare/plugins/image-optimization.plugin.ts @@ -0,0 +1,86 @@ +import type { App, AppPlugin } from "bknd"; + +export type ImageOptimizationPluginOptions = { + accessUrl?: string; + resolvePath?: string; + autoFormat?: boolean; + devBypass?: string; +}; + +export function ImageOptimizationPlugin({ + accessUrl = "/_plugin/image/optimize", + resolvePath = "/api/media/file", + autoFormat = true, + devBypass, +}: ImageOptimizationPluginOptions = {}): AppPlugin { + const disallowedAccessUrls = ["/api", "/admin", "/_optimize"]; + if (disallowedAccessUrls.includes(accessUrl) || accessUrl.length < 2) { + throw new Error(`Disallowed accessUrl: ${accessUrl}`); + } + + return (app: App) => ({ + name: "cf-image-optimization", + onBuilt: () => { + app.server.get(`${accessUrl}/:path{.+$}`, async (c) => { + const request = c.req.raw; + const url = new URL(request.url); + + if (devBypass) { + return c.redirect(devBypass + url.pathname + url.search, 302); + } + + const storage = app.module.media?.storage; + if (!storage) { + throw new Error("No media storage configured"); + } + + const path = c.req.param("path"); + if (!path) { + throw new Error("No url provided"); + } + + const imageURL = `${url.origin}${resolvePath}/${path}`; + const metadata = await storage.objectMetadata(path); + + // Cloudflare-specific options are in the cf object. + const params = Object.fromEntries(url.searchParams.entries()); + const options: RequestInitCfPropertiesImage = {}; + + // Copy parameters from query string to request options. + // You can implement various different parameters here. + if ("fit" in params) options.fit = params.fit as any; + if ("width" in params) options.width = Number.parseInt(params.width); + if ("height" in params) options.height = Number.parseInt(params.height); + if ("quality" in params) options.quality = Number.parseInt(params.quality); + + // Your Worker is responsible for automatic format negotiation. Check the Accept header. + if (autoFormat) { + const accept = request.headers.get("Accept")!; + if (/image\/avif/.test(accept)) { + options.format = "avif"; + } else if (/image\/webp/.test(accept)) { + options.format = "webp"; + } + } + + // Build a request that passes through request headers + const imageRequest = new Request(imageURL, { + headers: request.headers, + }); + + // Returning fetch() with resizing options will pass through response with the resized image. + const res = await fetch(imageRequest, { cf: { image: options } }); + + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: { + "Cache-Control": "public, max-age=600", + "Content-Type": metadata.type, + "Content-Length": metadata.size.toString(), + }, + }); + }); + }, + }); +} diff --git a/app/src/plugins/cloudflare/image-optimization-plugin.ts b/app/src/plugins/cloudflare/image-optimization-plugin.ts deleted file mode 100644 index d611279..0000000 --- a/app/src/plugins/cloudflare/image-optimization-plugin.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { App } from "../../App"; - -export type ImageOptimizationPluginOptions = { - accessUrl?: string; - resolvePath?: string; - autoFormat?: boolean; - devBypass?: string; -}; - -export function ImageOptimizationPlugin({ - accessUrl = "/_plugin/image/optimize", - resolvePath = "/api/media/file", - autoFormat = true, - devBypass, -}: ImageOptimizationPluginOptions = {}) { - const disallowedAccessUrls = ["/api", "/admin", "/_optimize"]; - if (disallowedAccessUrls.includes(accessUrl) || accessUrl.length < 2) { - throw new Error(`Disallowed accessUrl: ${accessUrl}`); - } - - return (app: App) => { - app.module.server.client.get(`${accessUrl}/:path{.+$}`, async (c) => { - const request = c.req.raw; - const url = new URL(request.url); - - if (devBypass) { - return c.redirect(devBypass + url.pathname + url.search, 302); - } - - const storage = app.module.media?.storage; - if (!storage) { - throw new Error("No media storage configured"); - } - - const path = c.req.param("path"); - if (!path) { - throw new Error("No url provided"); - } - - const imageURL = `${url.origin}${resolvePath}/${path}`; - const metadata = await storage.objectMetadata(path); - - // Cloudflare-specific options are in the cf object. - const params = Object.fromEntries(url.searchParams.entries()); - const options: RequestInitCfPropertiesImage = {}; - - // Copy parameters from query string to request options. - // You can implement various different parameters here. - if ("fit" in params) options.fit = params.fit as any; - if ("width" in params) options.width = Number.parseInt(params.width); - if ("height" in params) options.height = Number.parseInt(params.height); - if ("quality" in params) options.quality = Number.parseInt(params.quality); - - // Your Worker is responsible for automatic format negotiation. Check the Accept header. - if (autoFormat) { - const accept = request.headers.get("Accept")!; - if (/image\/avif/.test(accept)) { - options.format = "avif"; - } else if (/image\/webp/.test(accept)) { - options.format = "webp"; - } - } - - // Build a request that passes through request headers - const imageRequest = new Request(imageURL, { - headers: request.headers, - }); - - // Returning fetch() with resizing options will pass through response with the resized image. - const res = await fetch(imageRequest, { cf: { image: options } }); - - return new Response(res.body, { - status: res.status, - statusText: res.statusText, - headers: { - "Cache-Control": "public, max-age=600", - "Content-Type": metadata.type, - "Content-Length": metadata.size.toString(), - }, - }); - }); - }; -}