@astrojs/cloudflare 13.0.1 → 13.1.0

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.
@@ -0,0 +1,3 @@
1
+ import type { APIRoute } from 'astro';
2
+ export declare const prerender = false;
3
+ export declare const GET: APIRoute;
@@ -0,0 +1,49 @@
1
+ import { imageConfig } from "astro:assets";
2
+ import { isRemotePath } from "@astrojs/internal-helpers/path";
3
+ import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
4
+ import { env } from "cloudflare:workers";
5
+ const prerender = false;
6
+ const GET = async ({ request }) => {
7
+ try {
8
+ const url = new URL(request.url);
9
+ const href = url.searchParams.get("href");
10
+ if (!href) return new Response("Bad Request", { status: 400 });
11
+ const isRemote = isRemotePath(href);
12
+ let response;
13
+ if (isRemote) {
14
+ if (!isRemoteAllowed(href, imageConfig)) {
15
+ return new Response("Forbidden", { status: 403 });
16
+ }
17
+ response = await fetch(href, { redirect: "manual" });
18
+ } else {
19
+ const sourceUrl = new URL(href, url.origin);
20
+ if (sourceUrl.origin !== url.origin) {
21
+ return new Response("Forbidden", { status: 403 });
22
+ }
23
+ response = await env.ASSETS.fetch(new Request(sourceUrl, { headers: request.headers }));
24
+ }
25
+ if (response.status >= 300 && response.status < 400) {
26
+ return new Response("Not Found", { status: 404 });
27
+ }
28
+ if (!response.ok) {
29
+ return new Response("Not Found", { status: 404 });
30
+ }
31
+ const contentType = response.headers.get("Content-Type") ?? "";
32
+ if (!contentType.startsWith("image/")) {
33
+ return new Response("Forbidden", { status: 403 });
34
+ }
35
+ const headers = new Headers();
36
+ headers.set("Content-Type", contentType);
37
+ headers.set("Cache-Control", "public, max-age=31536000");
38
+ headers.set("Date", (/* @__PURE__ */ new Date()).toUTCString());
39
+ const etag = response.headers.get("ETag");
40
+ if (etag) headers.set("ETag", etag);
41
+ return new Response(response.body, { status: 200, headers });
42
+ } catch (_err) {
43
+ return new Response("Internal Server Error", { status: 500 });
44
+ }
45
+ };
46
+ export {
47
+ GET,
48
+ prerender
49
+ };
@@ -1,3 +1,5 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve as resolvePath } from "node:path";
1
3
  import {
2
4
  preview
3
5
  } from "vite";
@@ -14,6 +16,11 @@ const createPreviewServer = async ({
14
16
  host,
15
17
  root
16
18
  }) => {
19
+ const wranglerConfigPath = resolvePath(fileURLToPath(root), ".wrangler/deploy/config.json");
20
+ if (!existsSync(wranglerConfigPath)) {
21
+ logger.error("No build output found. Run `astro build` before running `astro preview`.");
22
+ process.exit(1);
23
+ }
17
24
  const startServerTime = performance.now();
18
25
  let previewServer;
19
26
  try {
@@ -80,7 +87,7 @@ function serverStart({
80
87
  host,
81
88
  base
82
89
  }) {
83
- const version = "13.0.1";
90
+ const version = "13.1.0";
84
91
  const localPrefix = `${colors.dim("\u2503")} Local `;
85
92
  const networkPrefix = `${colors.dim("\u2503")} Network `;
86
93
  const emptyPrefix = " ".repeat(11);
@@ -9,7 +9,7 @@ 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
+ const contents = frontmatterMatch[1].replace(/\breturn\s*;/g, "throw 0;").replace(/\breturn\b/g, "throw ");
13
13
  return {
14
14
  contents,
15
15
  loader: "ts"
package/dist/index.d.ts CHANGED
@@ -25,6 +25,13 @@ export interface Options extends Pick<PluginConfig, 'auxiliaryWorkers' | 'config
25
25
  * See https://developers.cloudflare.com/images/transform-images/bindings/ for more details.
26
26
  */
27
27
  imagesBindingName?: string;
28
+ /**
29
+ * Controls which runtime is used for prerendering static pages at build time.
30
+ *
31
+ * - `'workerd'` (default): Uses Cloudflare's workerd runtime.
32
+ * - `'node'`: Uses Astro's default node prerender environment.
33
+ */
34
+ prerenderEnvironment?: 'workerd' | 'node';
28
35
  experimental?: Pick<NonNullable<PluginConfig['experimental']>, 'headersAndRedirectsDevModeSupport'>;
29
36
  }
30
- export default function createIntegration({ imageService, sessionKVBindingName, imagesBindingName, ...cloudflareOptions }?: Options): AstroIntegration;
37
+ export default function createIntegration({ imageService, sessionKVBindingName, imagesBindingName, prerenderEnvironment, ...cloudflareOptions }?: Options): AstroIntegration;
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  setImageConfig
12
12
  } from "./utils/image-config.js";
13
13
  import { createConfigPlugin } from "./vite-plugin-config.js";
14
+ import { createNodePrerenderPlugin } from "./vite-plugin-dev-server-prerender-middleware.js";
14
15
  import {
15
16
  cloudflareConfigCustomizer,
16
17
  DEFAULT_SESSION_KV_BINDING_NAME,
@@ -19,11 +20,23 @@ import {
19
20
  import { parseEnv } from "node:util";
20
21
  import { sessionDrivers } from "astro/config";
21
22
  import { createCloudflarePrerenderer } from "./prerenderer.js";
22
- import { createRequire } from "node:module";
23
+ const CLOUDFLARE_KV_SESSION_DRIVER_ENTRYPOINT = sessionDrivers.cloudflareKVBinding().entrypoint;
24
+ function usesCloudflareKVSessionDriver(session) {
25
+ const driver = session?.driver;
26
+ if (!driver) {
27
+ return false;
28
+ }
29
+ if (typeof driver === "string") {
30
+ return driver === "cloudflareKVBinding" || driver === "cloudflare-kv-binding";
31
+ }
32
+ const entrypoint = typeof driver.entrypoint === "string" ? driver.entrypoint : driver.entrypoint.toString();
33
+ return entrypoint === CLOUDFLARE_KV_SESSION_DRIVER_ENTRYPOINT || entrypoint.endsWith("cloudflare-kv-binding");
34
+ }
23
35
  function createIntegration({
24
36
  imageService,
25
37
  sessionKVBindingName = DEFAULT_SESSION_KV_BINDING_NAME,
26
38
  imagesBindingName = DEFAULT_IMAGES_BINDING_NAME,
39
+ prerenderEnvironment = "workerd",
27
40
  ...cloudflareOptions
28
41
  } = {}) {
29
42
  let _config;
@@ -62,22 +75,26 @@ function createIntegration({
62
75
  ttl: session?.ttl
63
76
  };
64
77
  }
78
+ const needsSessionKVBinding = usesCloudflareKVSessionDriver(session);
65
79
  const needsImagesBindingForDev = isCompile && command === "dev";
66
80
  cfPluginConfig = {
67
81
  config: cloudflareConfigCustomizer({
82
+ needsSessionKVBinding,
68
83
  sessionKVBindingName,
69
84
  imagesBindingName: needsImagesBinding || needsImagesBindingForDev ? imagesBindingName : false
70
85
  }),
71
- experimental: {
72
- prerenderWorker: {
73
- config(_, { entryWorkerConfig }) {
74
- return {
75
- ...entryWorkerConfig,
76
- name: "prerender",
77
- ...needsImagesBinding && !entryWorkerConfig.images && {
78
- images: { binding: imagesBindingName }
79
- }
80
- };
86
+ ...prerenderEnvironment === "workerd" && {
87
+ experimental: {
88
+ prerenderWorker: {
89
+ config(_, { entryWorkerConfig }) {
90
+ return {
91
+ ...entryWorkerConfig,
92
+ name: "prerender",
93
+ ...needsImagesBinding && !entryWorkerConfig.images && {
94
+ images: { binding: imagesBindingName }
95
+ }
96
+ };
97
+ }
81
98
  }
82
99
  }
83
100
  }
@@ -92,7 +109,12 @@ function createIntegration({
92
109
  session,
93
110
  vite: {
94
111
  plugins: [
95
- cfVitePlugin({ ...cfPluginConfig, viteEnvironment: { name: "ssr" } }),
112
+ ...prerenderEnvironment === "node" && command === "dev" ? [createNodePrerenderPlugin()] : [],
113
+ cfVitePlugin({
114
+ ...cloudflareOptions,
115
+ ...cfPluginConfig,
116
+ viteEnvironment: { name: "ssr" }
117
+ }),
96
118
  {
97
119
  name: "@astrojs/cloudflare:cf-imports",
98
120
  enforce: "pre",
@@ -186,7 +208,7 @@ function createIntegration({
186
208
  image: setImageConfig(imageService, config.image, command, logger)
187
209
  });
188
210
  if (cloudflareOptions.configPath) {
189
- addWatchFile(createRequire(import.meta.url).resolve(cloudflareOptions.configPath));
211
+ addWatchFile(new URL(cloudflareOptions.configPath, config.root));
190
212
  }
191
213
  addWatchFile(new URL("./wrangler.toml", config.root));
192
214
  addWatchFile(new URL("./wrangler.json", config.root));
@@ -240,17 +262,19 @@ function createIntegration({
240
262
  }
241
263
  },
242
264
  "astro:build:start": ({ setPrerenderer }) => {
243
- setPrerenderer(
244
- createCloudflarePrerenderer({
245
- root: _config.root,
246
- serverDir: _config.build.server,
247
- clientDir: _config.build.client,
248
- base: _config.base,
249
- trailingSlash: _config.trailingSlash,
250
- cfPluginConfig,
251
- hasCompileImageService: buildService === "compile"
252
- })
253
- );
265
+ if (prerenderEnvironment === "workerd") {
266
+ setPrerenderer(
267
+ createCloudflarePrerenderer({
268
+ root: _config.root,
269
+ serverDir: _config.build.server,
270
+ clientDir: _config.build.client,
271
+ base: _config.base,
272
+ trailingSlash: _config.trailingSlash,
273
+ cfPluginConfig,
274
+ hasCompileImageService: buildService === "compile"
275
+ })
276
+ );
277
+ }
254
278
  },
255
279
  "astro:build:setup": ({ vite, target }) => {
256
280
  if (target === "server") {
@@ -1,6 +1,12 @@
1
1
  import { imageConfig } from "astro:assets";
2
2
  import { isRemotePath } from "@astrojs/internal-helpers/path";
3
3
  import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
4
+ const qualityTable = {
5
+ low: 25,
6
+ mid: 50,
7
+ high: 80,
8
+ max: 100
9
+ };
4
10
  async function transform(rawUrl, images, assets) {
5
11
  const url = new URL(rawUrl);
6
12
  const href = url.searchParams.get("href");
@@ -28,10 +34,11 @@ async function transform(rawUrl, images, assets) {
28
34
  return (await input.transform({
29
35
  width: url.searchParams.has("w") ? Number.parseInt(url.searchParams.get("w")) : void 0,
30
36
  height: url.searchParams.has("h") ? Number.parseInt(url.searchParams.get("h")) : void 0,
31
- // `quality` is documented, but doesn't appear to work in manual testing...
32
- // quality: url.searchParams.get('q'),
33
37
  fit: url.searchParams.get("fit")
34
- }).output({ format: outputFormat })).response();
38
+ }).output({
39
+ quality: url.searchParams.get("q") ? qualityTable[url.searchParams.get("q")] ?? Number.parseInt(url.searchParams.get("q")) : void 0,
40
+ format: outputFormat
41
+ })).response();
35
42
  }
36
43
  export {
37
44
  transform
@@ -10,12 +10,11 @@ export declare function normalizeImageServiceConfig(config: ImageServiceConfig |
10
10
  runtimeService: ImageServiceMode;
11
11
  };
12
12
  export declare function setImageConfig(service: ImageServiceConfig | undefined, config: AstroConfig['image'], command: HookParameters<'astro:config:setup'>['command'], logger: AstroIntegrationLogger): {
13
- service: import("astro").ImageServiceConfig<Record<string, any>>;
13
+ service: {
14
+ entrypoint: string;
15
+ };
14
16
  endpoint: {
15
17
  entrypoint: string;
16
- } | {
17
- route: string;
18
- entrypoint?: string | undefined;
19
18
  };
20
19
  domains: string[];
21
20
  remotePatterns: {
@@ -30,11 +29,10 @@ export declare function setImageConfig(service: ImageServiceConfig | undefined,
30
29
  objectPosition?: string | undefined;
31
30
  breakpoints?: number[] | undefined;
32
31
  } | {
33
- service: {
34
- entrypoint: string;
35
- };
32
+ service: import("astro").ImageServiceConfig<Record<string, any>>;
36
33
  endpoint: {
37
- entrypoint: string;
34
+ route: string;
35
+ entrypoint?: string | undefined;
38
36
  };
39
37
  domains: string[];
40
38
  remotePatterns: {
@@ -13,6 +13,9 @@ function normalizeImageServiceConfig(config) {
13
13
  };
14
14
  }
15
15
  const GENERIC_ENDPOINT = { entrypoint: "astro/assets/endpoint/generic" };
16
+ const CLOUDFLARE_PASSTHROUGH_ENDPOINT = {
17
+ entrypoint: "@astrojs/cloudflare/image-passthrough-endpoint"
18
+ };
16
19
  const WORKERD_IMAGE_SERVICE = { entrypoint: "@astrojs/cloudflare/image-service-workerd" };
17
20
  function setImageConfig(service, config, command, logger) {
18
21
  const { buildService, runtimeService } = normalizeImageServiceConfig(service);
@@ -21,7 +24,7 @@ function setImageConfig(service, config, command, logger) {
21
24
  return {
22
25
  ...config,
23
26
  service: passthroughImageService(),
24
- endpoint: command === "dev" ? GENERIC_ENDPOINT : config.endpoint
27
+ endpoint: command === "dev" ? GENERIC_ENDPOINT : CLOUDFLARE_PASSTHROUGH_ENDPOINT
25
28
  };
26
29
  case "cloudflare":
27
30
  return {
@@ -42,7 +45,7 @@ function setImageConfig(service, config, command, logger) {
42
45
  service: WORKERD_IMAGE_SERVICE,
43
46
  // Dev: IMAGES binding (via Cloudflare Vite plugin) for real transforms.
44
47
  // 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
48
+ endpoint: command === "dev" || runtimeService === "cloudflare-binding" ? { entrypoint: "@astrojs/cloudflare/image-transform-endpoint" } : CLOUDFLARE_PASSTHROUGH_ENDPOINT
46
49
  };
47
50
  case "custom":
48
51
  return { ...config };
@@ -0,0 +1,6 @@
1
+ import type * as vite from 'vite';
2
+ /**
3
+ * Enables Astro core prerender middleware in dev so prerendered routes can
4
+ * run in Node while non-prerendered routes continue through workerd.
5
+ */
6
+ export declare function createNodePrerenderPlugin(): vite.Plugin;
@@ -0,0 +1,26 @@
1
+ const devPrerenderMiddlewareSymbol = /* @__PURE__ */ Symbol.for("astro.devPrerenderMiddleware");
2
+ function createNodePrerenderPlugin() {
3
+ return {
4
+ name: "@astrojs/cloudflare:dev-server-prerender-middleware",
5
+ config() {
6
+ return {
7
+ environments: {
8
+ prerender: { dev: {} }
9
+ }
10
+ };
11
+ },
12
+ // Disable dep optimization for the `prerender` environment so dependencies
13
+ // are loaded via native import() with correct import.meta.url semantics.
14
+ configEnvironment(environmentName) {
15
+ if (environmentName === "prerender") {
16
+ return { optimizeDeps: { noDiscovery: true, include: [] } };
17
+ }
18
+ },
19
+ configureServer(server) {
20
+ server[devPrerenderMiddlewareSymbol] = true;
21
+ }
22
+ };
23
+ }
24
+ export {
25
+ createNodePrerenderPlugin
26
+ };
@@ -4,6 +4,7 @@ export declare const DEFAULT_IMAGES_BINDING_NAME = "IMAGES";
4
4
  export declare const DEFAULT_ASSETS_BINDING_NAME = "ASSETS";
5
5
  interface CloudflareConfigOptions {
6
6
  sessionKVBindingName: string | undefined;
7
+ needsSessionKVBinding?: boolean;
7
8
  imagesBindingName: string | false | undefined;
8
9
  }
9
10
  /**
package/dist/wrangler.js CHANGED
@@ -3,6 +3,7 @@ const DEFAULT_IMAGES_BINDING_NAME = "IMAGES";
3
3
  const DEFAULT_ASSETS_BINDING_NAME = "ASSETS";
4
4
  function cloudflareConfigCustomizer(options) {
5
5
  const sessionKVBindingName = options?.sessionKVBindingName ?? DEFAULT_SESSION_KV_BINDING_NAME;
6
+ const needsSessionKVBinding = options?.needsSessionKVBinding ?? true;
6
7
  const imagesBindingName = options?.imagesBindingName === false ? void 0 : options?.imagesBindingName ?? DEFAULT_IMAGES_BINDING_NAME;
7
8
  return (config) => {
8
9
  const hasSessionBinding = config.kv_namespaces?.some(
@@ -12,7 +13,7 @@ function cloudflareConfigCustomizer(options) {
12
13
  const hasAssetsBinding = config.assets?.binding !== void 0;
13
14
  return {
14
15
  main: config.main ?? "@astrojs/cloudflare/entrypoints/server",
15
- kv_namespaces: hasSessionBinding ? void 0 : [
16
+ kv_namespaces: !needsSessionKVBinding || hasSessionBinding ? void 0 : [
16
17
  {
17
18
  binding: sessionKVBindingName
18
19
  }
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.1",
4
+ "version": "13.1.0",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -25,6 +25,7 @@
25
25
  "./entrypoints/server.js": "./dist/entrypoints/server.js",
26
26
  "./image-service": "./dist/entrypoints/image-service-external.js",
27
27
  "./image-transform-endpoint": "./dist/entrypoints/image-transform-endpoint.js",
28
+ "./image-passthrough-endpoint": "./dist/entrypoints/image-passthrough-endpoint.js",
28
29
  "./image-service-workerd": "./dist/entrypoints/image-service-workerd.js",
29
30
  "./handler": "./dist/utils/handler.js",
30
31
  "./types.d.ts": "./types.d.ts",
@@ -51,8 +52,8 @@
51
52
  "@types/node": "^25.2.2",
52
53
  "cheerio": "1.2.0",
53
54
  "devalue": "^5.6.3",
54
- "astro": "6.0.0",
55
- "astro-scripts": "0.0.14"
55
+ "astro-scripts": "0.0.14",
56
+ "astro": "6.0.3"
56
57
  },
57
58
  "publishConfig": {
58
59
  "provenance": true