@astrojs/cloudflare 13.0.0-beta.8 → 13.0.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,7 +28,7 @@ const service = {
28
28
  return imageEndpoint;
29
29
  }
30
30
  };
31
- var image_service_default = service;
31
+ var image_service_external_default = service;
32
32
  export {
33
- image_service_default as default
33
+ image_service_external_default as default
34
34
  };
@@ -0,0 +1,14 @@
1
+ import type { LocalImageService } from 'astro';
2
+ /**
3
+ * Workerd-compatible image service stub.
4
+ *
5
+ * Handles getURL/getHTMLAttributes/addStaticImage in the workerd prerender
6
+ * environment without importing Sharp. Actual image transforms are handled
7
+ * elsewhere:
8
+ * - `compile`: Sharp runs on the Node side via the generation pipeline
9
+ * - `cloudflare-binding`: image-transform-endpoint uses the IMAGES binding directly
10
+ *
11
+ * transform() is a passthrough so the generic endpoint can call it in dev.
12
+ */
13
+ declare const service: LocalImageService;
14
+ export default service;
@@ -0,0 +1,11 @@
1
+ import { baseService } from "astro/assets";
2
+ const service = {
3
+ ...baseService,
4
+ async transform(inputBuffer, transform) {
5
+ return { data: inputBuffer, format: transform.format };
6
+ }
7
+ };
8
+ var image_service_workerd_default = service;
9
+ export {
10
+ image_service_workerd_default as default
11
+ };
@@ -1,8 +1,27 @@
1
- import { transform } from "../utils/image-binding-transform.js";
2
1
  import { env } from "cloudflare:workers";
2
+ import { transform } from "../utils/image-binding-transform.js";
3
3
  const prerender = false;
4
4
  const GET = async (ctx) => {
5
- return transform(ctx.request.url, env.IMAGES, env.ASSETS);
5
+ const cache = caches.default;
6
+ if (cache) {
7
+ const cached = await cache.match(ctx.request.url);
8
+ if (cached) return cached;
9
+ }
10
+ const response = await transform(ctx.request.url, env.IMAGES, env.ASSETS);
11
+ if (!response.ok) return response;
12
+ const headers = new Headers(response.headers);
13
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
14
+ const cachedResponse = new Response(response.body, {
15
+ status: response.status,
16
+ headers
17
+ });
18
+ if (cache) {
19
+ const cfContext = ctx.locals.cfContext;
20
+ if (cfContext) {
21
+ cfContext.waitUntil(cache.put(ctx.request.url, cachedResponse.clone()));
22
+ }
23
+ }
24
+ return cachedResponse;
6
25
  };
7
26
  export {
8
27
  GET,
@@ -5,7 +5,6 @@ import { fileURLToPath } from "node:url";
5
5
  import { cloudflare as cfVitePlugin } from "@cloudflare/vite-plugin";
6
6
  import colors from "piccolore";
7
7
  import { performance } from "node:perf_hooks";
8
- import { cloudflareConfigCustomizer } from "../wrangler.js";
9
8
  const createPreviewServer = async ({
10
9
  logger,
11
10
  base,
@@ -17,10 +16,6 @@ const createPreviewServer = async ({
17
16
  }) => {
18
17
  const startServerTime = performance.now();
19
18
  let previewServer;
20
- const cfPluginConfig = {
21
- viteEnvironment: { name: "ssr" },
22
- config: cloudflareConfigCustomizer()
23
- };
24
19
  try {
25
20
  previewServer = await preview({
26
21
  configFile: false,
@@ -37,7 +32,9 @@ const createPreviewServer = async ({
37
32
  open: false,
38
33
  allowedHosts: []
39
34
  },
40
- plugins: [cfVitePlugin(cfPluginConfig)]
35
+ plugins: [
36
+ cfVitePlugin({ ...globalThis.astroCloudflareOptions, viteEnvironment: { name: "ssr" } })
37
+ ]
41
38
  });
42
39
  } catch (err) {
43
40
  if (err instanceof Error) {
@@ -83,7 +80,7 @@ function serverStart({
83
80
  host,
84
81
  base
85
82
  }) {
86
- const version = "13.0.0-beta.8";
83
+ const version = "13.0.0-beta.9";
87
84
  const localPrefix = `${colors.dim("\u2503")} Local `;
88
85
  const networkPrefix = `${colors.dim("\u2503")} Network `;
89
86
  const emptyPrefix = " ".repeat(11);
@@ -9,8 +9,9 @@ function astroFrontmatterScanPlugin() {
9
9
  const code = await readFile(args.path, "utf-8");
10
10
  const frontmatterMatch = FRONTMATTER_RE.exec(code);
11
11
  if (frontmatterMatch) {
12
+ const contents = frontmatterMatch[1].replace(/\breturn\b/g, "throw ");
12
13
  return {
13
- contents: frontmatterMatch[1],
14
+ contents,
14
15
  loader: "ts"
15
16
  };
16
17
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
+ import { type PluginConfig } from '@cloudflare/vite-plugin';
1
2
  import type { AstroIntegration } from 'astro';
2
- import { type ImageService } from './utils/image-config.js';
3
+ import { type ImageServiceConfig } from './utils/image-config.js';
3
4
  export type { Runtime } from './utils/handler.js';
4
- export type Options = {
5
+ export interface Options extends Pick<PluginConfig, 'auxiliaryWorkers' | 'configPath' | 'inspectorPort' | 'persistState' | 'remoteBindings'> {
5
6
  /** Options for handling images. */
6
- imageService?: ImageService;
7
+ imageService?: ImageServiceConfig;
7
8
  /**
8
9
  * By default, Astro will be configured to use Cloudflare KV to store session data. The KV namespace
9
10
  * will be automatically provisioned when you deploy.
@@ -24,5 +25,6 @@ export type Options = {
24
25
  * See https://developers.cloudflare.com/images/transform-images/bindings/ for more details.
25
26
  */
26
27
  imagesBindingName?: string;
27
- };
28
- export default function createIntegration(args?: Options): AstroIntegration;
28
+ experimental?: Pick<NonNullable<PluginConfig['experimental']>, 'headersAndRedirectsDevModeSupport'>;
29
+ }
30
+ export default function createIntegration({ imageService, sessionKVBindingName, imagesBindingName, ...cloudflareOptions }?: Options): AstroIntegration;
package/dist/index.js CHANGED
@@ -6,7 +6,10 @@ import { createRedirectsFromAstroRoutes, printAsRedirects } from "@astrojs/under
6
6
  import { cloudflare as cfVitePlugin } from "@cloudflare/vite-plugin";
7
7
  import { astroFrontmatterScanPlugin } from "./esbuild-plugin-astro-frontmatter.js";
8
8
  import { getParts } from "./utils/generate-routes-json.js";
9
- import { setImageConfig } from "./utils/image-config.js";
9
+ import {
10
+ normalizeImageServiceConfig,
11
+ setImageConfig
12
+ } from "./utils/image-config.js";
10
13
  import { createConfigPlugin } from "./vite-plugin-config.js";
11
14
  import {
12
15
  cloudflareConfigCustomizer,
@@ -16,21 +19,36 @@ import {
16
19
  import { parseEnv } from "node:util";
17
20
  import { sessionDrivers } from "astro/config";
18
21
  import { createCloudflarePrerenderer } from "./prerenderer.js";
19
- function createIntegration(args) {
22
+ import { createRequire } from "node:module";
23
+ function createIntegration({
24
+ imageService,
25
+ sessionKVBindingName = DEFAULT_SESSION_KV_BINDING_NAME,
26
+ imagesBindingName = DEFAULT_IMAGES_BINDING_NAME,
27
+ ...cloudflareOptions
28
+ } = {}) {
20
29
  let _config;
21
30
  let _routes;
22
31
  let _isFullyStatic = false;
23
- const sessionKVBindingName = args?.sessionKVBindingName ?? DEFAULT_SESSION_KV_BINDING_NAME;
24
- const imagesBindingName = args?.imagesBindingName ?? DEFAULT_IMAGES_BINDING_NAME;
32
+ let cfPluginConfig;
33
+ const { buildService, runtimeService } = normalizeImageServiceConfig(imageService);
34
+ const needsImagesBinding = runtimeService === "cloudflare-binding";
25
35
  return {
26
36
  name: "@astrojs/cloudflare",
27
37
  hooks: {
28
38
  "astro:config:setup": ({ command, config, updateConfig, logger, addWatchFile }) => {
39
+ if (!!process.versions.webcontainer) {
40
+ throw new Error("`workerd` does not run on Stackblitz.");
41
+ }
29
42
  let session = config.session;
30
- if (args?.imageService === "cloudflare-binding") {
43
+ const isCompile = buildService === "compile";
44
+ if (needsImagesBinding) {
31
45
  logger.info(
32
46
  `Enabling image processing with Cloudflare Images for production with the "${imagesBindingName}" Images binding.`
33
47
  );
48
+ } else if (isCompile) {
49
+ logger.info(
50
+ `Enabling compile-time image optimization. Images will be pre-optimized at build time.`
51
+ );
34
52
  }
35
53
  if (!session?.driver) {
36
54
  logger.info(
@@ -44,24 +62,29 @@ function createIntegration(args) {
44
62
  ttl: session?.ttl
45
63
  };
46
64
  }
47
- const cfPluginConfig = {
48
- viteEnvironment: { name: "ssr" },
65
+ const needsImagesBindingForDev = isCompile && command === "dev";
66
+ cfPluginConfig = {
49
67
  config: cloudflareConfigCustomizer({
50
- sessionKVBindingName: args?.sessionKVBindingName,
51
- imagesBindingName: args?.imageService === "cloudflare-binding" ? args?.imagesBindingName : false
68
+ sessionKVBindingName,
69
+ imagesBindingName: needsImagesBinding || needsImagesBindingForDev ? imagesBindingName : false
52
70
  }),
53
71
  experimental: {
54
72
  prerenderWorker: {
55
73
  config(_, { entryWorkerConfig }) {
56
74
  return {
57
75
  ...entryWorkerConfig,
58
- // This is the Vite environment name used for prerendering
59
- name: "prerender"
76
+ name: "prerender",
77
+ ...needsImagesBinding && !entryWorkerConfig.images && {
78
+ images: { binding: imagesBindingName }
79
+ }
60
80
  };
61
81
  }
62
82
  }
63
83
  }
64
84
  };
85
+ if (command === "preview") {
86
+ globalThis.astroCloudflareOptions = cfPluginConfig;
87
+ }
65
88
  updateConfig({
66
89
  build: {
67
90
  redirects: false
@@ -69,7 +92,7 @@ function createIntegration(args) {
69
92
  session,
70
93
  vite: {
71
94
  plugins: [
72
- cfVitePlugin(cfPluginConfig),
95
+ cfVitePlugin({ ...cfPluginConfig, viteEnvironment: { name: "ssr" } }),
73
96
  {
74
97
  name: "@astrojs/cloudflare:cf-imports",
75
98
  enforce: "pre",
@@ -108,6 +131,7 @@ function createIntegration(args) {
108
131
  "astro > neotraverse/modern",
109
132
  "astro > piccolore",
110
133
  "astro/app",
134
+ "astro/assets",
111
135
  "astro/compiler-runtime"
112
136
  ],
113
137
  exclude: [
@@ -148,12 +172,21 @@ function createIntegration(args) {
148
172
  }
149
173
  },
150
174
  createConfigPlugin({
151
- sessionKVBindingName
175
+ sessionKVBindingName,
176
+ compileImageConfig: isCompile && command !== "dev" ? {
177
+ base: config.base,
178
+ assetsPrefix: typeof config.build.assetsPrefix === "string" ? config.build.assetsPrefix : void 0,
179
+ imageServiceEntrypoint: "@astrojs/cloudflare/image-service-workerd",
180
+ buildAssets: config.build.assets ?? "_astro"
181
+ } : null
152
182
  })
153
183
  ]
154
184
  },
155
- image: setImageConfig(args?.imageService ?? "compile", config.image, command, logger)
185
+ image: setImageConfig(imageService, config.image, command, logger)
156
186
  });
187
+ if (cloudflareOptions.configPath) {
188
+ addWatchFile(createRequire(import.meta.url).resolve(cloudflareOptions.configPath));
189
+ }
157
190
  addWatchFile(new URL("./wrangler.toml", config.root));
158
191
  addWatchFile(new URL("./wrangler.json", config.root));
159
192
  addWatchFile(new URL("./wrangler.jsonc", config.root));
@@ -186,7 +219,7 @@ function createIntegration(args) {
186
219
  support: "limited",
187
220
  message: "When using a custom image service, ensure it is compatible with the Cloudflare Workers runtime.",
188
221
  // Only 'custom' could potentially use sharp at runtime.
189
- suppress: args?.imageService === "custom" ? "default" : "all"
222
+ suppress: buildService === "custom" ? "default" : "all"
190
223
  },
191
224
  envGetSecret: "stable"
192
225
  }
@@ -211,7 +244,9 @@ function createIntegration(args) {
211
244
  serverDir: _config.build.server,
212
245
  clientDir: _config.build.client,
213
246
  base: _config.base,
214
- trailingSlash: _config.trailingSlash
247
+ trailingSlash: _config.trailingSlash,
248
+ cfPluginConfig,
249
+ hasCompileImageService: buildService === "compile"
215
250
  })
216
251
  );
217
252
  },
@@ -228,9 +263,7 @@ function createIntegration(args) {
228
263
  vite.build.rollupOptions.output.banner ||= "globalThis.process ??= {}; globalThis.process.env ??= {};";
229
264
  vite.define = {
230
265
  "process.env": "process.env",
231
- "globalThis.__ASTRO_IMAGES_BINDING_NAME": JSON.stringify(
232
- args?.imagesBindingName ?? "IMAGES"
233
- ),
266
+ "globalThis.__ASTRO_IMAGES_BINDING_NAME": JSON.stringify(imagesBindingName),
234
267
  ...vite.define
235
268
  };
236
269
  }
@@ -19,4 +19,14 @@ export interface PrerenderRequest {
19
19
  url: string;
20
20
  routeData: SerializedRouteData;
21
21
  }
22
+ export interface SerializedStaticImageEntry {
23
+ originalPath: string;
24
+ originalSrcPath: string | undefined;
25
+ transforms: Array<{
26
+ hash: string;
27
+ finalPath: string;
28
+ transform: Record<string, any>;
29
+ }>;
30
+ }
31
+ export type StaticImagesResponse = SerializedStaticImageEntry[];
22
32
  export {};
@@ -1,14 +1,17 @@
1
1
  import type { AstroConfig, AstroPrerenderer } from 'astro';
2
+ import { type PluginConfig } from '@cloudflare/vite-plugin';
2
3
  interface CloudflarePrerendererOptions {
3
4
  root: AstroConfig['root'];
4
5
  serverDir: AstroConfig['build']['server'];
5
6
  clientDir: AstroConfig['build']['client'];
6
7
  base: AstroConfig['base'];
7
8
  trailingSlash: AstroConfig['trailingSlash'];
9
+ cfPluginConfig: PluginConfig;
10
+ hasCompileImageService: boolean;
8
11
  }
9
12
  /**
10
13
  * Creates a prerenderer that uses Cloudflare's workerd runtime via a preview server.
11
14
  * This allows prerendering to happen in the same runtime that will serve the pages.
12
15
  */
13
- export declare function createCloudflarePrerenderer({ root, serverDir, clientDir, base, trailingSlash, }: CloudflarePrerendererOptions): AstroPrerenderer;
16
+ export declare function createCloudflarePrerenderer({ root, serverDir, clientDir, base, trailingSlash, cfPluginConfig, hasCompileImageService, }: CloudflarePrerendererOptions): AstroPrerenderer;
14
17
  export {};
@@ -2,15 +2,20 @@ import { preview } from "vite";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { mkdir } from "node:fs/promises";
4
4
  import { cloudflare as cfVitePlugin } from "@cloudflare/vite-plugin";
5
- import { cloudflareConfigCustomizer } from "./wrangler.js";
6
5
  import { serializeRouteData, deserializeRouteData } from "astro/app/manifest";
7
- import { STATIC_PATHS_ENDPOINT, PRERENDER_ENDPOINT } from "./utils/prerender-constants.js";
6
+ import {
7
+ STATIC_PATHS_ENDPOINT,
8
+ PRERENDER_ENDPOINT,
9
+ STATIC_IMAGES_ENDPOINT
10
+ } from "./utils/prerender-constants.js";
8
11
  function createCloudflarePrerenderer({
9
12
  root,
10
13
  serverDir,
11
14
  clientDir,
12
15
  base,
13
- trailingSlash
16
+ trailingSlash,
17
+ cfPluginConfig,
18
+ hasCompileImageService
14
19
  }) {
15
20
  let previewServer;
16
21
  let serverUrl;
@@ -18,10 +23,6 @@ function createCloudflarePrerenderer({
18
23
  name: "@astrojs/cloudflare:prerenderer",
19
24
  async setup() {
20
25
  await mkdir(clientDir, { recursive: true });
21
- const cfPluginConfig = {
22
- viteEnvironment: { name: "prerender" },
23
- config: cloudflareConfigCustomizer()
24
- };
25
26
  previewServer = await preview({
26
27
  configFile: false,
27
28
  base,
@@ -36,7 +37,7 @@ function createCloudflarePrerenderer({
36
37
  // Let the OS pick a free port
37
38
  open: false
38
39
  },
39
- plugins: [cfVitePlugin(cfPluginConfig)]
40
+ plugins: [cfVitePlugin({ ...cfPluginConfig, viteEnvironment: { name: "prerender" } })]
40
41
  });
41
42
  const address = previewServer.httpServer.address();
42
43
  if (address && typeof address === "object") {
@@ -75,6 +76,33 @@ function createCloudflarePrerenderer({
75
76
  });
76
77
  return response;
77
78
  },
79
+ collectStaticImages: hasCompileImageService ? async () => {
80
+ const response = await fetch(`${serverUrl}${STATIC_IMAGES_ENDPOINT}`, {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/json" }
83
+ });
84
+ if (!response.ok) {
85
+ throw new Error(
86
+ `Failed to get static images from the Cloudflare prerender server (${response.status}: ${response.statusText}).`
87
+ );
88
+ }
89
+ const entries = await response.json();
90
+ const { default: sharpService } = await import("astro/assets/services/sharp");
91
+ globalThis.astroAsset ??= {};
92
+ globalThis.astroAsset.imageService = sharpService;
93
+ const staticImages = /* @__PURE__ */ new Map();
94
+ for (const entry of entries) {
95
+ const transforms = /* @__PURE__ */ new Map();
96
+ for (const t of entry.transforms) {
97
+ transforms.set(t.hash, { finalPath: t.finalPath, transform: t.transform });
98
+ }
99
+ staticImages.set(entry.originalPath, {
100
+ originalSrcPath: entry.originalSrcPath,
101
+ transforms
102
+ });
103
+ }
104
+ return staticImages;
105
+ } : void 0,
78
106
  async teardown() {
79
107
  if (previewServer) {
80
108
  await previewServer.close();
@@ -1,5 +1,9 @@
1
1
  import { env as globalEnv } from "cloudflare:workers";
2
- import { sessionKVBindingName, isPrerender } from "virtual:astro-cloudflare:config";
2
+ import {
3
+ sessionKVBindingName,
4
+ compileImageConfig,
5
+ isPrerender
6
+ } from "virtual:astro-cloudflare:config";
3
7
  import { createApp } from "astro/app/entrypoint";
4
8
  import { setGetEnv } from "astro/env/setup";
5
9
  import { createGetEnv } from "../utils/env.js";
@@ -7,18 +11,27 @@ import {
7
11
  isStaticPathsRequest,
8
12
  isPrerenderRequest,
9
13
  handleStaticPathsRequest,
10
- handlePrerenderRequest
14
+ handlePrerenderRequest,
15
+ isStaticImagesRequest,
16
+ handleStaticImagesRequest
11
17
  } from "./prerender.js";
12
18
  setGetEnv(createGetEnv(globalEnv));
13
19
  const app = createApp();
14
20
  async function handle(request, env, context) {
15
21
  if (isPrerender) {
22
+ if (compileImageConfig) {
23
+ const { installAddStaticImage } = await import("./static-image-collection.js");
24
+ installAddStaticImage(compileImageConfig);
25
+ }
16
26
  if (isStaticPathsRequest(request)) {
17
27
  return handleStaticPathsRequest(app);
18
28
  }
19
29
  if (isPrerenderRequest(request)) {
20
30
  return handlePrerenderRequest(app, request);
21
31
  }
32
+ if (isStaticImagesRequest(request)) {
33
+ return handleStaticImagesRequest();
34
+ }
22
35
  }
23
36
  const { pathname: requestPathname } = new URL(request.url);
24
37
  if (env[sessionKVBindingName]) {
@@ -13,17 +13,25 @@ async function transform(rawUrl, images, assets) {
13
13
  return new Response(null, { status: 404 });
14
14
  }
15
15
  const input = images.input(content.body);
16
- const format = url.searchParams.get("f");
17
- if (!format || !["avif", "webp", "jpeg"].includes(format)) {
18
- return new Response(`The "${format}" format is not supported`, { status: 400 });
16
+ const supportedFormats = {
17
+ jpeg: "image/jpeg",
18
+ jpg: "image/jpeg",
19
+ png: "image/png",
20
+ gif: "image/gif",
21
+ webp: "image/webp",
22
+ avif: "image/avif"
23
+ };
24
+ const outputFormat = supportedFormats[url.searchParams.get("f") ?? ""];
25
+ if (!outputFormat) {
26
+ return new Response(`Unsupported format: ${url.searchParams.get("f")}`, { status: 400 });
19
27
  }
20
28
  return (await input.transform({
21
- width: url.searchParams.has("w") ? parseInt(url.searchParams.get("w")) : void 0,
22
- height: url.searchParams.has("h") ? parseInt(url.searchParams.get("h")) : void 0,
29
+ width: url.searchParams.has("w") ? Number.parseInt(url.searchParams.get("w")) : void 0,
30
+ height: url.searchParams.has("h") ? Number.parseInt(url.searchParams.get("h")) : void 0,
23
31
  // `quality` is documented, but doesn't appear to work in manual testing...
24
32
  // quality: url.searchParams.get('q'),
25
33
  fit: url.searchParams.get("fit")
26
- }).output({ format: `image/${format}` })).response();
34
+ }).output({ format: outputFormat })).response();
27
35
  }
28
36
  export {
29
37
  transform
@@ -1,8 +1,19 @@
1
1
  import type { AstroConfig, AstroIntegrationLogger, HookParameters } from 'astro';
2
- export type ImageService = 'passthrough' | 'cloudflare' | 'cloudflare-binding' | 'compile' | 'custom';
3
- export declare function setImageConfig(service: ImageService, config: AstroConfig['image'], command: HookParameters<'astro:config:setup'>['command'], logger: AstroIntegrationLogger): {
2
+ export type ImageServiceMode = 'passthrough' | 'cloudflare' | 'cloudflare-binding' | 'compile' | 'custom';
3
+ export type ImageServiceConfig = ImageServiceMode | {
4
+ build: 'compile';
5
+ runtime?: 'passthrough' | 'cloudflare-binding';
6
+ };
7
+ /** Normalize string | compound config into separate build/runtime modes. */
8
+ export declare function normalizeImageServiceConfig(config: ImageServiceConfig | undefined): {
9
+ buildService: ImageServiceMode;
10
+ runtimeService: ImageServiceMode;
11
+ };
12
+ export declare function setImageConfig(service: ImageServiceConfig | undefined, config: AstroConfig['image'], command: HookParameters<'astro:config:setup'>['command'], logger: AstroIntegrationLogger): {
4
13
  service: import("astro").ImageServiceConfig<Record<string, any>>;
5
14
  endpoint: {
15
+ entrypoint: string;
16
+ } | {
6
17
  route: string;
7
18
  entrypoint?: string | undefined;
8
19
  };
@@ -19,9 +30,11 @@ export declare function setImageConfig(service: ImageService, config: AstroConfi
19
30
  objectPosition?: string | undefined;
20
31
  breakpoints?: number[] | undefined;
21
32
  } | {
22
- service: import("astro").ImageServiceConfig<Record<string, any>>;
33
+ service: {
34
+ entrypoint: string;
35
+ };
23
36
  endpoint: {
24
- entrypoint: string | undefined;
37
+ entrypoint: string;
25
38
  };
26
39
  domains: string[];
27
40
  remotePatterns: {
@@ -1,16 +1,37 @@
1
- import { passthroughImageService, sharpImageService } from "astro/config";
1
+ import { passthroughImageService } from "astro/config";
2
+ function normalizeImageServiceConfig(config) {
3
+ if (!config || typeof config === "string") {
4
+ const mode = config ?? "cloudflare-binding";
5
+ return {
6
+ buildService: mode,
7
+ runtimeService: mode === "compile" ? "passthrough" : mode
8
+ };
9
+ }
10
+ return {
11
+ buildService: "compile",
12
+ runtimeService: config.runtime ?? "passthrough"
13
+ };
14
+ }
15
+ const GENERIC_ENDPOINT = { entrypoint: "astro/assets/endpoint/generic" };
16
+ const WORKERD_IMAGE_SERVICE = { entrypoint: "@astrojs/cloudflare/image-service-workerd" };
2
17
  function setImageConfig(service, config, command, logger) {
3
- switch (service) {
18
+ const { buildService, runtimeService } = normalizeImageServiceConfig(service);
19
+ switch (buildService) {
4
20
  case "passthrough":
5
- return { ...config, service: passthroughImageService() };
21
+ return {
22
+ ...config,
23
+ service: passthroughImageService(),
24
+ endpoint: command === "dev" ? GENERIC_ENDPOINT : config.endpoint
25
+ };
6
26
  case "cloudflare":
7
27
  return {
8
28
  ...config,
9
- service: command === "dev" ? sharpImageService() : { entrypoint: "@astrojs/cloudflare/image-service" }
29
+ service: { entrypoint: "@astrojs/cloudflare/image-service" }
10
30
  };
11
31
  case "cloudflare-binding":
12
32
  return {
13
33
  ...config,
34
+ service: WORKERD_IMAGE_SERVICE,
14
35
  endpoint: {
15
36
  entrypoint: "@astrojs/cloudflare/image-transform-endpoint"
16
37
  }
@@ -18,10 +39,10 @@ function setImageConfig(service, config, command, logger) {
18
39
  case "compile":
19
40
  return {
20
41
  ...config,
21
- service: sharpImageService(),
22
- endpoint: {
23
- entrypoint: command === "dev" ? void 0 : "@astrojs/cloudflare/image-endpoint"
24
- }
42
+ service: WORKERD_IMAGE_SERVICE,
43
+ // Dev: IMAGES binding (via Cloudflare Vite plugin) for real transforms.
44
+ // Build: endpoint depends on runtime - `cloudflare-binding` uses IMAGES, `passthrough` uses generic.
45
+ endpoint: command === "dev" || runtimeService === "cloudflare-binding" ? { entrypoint: "@astrojs/cloudflare/image-transform-endpoint" } : GENERIC_ENDPOINT
25
46
  };
26
47
  case "custom":
27
48
  return { ...config };
@@ -36,5 +57,6 @@ function setImageConfig(service, config, command, logger) {
36
57
  }
37
58
  }
38
59
  export {
60
+ normalizeImageServiceConfig,
39
61
  setImageConfig
40
62
  };
@@ -5,3 +5,5 @@
5
5
  export declare const STATIC_PATHS_ENDPOINT = "/__astro_static_paths";
6
6
  /** Internal endpoint for rendering a specific page during prerendering */
7
7
  export declare const PRERENDER_ENDPOINT = "/__astro_prerender";
8
+ /** Internal endpoint for fetching static images collected in workerd during `compile` builds */
9
+ export declare const STATIC_IMAGES_ENDPOINT = "/__astro_static_images";
@@ -1,6 +1,8 @@
1
1
  const STATIC_PATHS_ENDPOINT = "/__astro_static_paths";
2
2
  const PRERENDER_ENDPOINT = "/__astro_prerender";
3
+ const STATIC_IMAGES_ENDPOINT = "/__astro_static_images";
3
4
  export {
4
5
  PRERENDER_ENDPOINT,
6
+ STATIC_IMAGES_ENDPOINT,
5
7
  STATIC_PATHS_ENDPOINT
6
8
  };
@@ -32,3 +32,6 @@ export declare function handleStaticPathsRequest(app: BaseApp): Promise<Response
32
32
  * Handles a prerender request, rendering the specified page.
33
33
  */
34
34
  export declare function handlePrerenderRequest(app: BaseApp, request: Request): Promise<Response>;
35
+ export declare function isStaticImagesRequest(request: Request): boolean;
36
+ /** Serializes the global staticImages map collected in workerd back to the Node-side build. */
37
+ export declare function handleStaticImagesRequest(): Response;
@@ -1,6 +1,10 @@
1
1
  import { serializeRouteData, deserializeRouteData } from "astro/app/manifest";
2
2
  import { StaticPaths } from "astro:static-paths";
3
- import { STATIC_PATHS_ENDPOINT, PRERENDER_ENDPOINT } from "./prerender-constants.js";
3
+ import {
4
+ STATIC_PATHS_ENDPOINT,
5
+ PRERENDER_ENDPOINT,
6
+ STATIC_IMAGES_ENDPOINT
7
+ } from "./prerender-constants.js";
4
8
  function isStaticPathsRequest(request) {
5
9
  const { pathname } = new URL(request.url);
6
10
  return pathname === STATIC_PATHS_ENDPOINT && request.method === "POST";
@@ -35,9 +39,38 @@ async function handlePrerenderRequest(app, request) {
35
39
  });
36
40
  return app.render(prerenderRequest, { routeData });
37
41
  }
42
+ function isStaticImagesRequest(request) {
43
+ const { pathname } = new URL(request.url);
44
+ return pathname === STATIC_IMAGES_ENDPOINT && request.method === "POST";
45
+ }
46
+ function handleStaticImagesRequest() {
47
+ const staticImages = globalThis.astroAsset?.staticImages;
48
+ if (!staticImages || staticImages.size === 0) {
49
+ return new Response("[]", {
50
+ headers: { "Content-Type": "application/json" }
51
+ });
52
+ }
53
+ const entries = [];
54
+ for (const [originalPath, { originalSrcPath, transforms }] of staticImages) {
55
+ const serializedTransforms = [];
56
+ for (const [hash, { finalPath, transform }] of transforms) {
57
+ serializedTransforms.push({
58
+ hash,
59
+ finalPath,
60
+ transform
61
+ });
62
+ }
63
+ entries.push({ originalPath, originalSrcPath, transforms: serializedTransforms });
64
+ }
65
+ return new Response(JSON.stringify(entries), {
66
+ headers: { "Content-Type": "application/json" }
67
+ });
68
+ }
38
69
  export {
39
70
  handlePrerenderRequest,
71
+ handleStaticImagesRequest,
40
72
  handleStaticPathsRequest,
41
73
  isPrerenderRequest,
74
+ isStaticImagesRequest,
42
75
  isStaticPathsRequest
43
76
  };
@@ -0,0 +1,7 @@
1
+ import type { CompileImageConfig } from '../vite-plugin-config.js';
2
+ /**
3
+ * Installs `globalThis.astroAsset.addStaticImage` for use inside workerd
4
+ * during prerendering. This mirrors the logic in astro's vite-plugin-assets.ts
5
+ * but uses only workerd-safe APIs (no node: imports).
6
+ */
7
+ export declare function installAddStaticImage(config: CompileImageConfig): void;
@@ -0,0 +1,51 @@
1
+ import { joinPaths, prependForwardSlash, removeBase } from "@astrojs/internal-helpers/path";
2
+ import { hashTransform, propsToFilename } from "astro/assets";
3
+ import { isESMImportedImage } from "astro/assets/utils";
4
+ function installAddStaticImage(config) {
5
+ if (globalThis.astroAsset?.addStaticImage) return;
6
+ if (!globalThis.astroAsset) {
7
+ globalThis.astroAsset = { referencedImages: /* @__PURE__ */ new Set() };
8
+ }
9
+ globalThis.astroAsset.addStaticImage = (options, hashProperties, _originalFSPath) => {
10
+ if (!globalThis.astroAsset.staticImages) {
11
+ globalThis.astroAsset.staticImages = /* @__PURE__ */ new Map();
12
+ }
13
+ const ESMImportedImageSrc = isESMImportedImage(options.src) ? options.src.src : options.src;
14
+ const finalOriginalPath = removeBase(
15
+ removeBase(ESMImportedImageSrc, config.base),
16
+ config.assetsPrefix ?? ""
17
+ );
18
+ const hash = hashTransform(options, config.imageServiceEntrypoint, hashProperties);
19
+ let finalFilePath;
20
+ let transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath);
21
+ const transformForHash = transformsForPath?.transforms.get(hash);
22
+ if (transformsForPath && transformForHash) {
23
+ finalFilePath = transformForHash.finalPath;
24
+ } else {
25
+ finalFilePath = prependForwardSlash(
26
+ joinPaths(
27
+ isESMImportedImage(options.src) ? "" : config.buildAssets,
28
+ prependForwardSlash(propsToFilename(finalOriginalPath, options, hash))
29
+ )
30
+ );
31
+ if (!transformsForPath) {
32
+ globalThis.astroAsset.staticImages.set(finalOriginalPath, {
33
+ originalSrcPath: _originalFSPath,
34
+ transforms: /* @__PURE__ */ new Map()
35
+ });
36
+ transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath);
37
+ }
38
+ transformsForPath.transforms.set(hash, {
39
+ finalPath: finalFilePath,
40
+ transform: options
41
+ });
42
+ }
43
+ if (config.assetsPrefix) {
44
+ return encodeURI(joinPaths(config.assetsPrefix, finalFilePath));
45
+ }
46
+ return encodeURI(prependForwardSlash(joinPaths(config.base, finalFilePath)));
47
+ };
48
+ }
49
+ export {
50
+ installAddStaticImage
51
+ };
@@ -1,6 +1,13 @@
1
1
  import type { PluginOption } from 'vite';
2
+ export interface CompileImageConfig {
3
+ base: string;
4
+ assetsPrefix: string | undefined;
5
+ imageServiceEntrypoint: string;
6
+ buildAssets: string;
7
+ }
2
8
  export interface Config {
3
9
  sessionKVBindingName: string;
10
+ compileImageConfig: CompileImageConfig | null;
4
11
  isPrerender: boolean;
5
12
  }
6
13
  export declare function createConfigPlugin(config: Omit<Config, 'isPrerender'>): PluginOption;
@@ -3,12 +3,12 @@ export declare const DEFAULT_SESSION_KV_BINDING_NAME = "SESSION";
3
3
  export declare const DEFAULT_IMAGES_BINDING_NAME = "IMAGES";
4
4
  export declare const DEFAULT_ASSETS_BINDING_NAME = "ASSETS";
5
5
  interface CloudflareConfigOptions {
6
- sessionKVBindingName?: string;
7
- imagesBindingName?: string | false;
6
+ sessionKVBindingName: string | undefined;
7
+ imagesBindingName: string | false | undefined;
8
8
  }
9
9
  /**
10
10
  * Returns a config customizer that sets up the Astro Cloudflare defaults.
11
11
  * Sets the main entrypoint and adds bindings for auto-provisioning.
12
12
  */
13
- export declare function cloudflareConfigCustomizer(options?: CloudflareConfigOptions): PluginConfig['config'];
13
+ export declare function cloudflareConfigCustomizer(options: CloudflareConfigOptions): PluginConfig['config'];
14
14
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astrojs/cloudflare",
3
3
  "description": "Deploy your site to Cloudflare Workers",
4
- "version": "13.0.0-beta.8",
4
+ "version": "13.0.0-beta.9",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -23,9 +23,9 @@
23
23
  "./entrypoints/server": "./dist/entrypoints/server.js",
24
24
  "./entrypoints/preview": "./dist/entrypoints/preview.js",
25
25
  "./entrypoints/server.js": "./dist/entrypoints/server.js",
26
- "./image-service": "./dist/entrypoints/image-service.js",
27
- "./image-endpoint": "./dist/entrypoints/image-endpoint.js",
26
+ "./image-service": "./dist/entrypoints/image-service-external.js",
28
27
  "./image-transform-endpoint": "./dist/entrypoints/image-transform-endpoint.js",
28
+ "./image-service-workerd": "./dist/entrypoints/image-service-workerd.js",
29
29
  "./handler": "./dist/utils/handler.js",
30
30
  "./types.d.ts": "./types.d.ts",
31
31
  "./package.json": "./package.json"
@@ -35,7 +35,7 @@
35
35
  "types.d.ts"
36
36
  ],
37
37
  "dependencies": {
38
- "@cloudflare/vite-plugin": "^1.25.0",
38
+ "@cloudflare/vite-plugin": "^1.25.2",
39
39
  "piccolore": "^0.1.3",
40
40
  "tinyglobby": "^0.2.15",
41
41
  "vite": "^7.3.1",
@@ -47,11 +47,11 @@
47
47
  "wrangler": "^4.61.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@cloudflare/workers-types": "^4.20260213.0",
50
+ "@cloudflare/workers-types": "^4.20260228.0",
51
51
  "@types/node": "^25.2.2",
52
52
  "cheerio": "1.2.0",
53
- "devalue": "^5.6.2",
54
- "astro": "6.0.0-beta.12",
53
+ "devalue": "^5.6.3",
54
+ "astro": "6.0.0-beta.15",
55
55
  "astro-scripts": "0.0.14"
56
56
  },
57
57
  "publishConfig": {
@@ -1,3 +0,0 @@
1
- import type { APIRoute } from 'astro';
2
- export declare const prerender = false;
3
- export declare const GET: APIRoute;
@@ -1,29 +0,0 @@
1
- import { imageConfig } from "astro:assets";
2
- import { isRemotePath } from "@astrojs/internal-helpers/path";
3
- import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
4
- const prerender = false;
5
- const GET = (ctx) => {
6
- const href = ctx.url.searchParams.get("href");
7
- if (!href) {
8
- return new Response("Missing 'href' query parameter", {
9
- status: 400,
10
- statusText: "Missing 'href' query parameter"
11
- });
12
- }
13
- if (isRemotePath(href)) {
14
- if (isRemoteAllowed(href, imageConfig) === false) {
15
- return new Response("Forbidden", { status: 403 });
16
- } else {
17
- return Response.redirect(href, 302);
18
- }
19
- }
20
- const proxied = new URL(href, ctx.url.origin);
21
- if (proxied.origin !== ctx.url.origin) {
22
- return new Response("Forbidden", { status: 403 });
23
- }
24
- return fetch(proxied);
25
- };
26
- export {
27
- GET,
28
- prerender
29
- };