Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/cloudflare-compile-respect-image-service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/cloudflare': patch
---

Respect custom `image.service` configuration for Cloudflare compile-time image generation
Comment thread
adamchal marked this conversation as resolved.
Outdated
17 changes: 16 additions & 1 deletion packages/integrations/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getParts } from './utils/generate-routes-json.js';
import { buildAssetsHeadersContent } from './utils/headers.js';
import {
type ImageServiceConfig,
hasUserImageService,
normalizeImageServiceConfig,
setImageConfig,
} from './utils/image-config.js';
Expand Down Expand Up @@ -74,6 +75,13 @@ function hasContentCollectionsConfig(srcDir: URL) {
return contentConfigPaths.some((configPath) => existsSync(new URL(`./${configPath}`, srcDir)));
}

function resolveImageServiceEntrypoint(entrypoint: string, root: URL): string {
if (entrypoint.startsWith('.')) {
return new URL(entrypoint, root).href;
}
return entrypoint;
}

export interface Options
extends Pick<
PluginConfig,
Expand Down Expand Up @@ -131,6 +139,7 @@ export default function createIntegration({

let _routes: IntegrationResolvedRoute[];
let cfPluginConfig: PluginConfig;
let hasCustomCompileImageService = false;

const { buildService, runtimeService } = normalizeImageServiceConfig(imageService);
const needsImagesBinding = runtimeService === 'cloudflare-binding';
Expand All @@ -145,6 +154,7 @@ export default function createIntegration({

let session = config.session;
const isCompile = buildService === 'compile';
hasCustomCompileImageService = isCompile && hasUserImageService(config.image);

if (needsImagesBinding) {
logger.info(
Expand Down Expand Up @@ -376,7 +386,9 @@ export default function createIntegration({
typeof config.build.assetsPrefix === 'string'
? config.build.assetsPrefix
: undefined,
imageServiceEntrypoint: '@astrojs/cloudflare/image-service-workerd',
imageServiceEntrypoint: hasCustomCompileImageService
? config.image.service.entrypoint
: '@astrojs/cloudflare/image-service-workerd',
buildAssets: config.build.assets ?? '_astro',
}
: null,
Expand Down Expand Up @@ -468,6 +480,9 @@ export default function createIntegration({
trailingSlash: _config.trailingSlash,
cfPluginConfig,
hasCompileImageService: buildService === 'compile',
userImageServiceEntrypoint: hasCustomCompileImageService
? resolveImageServiceEntrypoint(_config.image.service.entrypoint, _config.root)
: undefined,
}),
);
}
Expand Down
12 changes: 9 additions & 3 deletions packages/integrations/cloudflare/src/prerenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface CloudflarePrerendererOptions {
trailingSlash: AstroConfig['trailingSlash'];
cfPluginConfig: PluginConfig;
hasCompileImageService: boolean;
userImageServiceEntrypoint?: string;
}

/**
Expand All @@ -42,6 +43,7 @@ export function createCloudflarePrerenderer({
trailingSlash,
cfPluginConfig,
hasCompileImageService,
userImageServiceEntrypoint,
}: CloudflarePrerendererOptions): AstroPrerenderer {
let previewServer: VitePreviewServer | undefined;
let serverUrl: string;
Expand Down Expand Up @@ -154,10 +156,14 @@ export function createCloudflarePrerenderer({

const entries: StaticImagesResponse = await response.json();

// Switch from the workerd stub to Sharp for the Node-side generation pipeline
const { default: sharpService } = await import('astro/assets/services/sharp');
globalThis.astroAsset ??= {};
globalThis.astroAsset.imageService = sharpService;
if (userImageServiceEntrypoint) {
const mod = await import(userImageServiceEntrypoint);
globalThis.astroAsset.imageService = mod.default ?? mod;
} else {
const { default: sharpService } = await import('astro/assets/services/sharp');
globalThis.astroAsset.imageService = sharpService;
}

const staticImages: AssetsGlobalStaticImagesList = new Map();
for (const entry of entries) {
Expand Down
24 changes: 16 additions & 8 deletions packages/integrations/cloudflare/src/utils/image-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ const CLOUDFLARE_PASSTHROUGH_ENDPOINT = {
// Used by both `compile` and `cloudflare-binding` for URL generation in workerd.
const WORKERD_IMAGE_SERVICE = { entrypoint: '@astrojs/cloudflare/image-service-workerd' };

const SHARP_IMAGE_SERVICE = 'astro/assets/services/sharp';

export function hasUserImageService(config: AstroConfig['image']): boolean {
return !!config.service?.entrypoint && config.service.entrypoint !== SHARP_IMAGE_SERVICE;
}

export function setImageConfig(
service: ImageServiceConfig | undefined,
config: AstroConfig['image'],
Expand Down Expand Up @@ -89,17 +95,19 @@ export function setImageConfig(
},
};

case 'compile':
case 'compile': {
// Dev: IMAGES binding (via Cloudflare Vite plugin) for real transforms.
// Build: endpoint depends on runtime - `cloudflare-binding` uses IMAGES, `passthrough` uses generic.
const endpoint =
command === 'dev' || runtimeService === 'cloudflare-binding'
? { entrypoint: '@astrojs/cloudflare/image-transform-endpoint' }
: CLOUDFLARE_PASSTHROUGH_ENDPOINT;
return {
...config,
service: WORKERD_IMAGE_SERVICE,
// Dev: IMAGES binding (via Cloudflare Vite plugin) for real transforms.
// Build: endpoint depends on runtime - `cloudflare-binding` uses IMAGES, `passthrough` uses generic.
endpoint:
command === 'dev' || runtimeService === 'cloudflare-binding'
? { entrypoint: '@astrojs/cloudflare/image-transform-endpoint' }
: CLOUDFLARE_PASSTHROUGH_ENDPOINT,
service: hasUserImageService(config) ? config.service : WORKERD_IMAGE_SERVICE,
endpoint,
};
}

case 'custom':
return { ...config };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,34 @@ describe('CompileImageService', () => {
});
});
});

describe('CompileImageService custom service', () => {
let fixture: Fixture;
let html: string;

before(async () => {
fixture = await loadFixture({
root: './fixtures/compile-custom-image-service/',
outDir: './dist/compile-custom-image-service/',
});
await fixture.build();
html = await fixture.readFile('client/index.html');
});

it('uses the custom service for markup', () => {
const $ = cheerio.load(html);
const img = $('img');

assert.equal(img.attr('data-image-service'), 'custom');
assert.match(img.attr('src') ?? '', /^\/_astro\/.+\.webp$/);
});

it('uses the custom service for generated images', async () => {
const $ = cheerio.load(html);
const src = $('img').attr('src');
assert.ok(src);

const data = (await fixture.readFile(`client${src}`, null)) as unknown as Buffer;
assert.equal(Buffer.from(data.subarray(0, 20)).toString('utf8'), 'CUSTOM_TRANSFORM_RAN');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import cloudflare from '@astrojs/cloudflare';
import { defineConfig } from 'astro/config';

// `imageService: 'compile'` with a user-defined `image.service`. The adapter
// should preserve the custom service for getURL/getHTMLAttributes AND invoke its
// transform() during the build-time generation pass (instead of hardcoding sharp).
export default defineConfig({
adapter: cloudflare({
imageService: 'compile',
}),
output: 'static',
image: {
service: {
entrypoint: './src/image-service.ts',
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@test/astro-cloudflare-compile-custom-image-service",
"version": "0.0.0",
"private": true,
"type": "module",
"dependencies": {
"@astrojs/cloudflare": "workspace:*",
"astro": "workspace:*"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { LocalImageService } from 'astro';
import { baseService } from 'astro/assets';

export const TRANSFORM_MARKER = 'CUSTOM_TRANSFORM_RAN';

const service: LocalImageService = {
...baseService,

getHTMLAttributes(options, config) {
const attrs = baseService.getHTMLAttributes?.(options, config) ?? {};
return { ...attrs, 'data-image-service': 'custom' };
},

async transform(inputBuffer, transformOptions, config) {
const marker = new TextEncoder().encode(`${TRANSFORM_MARKER}\n`);
const data = new Uint8Array(marker.length + inputBuffer.length);
data.set(marker, 0);
data.set(inputBuffer, marker.length);
return { data, format: transformOptions.format ?? 'png' };
},
};

export default service;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const prerender = false;

export const GET = () => new Response('ok');
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
import { Image } from 'astro:assets';
import testImage from '../assets/test.jpg';
---

<html lang="en">
<head>
<meta charset="utf-8" />
<title>compile-custom-image-service</title>
</head>
<body>
<Image src={testImage} width={100} alt="test" />
</body>
</html>
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading