Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/integrations/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"./entrypoints/middleware.js": "./dist/entrypoints/middleware.js",
"./image-service": "./dist/entrypoints/image-service.js",
"./image-endpoint": "./dist/entrypoints/image-endpoint.js",
"./image-transform-endpoint": "./dist/entrypoints/image-transform-endpoint.js",
"./handler": "./dist/utils/handler.js",
"./package.json": "./package.json"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { APIRoute } from 'astro';
import { transform } from '../utils/image-binding-transform.js';

export const prerender = false;

// @ts-expect-error The Header types between libdom and @cloudflare/workers-types are causing issues
export const GET: APIRoute = async (ctx) => {
// @ts-expect-error The runtime locals types are not populated here
return transform(ctx.request.url, ctx.locals.runtime.env.IMAGES, ctx.locals.runtime.env.ASSETS);
};
31 changes: 31 additions & 0 deletions packages/integrations/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,23 @@ export type Options = {
*/
sessionKVBindingName?: string;

/**
* When configured as `cloudflare-binding`, the Cloudflare Images binding will be used to transform images:
* - https://developers.cloudflare.com/images/transform-images/bindings/
*
* By default, this will use the "IMAGES" binding name, but this can be customised in your `wrangler.json`:
*
* ```json
* {
* "images": {
* "binding": "IMAGES" // <-- this should match `imagesBindingName`
* }
* }
* ```
*
*/
imagesBindingName?: string;

/**
* This configuration option allows you to specify a custom entryPoint for your Cloudflare Worker.
* The entry point is the file that will be executed when your Worker is invoked.
Expand Down Expand Up @@ -165,6 +182,17 @@ export default function createIntegration(args?: Options): AstroIntegration {

const isBuild = command === 'build';

if (isBuild && args?.imageService === 'cloudflare-binding') {
const bindingName = args?.imagesBindingName ?? 'IMAGES';

logger.info(
`Enabling image processing with Cloudflare Images for production with the "${bindingName}" Images binding.`,
);
logger.info(
`If you see the error "Invalid binding \`${bindingName}\`" in your build output, you need to add the binding to your wrangler config file.`,
);
}

if (!session?.driver) {
const sessionDir = isBuild ? undefined : createCodegenDir();
const bindingName = args?.sessionKVBindingName ?? 'SESSION';
Expand Down Expand Up @@ -365,6 +393,9 @@ export default function createIntegration(args?: Options): AstroIntegration {
'globalThis.__ASTRO_SESSION_BINDING_NAME': JSON.stringify(
args?.sessionKVBindingName ?? 'SESSION',
),
'globalThis.__ASTRO_IMAGES_BINDING_NAME': JSON.stringify(
args?.imagesBindingName ?? 'IMAGES',
),
...vite.define,
};
}
Expand Down
4 changes: 4 additions & 0 deletions packages/integrations/cloudflare/src/utils/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ declare global {
// eslint-disable-next-line no-var
var __ASTRO_SESSION_BINDING_NAME: string;

// This is not a real global, but is injected using Vite define to allow us to specify the Images binding name in the config.
// eslint-disable-next-line no-var
var __ASTRO_IMAGES_BINDING_NAME: string;

// Just used to pass the KV binding to unstorage.
// eslint-disable-next-line no-var
var __env__: Partial<Env>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @ts-expect-error Not sure how to make this typecheck properly
import { imageConfig } from 'astro:assets';
import { isRemotePath } from '@astrojs/internal-helpers/path';
import { isRemoteAllowed } from '@astrojs/internal-helpers/remote';

import type {
Fetcher,
ImagesBinding,
ImageTransform,
ReadableStream,
} from '@cloudflare/workers-types';

export async function transform(rawUrl: string, images: ImagesBinding, assets: Fetcher) {
const url = new URL(rawUrl);

const href = url.searchParams.get('href');

if (!href || (isRemotePath(href) && !isRemoteAllowed(href, imageConfig))) {
return new Response('Forbidden', { status: 403 });
}

const imageSrc = new URL(href, url.origin);
const content = await (isRemotePath(href) ? fetch(imageSrc) : assets.fetch(imageSrc));
if (!content.body) {
return new Response(null, { status: 404 });
}
const input = images.input(content.body as ReadableStream);

const format = url.searchParams.get('f');

if (!format || !['avif', 'webp', 'jpeg'].includes(format)) {
return new Response(`The "${format}" format is not supported`, { status: 400 });
}

return (
await input
.transform({
width: url.searchParams.has('w') ? parseInt(url.searchParams.get('w')!) : undefined,
height: url.searchParams.has('h') ? parseInt(url.searchParams.get('h')!) : undefined,
// `quality` is documented, but doesn't appear to work in manual testing...
// quality: url.searchParams.get('q'),
fit: url.searchParams.get('fit') as ImageTransform['fit'],
})
.output({ format: `image/${format as 'webp' | 'avif' | 'jpeg'}` })
).response();
}
18 changes: 17 additions & 1 deletion packages/integrations/cloudflare/src/utils/image-config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { AstroConfig, AstroIntegrationLogger, HookParameters } from 'astro';
import { passthroughImageService, sharpImageService } from 'astro/config';

export type ImageService = 'passthrough' | 'cloudflare' | 'compile' | 'custom';
export type ImageService =
| 'passthrough'
| 'cloudflare'
| 'cloudflare-binding'
| 'compile'
| 'custom';

export function setImageConfig(
service: ImageService,
Expand All @@ -21,6 +26,17 @@ export function setImageConfig(
? sharpImageService()
: { entrypoint: '@astrojs/cloudflare/image-service' },
};
case 'cloudflare-binding':
return {
...config,
service: command === 'dev' ? sharpImageService() : undefined,
endpoint:
command === 'dev'
? undefined
: {
entrypoint: '@astrojs/cloudflare/image-transform-endpoint',
},
};

case 'compile':
return {
Expand Down
Loading