Skip to content

Comments

fix: update Cloudflare adapter's default image service#15435

Merged
Princesseuh merged 21 commits intowithastro:mainfrom
rururux:cloudflare-image-component
Feb 24, 2026
Merged

fix: update Cloudflare adapter's default image service#15435
Princesseuh merged 21 commits intowithastro:mainfrom
rururux:cloudflare-image-component

Conversation

@rururux
Copy link
Contributor

@rururux rururux commented Feb 8, 2026

fixes #15319, fixes #15437

From @Princesseuh:

Updates the default option for the Cloudflare adapter to use the bindings by default, and fix the dev experience on all the other settings. In v6 sharp cannot work in dev anymore because it doesn't run in workerd, so everything became passthrough.

Additionally, this PR changes how we do the picomatch excludes to ensure we don't import CJS at runtime in the endpoint. Sucks that picomatch is still in CJS.

Docs: withastro/docs#13267

Original comment here

Changes

Previously, an error occurred when using the Cloudflare adapter in combination with the <Image /> component. This issue is caused by Astro's default behavior: when no image entrypoint is specified in a development environment, it defaults to astro/assets/endpoint/dev.

const endpointEntrypoint =
settings.config.image.endpoint.entrypoint === undefined // If not set, use default endpoint
? mode === 'dev'
? 'astro/assets/endpoint/dev'
: 'astro/assets/endpoint/generic'
: settings.config.image.endpoint.entrypoint;

Since this default dev entrypoint depends on Vite, it attempts to bundle Vite as a dependency during the development server build, leading to the error.
import { type AnymatchFn, isFileLoadingAllowed, type ResolvedConfig } from 'vite';

To resolve this, I have updated the Cloudflare adapter to automatically use @astrojs/cloudflare/image-endpoint as the image entrypoint for development.
There might be a more optimal way to specify this configuration, but I couldn't come up with a better alternative at this time.
I am submitting this PR as-is for initial review.

Testing

I have updated compile-image-service.test.js, which is affected by this change, to include tests for the development server.
While it may be necessary to add tests covering all imageService options, I have left it as is for now to facilitate the review process.

Docs

N/A

@changeset-bot
Copy link

changeset-bot bot commented Feb 8, 2026

🦋 Changeset detected

Latest commit: 233774b

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 Feb 8, 2026
@github-actions github-actions bot added the pkg: astro Related to the core `astro` package (scope) label Feb 12, 2026
@Princesseuh Princesseuh marked this pull request as ready for review February 12, 2026 00:22
@Princesseuh Princesseuh changed the title fix: use @astrojs/cloudflare/image-endpoint for Cloudflare dev server fix: update Cloudflare adapter's default image service Feb 12, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Feb 12, 2026

Merging this PR will not alter performance

✅ 18 untouched benchmarks


Comparing rururux:cloudflare-image-component (233774b) with main (35bc814)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (3252a25) during the generation of this report, so 35bc814 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@OliverSpeir
Copy link
Contributor

OliverSpeir commented Feb 12, 2026

Why not keep compile as the default? doesn't this fix compile in dev as well?

@Princesseuh
Copy link
Member

Princesseuh commented Feb 12, 2026

Why not keep compile as the default? doesn't this fix compile in dev as well?

No, in v6 since the dev runs in workerd sharp never actually works. The flag isn't enough because of the native deps and what not. The current config right now just results in broken images in dev, no matter what you do.

Realized I misread this, yes compile now works in dev by using passthrough, but it's not an amazing default experience to have image optimization be noop so bindings should lead to a better experience

@alexanderniebuhr

This comment was marked as outdated.

@matthewp
Copy link
Contributor

This PR is missing a changeset pnpm changeset

@Princesseuh
Copy link
Member

I'll do the changeset once I actually know what we're doing. What the PR actually does has already changed twice since its creation

@github-actions github-actions bot added pkg: example Related to an example package (scope) docs pr labels Feb 16, 2026
@OliverSpeir OliverSpeir force-pushed the cloudflare-image-component branch from 83a206a to 4ffe2d0 Compare February 16, 2026 11:03
@github-actions github-actions bot removed the pkg: example Related to an example package (scope) label Feb 16, 2026
@OliverSpeir OliverSpeir force-pushed the cloudflare-image-component branch from fa86b90 to a1237c1 Compare February 16, 2026 11:11
@OliverSpeir
Copy link
Contributor

OliverSpeir commented Feb 16, 2026

I don't mean to hijack this, we can move the 66a44d7 out of here if we want but I figured it's easier to keep all in context at least to get some feedback

the idea is this will fix compile and then it opens the quesiton up if we do fix compile and everything works properly which do we want as the default? Netlify adapter I think now defaults to the equivalent of cloudflare-binding which skips transforming images at build time

@OliverSpeir OliverSpeir force-pushed the cloudflare-image-component branch from a1237c1 to d4c29ff Compare February 16, 2026 11:18
@alexanderniebuhr
Copy link
Member

I still think cloudflare-binding is the correct default option.

@OliverSpeir OliverSpeir force-pushed the cloudflare-image-component branch from d4c29ff to 0f97375 Compare February 16, 2026 16:52
@OliverSpeir OliverSpeir force-pushed the cloudflare-image-component branch from 0f97375 to 66a44d7 Compare February 16, 2026 17:24
@OliverSpeir
Copy link
Contributor

OliverSpeir commented Feb 17, 2026

Here's a little ai overview of the commit, because its a bit dense

Commit Breakdown: `66a44d7` — feat: fix/improve compile

1. Refactor generate.ts to decouple from App instance

packages/astro/src/assets/build/generate.ts

prepareAssetsGenerationEnv now takes StaticBuildOptions directly instead of needing an App instance. This
matters because Cloudflare's prerenderer doesn't create a default Astro App — it runs in workerd with its own
createApp(). The generation pipeline just needs config/logger/paths, not the full app.

2. Vendor deterministic-string, move hash utils to runtime-agnostic modules

packages/astro/src/assets/utils/deterministic-string.ts, packages/astro/src/assets/utils/hash.ts

The deterministic-object-hash package pulled in node:crypto, which requires nodejs_compat in workerd. This
commit vendors only the deterministicString function (excluding the async deterministicHash that uses
node:crypto), and moves hashTransform + propsToFilename into a standalone hash.ts with pure-string
replacements for path.posix.basename/dirname/extname — zero node: imports. This means astro/assets can be
imported in workerd without triggering nodejs_compat requirements.

3. Move teardown after asset generation

packages/astro/src/core/build/generate.ts:185-196

The ordering is now: render pages → collectStaticImages()teardown() → generate images with Sharp.
Previously teardown happened before image generation. This is critical because collectStaticImages needs to
fetch the image list from the workerd preview server (which teardown shuts down).

4. Extend AstroPrerenderer with collectStaticImages

packages/astro/src/types/public/integrations.ts:196

New optional method on the prerenderer interface:

collectStaticImages?: () => Promise<AssetsGlobalStaticImagesList>;

This lets adapters contribute images discovered in their runtime back to the Node-side generation pipeline. The
core pipeline merges these into staticImageList before running Sharp transforms.

5. Rename image service, add workerd stub

image-service.tsimage-service-external.ts, new image-service-workerd.ts

The old image-service.ts (Cloudflare URL-based transforms) is renamed to image-service-external.ts for
clarity. The new image-service-workerd.ts extends baseService with a passthrough transform(). It's the
URL generation stub: it handles getURL/getHTMLAttributes/addStaticImage inside workerd without
importing Sharp. The actual pixel work happens later on the Node side via Sharp.

6. Provision IMAGES binding in dev for compile mode

packages/integrations/cloudflare/src/index.ts:97-106

In dev, compile mode needs the IMAGES binding so the image-transform-endpoint can use miniflare's local
image transforms. Without this, dev would need Sharp in workerd, which works but spews confusing compatibility
warnings. At build time, compile uses Sharp on the Node side instead, so the binding isn't needed.

7. Split image service config into build-time and runtime

packages/integrations/cloudflare/src/utils/image-config.ts:19-36

normalizeImageServiceConfig now splits a single imageService option into { buildService, runtimeService }.
This supports the compound config { build: 'compile', runtime?: 'cloudflare-binding' | 'passthrough' }
compile-time Sharp transforms at build, with a separate runtime strategy for SSR routes.

8. Virtual config plugin provides compileImageConfig and isPrerender flags

packages/integrations/cloudflare/src/vite-plugin-config.ts

The virtual:astro-cloudflare:config virtual module now exports compileImageConfig (the
base/assetsPrefix/entrypoint config needed to install addStaticImage) and isPrerender (true when the Vite
environment is 'prerender'). These are consumed by handler.ts to conditionally enable the collection and
endpoint machinery.

9. How the static image collection and endpoint works

This is the core mechanism of the PR. Static images accumulate as a
side effect of rendering pages in workerd, and then Node pulls the collected list out via an internal
endpoint.

Collection (side effect of rendering)

When isPrerender is true and compileImageConfig is set, every incoming request to the worker installs
addStaticImage on globalThis.astroAsset:

handler.ts:41-45 — entry point, conditionally installs the collector:

if (isPrerender) {
    if (compileImageConfig) {
        const { installAddStaticImage } = await import('./static-image-collection.js');
        installAddStaticImage(compileImageConfig);
    }
    // ... route to internal endpoints
}

static-image-collection.ts:11-65installAddStaticImage sets globalThis.astroAsset.addStaticImage,
which mirrors vite-plugin-assets.ts logic but with zero node: imports. When Astro components render
<Image> tags during prerendering, the framework calls addStaticImage(), which hashes the transform and
records { finalPath, transform } into globalThis.astroAsset.staticImages (a Map).

So by the time all pages have been rendered via /__astro_prerender, globalThis.astroAsset.staticImages
contains every image transform discovered during prerendering.

When the Node-side build is done rendering pages, it calls prerenderer.collectStaticImages():

prerenderer.ts:126-158 — sends a POST to /__astro_static_images on the preview server:

const response = await fetch(`${serverUrl}${STATIC_IMAGES_ENDPOINT}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
});

This request hits the preview server (started at prerenderer.ts:60-74 via Vite's preview()), the Cloudflare
Vite plugin routes it into workerd, and handler.ts:52-55 matches it:

if (isStaticImagesRequest(request)) {
    return handleStaticImagesRequest() as unknown as CfResponse;
}

prerender.ts:90-114 (utils) — handleStaticImagesRequest() serializes
globalThis.astroAsset.staticImages into JSON and returns it:

export function handleStaticImagesRequest(): Response {
    const staticImages = globalThis.astroAsset?.staticImages;
    // ... serialize Map entries to JSON array
    return new Response(JSON.stringify(entries), { ... });
}

Back on the Node side

prerenderer.ts:141-157 — after receiving the JSON, the adapter:

  1. Deserializes the entries back into an AssetsGlobalStaticImagesList Map
  2. Swaps the image service to Sharp: globalThis.astroAsset.imageService = sharpService — workerd used the
    passthrough stub, but the Node pipeline needs Sharp for actual transforms

generate.ts:187-192 — the core build merges adapter images into the static image list:

if (prerenderer.collectStaticImages) {
    const adapterImages = await prerenderer.collectStaticImages();
    for (const [path, entry] of adapterImages) {
        staticImageList.set(path, entry);
    }
}

Then teardown happens at :196, and the standard Sharp generation pipeline runs at :203-288.

Internal prerender endpoints

packages/integrations/cloudflare/src/utils/prerender-constants.ts

Three internal endpoints, all only active when isPrerender is true:

  • /__astro_static_paths — returns routes that need prerendering
  • /__astro_prerender — renders a single page
  • /__astro_static_images — drains the collected static image list from workerd

Copy link
Member

@sarah11918 sarah11918 left a comment

Choose a reason for hiding this comment

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

Ugh, apparently I had a pending review comment here

@@ -0,0 +1,5 @@
---
"@astrojs/cloudflare": minor
Copy link
Member

Choose a reason for hiding this comment

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

Just checking that changing a default setting is a potentially a breaking change, and probably a major?

Will note that this requires an entry in the specific Cloudflare upgrade guide, so if you want to follow the pattern for other major upgrades and simply write a one-liner, then link to the docs there, that would be fine!

Copy link
Contributor

@matthewp matthewp left a comment

Choose a reason for hiding this comment

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

Very clever approach to generating the list in the workerd side and then passing back over to Node.js side for sharp processing. Very nicely done!

@florian-lefebvre
Copy link
Member

Does this also fix #15437?

@Princesseuh
Copy link
Member

Does this also fix #15437?

Yes

@ematipico
Copy link
Member

@rururux i'm ready to merge the PR once the conflicts are resolved

@rururux
Copy link
Contributor Author

rururux commented Feb 23, 2026

Sorry, I was a bit slow to react! 😅 Thanks for jumping in and handling this, @Princesseuh!

@sarah11918
Copy link
Member

Will note that something came up while preparing the docs that I think is relevant to ask here! withastro/docs#13267 (comment)

OliverSpeir and others added 2 commits February 23, 2026 14:04
Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com>
Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com>
Copy link
Member

@sarah11918 sarah11918 left a comment

Choose a reason for hiding this comment

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

Approving for docs, and the docs PR is also approved for merging when the feature is released!

Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com>
@Princesseuh Princesseuh merged commit 957b9fe into withastro:main Feb 24, 2026
26 of 27 checks passed
@rururux
Copy link
Contributor Author

rururux commented Feb 24, 2026

When I first opened this pull request, I never imagined it would grow into something this big. Thank you all so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs pr pkg: astro Related to the core `astro` package (scope) pkg: integration Related to any renderer integration (scope)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v6 + Cloudflare + Image component still relies on CJS Astro v6 + CloudFlare adapter + <Image> in dev mode throws error

8 participants