Skip to content

fix(cloudflare): preserve user-defined image service across all image… - #16654

Closed
greatjourney589 wants to merge 2 commits into
withastro:mainfrom
greatjourney589:fix/cloudflare-respect-custom-image-service
Closed

fix(cloudflare): preserve user-defined image service across all image…#16654
greatjourney589 wants to merge 2 commits into
withastro:mainfrom
greatjourney589:fix/cloudflare-respect-custom-image-service

Conversation

@greatjourney589

Copy link
Copy Markdown
Contributor

Changes

  • Fixes #16201 — the Cloudflare adapter silently replaced a user-defined image.service with @astrojs/cloudflare/image-service-workerd whenever imageService was unset (default 'cloudflare-binding') or set to 'compile'/'cloudflare-binding'/'cloudflare'/'passthrough'. Custom services (e.g., a third-party CDN service) are now preserved across all modes.
  • Added a guard at the top of setImageConfig that detects a non-default image.service (entrypoint ≠ astro/assets/services/sharp) and returns the config untouched, mirroring the precedent in the existing fallback default: branch and the explicit 'custom' mode.
  • Emits a single logger.info line when this preservation kicks in, telling the user the override was skipped and pointing them at imageService: 'custom' to silence the notice.
  • Added a .changeset (@astrojs/cloudflare patch).

Testing

  • New unit test: packages/integrations/cloudflare/test/image-config.test.ts (10 cases) covering preservation across undefined, 'passthrough', 'cloudflare', 'cloudflare-binding', 'compile', and the compound { build: 'compile' } / { build: 'compile', runtime: 'cloudflare-binding' } configs. Also asserts the existing override of default sharp in 'cloudflare-binding' and 'compile' is preserved (no regression) and that explicit 'custom' continues to work.
  • Existing image-related fixture tests run clean: external-image-service.test.ts (2/2), binding-image-service.test.ts (7/7), compile-image-service.test.ts (5/5).
  • pnpm run build succeeds; tsc -b clean.

Docs

No user-facing docs changes needed. Behavior aligns with what the existing docs already imply (a custom image.service should be respected); this PR brings the Cloudflare adapter into line. Worth a brief note in the Cloudflare adapter README that custom services are preserved alongside imageService modes — happy to follow up in withastro/docs if maintainers prefer.

Closes #16201

@changeset-bot

changeset-bot Bot commented May 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5f6d386

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the pkg: integration Related to any renderer integration (scope) label May 7, 2026
@adamchal

This comment was marked as low quality.

@greatjourney589

Copy link
Copy Markdown
Contributor Author

Thanks for tackling this! I want to flag that this PR fixes one of the two bugs identified in the issue, but I think the second one still needs to be addressed before the issue can be closed.

The issue describes two distinct bugs

From #16201:

  1. In setImageConfig() (image-config.js), the "compile" case unconditionally overrides the image service…
  2. Additionally, in prerenderer.js, collectStaticImages hardcodes astro/assets/services/sharp for build-time image transforms…

The first controls which service handles getURL() / getHTMLAttributes(); the second controls which service performs the actual byte-level transform during SSG.

What this PR fixes

The early-return guard in setImageConfig correctly preserves a user-defined image.service across all imageService modes. After this PR, a custom service's getURL() and getHTMLAttributes() will be called as expected. ✅

What this PR does not fix

packages/integrations/cloudflare/src/prerenderer.ts still contains:

const { default: sharpService } = await import('astro/assets/services/sharp');
globalThis.astroAsset ??= {};
globalThis.astroAsset.imageService = sharpService;

This unconditionally installs sharp as the build-time transform service, regardless of what the user configured in image.service. So even after this PR, a custom service wired up for an external CDN/transform pipeline:

  • ✅ will be used for URL generation and HTML attributes
  • ❌ will not be used for the actual image transforms during build: "compile" — sharp still runs

For users whose custom service exists specifically to route transforms through an external pipeline (e.g., an image CDN's on-the-fly resize endpoint), this means they still need a workaround — currently I'm using a Vite plugin that traps the globalThis.astroAsset.imageService assignment with Object.defineProperty to prevent the sharp override.

Suggestion

Could the same hasCustomService check be applied in collectStaticImages to skip the sharp default when the user has configured a custom service? Something like:

const hasCustomService =
  config.image.service.entrypoint !== 'astro/assets/services/sharp';

if (!hasCustomService) {
  const { default: sharpService } = await import('astro/assets/services/sharp');
  globalThis.astroAsset ??= {};
  globalThis.astroAsset.imageService = sharpService;
}
// else: leave it to the user's service to register itself, or load it via its entrypoint

Happy to test against my reproduction repo if you'd like to extend the PR — otherwise I can open a follow-up. Either way, thanks for moving this forward!

Hi, @adamchal ,
I've just updated, please review again.
Thanks.

@adamchal

adamchal commented May 7, 2026

Copy link
Copy Markdown
Contributor

@greatjourney589 confirmed this works very well. This is really smart sleuthing and great work! Thanks again for taking the time on this.

@ematipico ematipico left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changeset needs to be reworded. I added more comments regarding code and tests

Comment on lines +5 to +7
Preserve user-defined image services when the Cloudflare adapter is used. Previously, the adapter's `imageService` mode (including the default `'cloudflare-binding'` and `'compile'`) would silently overwrite a custom `image.service` configured in `astro.config.*`, replacing it with the workerd image service. Custom services (e.g., third-party CDNs) are now preserved across all modes, matching the behavior of the explicit `'custom'` mode.

Additionally, the workerd prerenderer no longer hard-swaps `globalThis.astroAsset.imageService` to `astro/assets/services/sharp` for byte-level static image generation when a custom service is in use. Previously the `'compile'` mode bypassed the preserved service during the Node-side image generation pass; it now uses the user's configured service throughout the build.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changeset is too lenghtly and too focused on the technical side of the fix. Changesets are meant for end users, not maintainers. Follow this guide https://contribute.docs.astro.build/docs-for-code-changes/changesets/#tips-and-examples

// effect or `'compile'`/`'cloudflare-binding'` is selected.
const hasCustomService = config.service.entrypoint !== 'astro/assets/services/sharp';
if (hasCustomService && buildService !== 'custom') {
logger.info(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already check for custom in the switch, we just need to add hasCustomService check there and fallback to sharp otherwise.

Comment on lines +10 to +22
function createNoopLogger(): AstroIntegrationLogger {
const noop = () => {};
const logger = {
options: {} as never,
label: 'test',
fork: () => logger,
info: noop,
warn: noop,
error: noop,
debug: noop,
};
return logger as unknown as AstroIntegrationLogger;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't needed, you can reduce this stub to

const spyLogger = new SpyLogger();
const logger = spyLogger.forkIntegrationLogger('test-spy');

this is what we use in the mdx tests

Comment on lines +29 to +37
for (const mode of [
undefined, // default → 'cloudflare-binding'
'passthrough',
'cloudflare',
'cloudflare-binding',
'compile',
{ build: 'compile' as const },
{ build: 'compile' as const, runtime: 'cloudflare-binding' as const },
]) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we test custom too? That's essentially where the PR is fixing things

@delucis

delucis commented Jun 12, 2026

Copy link
Copy Markdown
Member

Hey @greatjourney589 are you still interested in working on this or should we close this PR?

@adamchal

Copy link
Copy Markdown
Contributor

@greatjourney589 I’m happy to take this over and see it through if you are strapped. It’s a real sore spot on my projects.

@adamchal

Copy link
Copy Markdown
Contributor

I submitted a separate PR #17099 that directly addresses the issue #16201. This PR should be updated to:

@delucis

delucis commented Jun 17, 2026

Copy link
Copy Markdown
Member

Thanks @adamchal. Ok in that case I'll close this one.

@delucis delucis closed this Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: integration Related to any renderer integration (scope)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: imageService: { build: "compile" } overrides user-configured custom image service

4 participants