@astrojs/cloudflare 14.0.0-alpha.0 → 14.0.0-alpha.1

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.
package/README.md CHANGED
@@ -34,5 +34,5 @@ Copyright (c) 2023–present [Astro][astro]
34
34
  [coc]: https://github.com/withastro/.github/blob/main/CODE_OF_CONDUCT.md
35
35
  [community]: https://github.com/withastro/.github/blob/main/COMMUNITY_GUIDE.md
36
36
  [discord]: https://astro.build/chat/
37
- [issues]: https://github.com/withastro/adapter/issues
37
+ [issues]: https://github.com/withastro/astro/issues
38
38
  [astro-integration]: https://docs.astro.build/en/guides/integrations/
@@ -2,6 +2,7 @@ import { imageConfig } from "astro:assets";
2
2
  import { isRemotePath } from "@astrojs/internal-helpers/path";
3
3
  import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
4
4
  import { env } from "cloudflare:workers";
5
+ import { fetchWithRedirects } from "astro/assets";
5
6
  const prerender = false;
6
7
  const GET = async ({ request }) => {
7
8
  try {
@@ -14,7 +15,10 @@ const GET = async ({ request }) => {
14
15
  if (!isRemoteAllowed(href, imageConfig)) {
15
16
  return new Response("Forbidden", { status: 403 });
16
17
  }
17
- response = await fetch(href, { redirect: "manual" });
18
+ response = await fetchWithRedirects({
19
+ url: href,
20
+ imageConfig
21
+ });
18
22
  } else {
19
23
  const sourceUrl = new URL(href, url.origin);
20
24
  if (sourceUrl.origin !== url.origin) {
@@ -41,7 +41,7 @@ const createPreviewServer = async ({
41
41
  allowedHosts
42
42
  },
43
43
  plugins: [
44
- cfVitePlugin({ ...globalThis.astroCloudflareOptions, viteEnvironment: { name: "ssr" } })
44
+ cfVitePlugin({ ...globalThis.astroCloudflareConfig, viteEnvironment: { name: "ssr" } })
45
45
  ]
46
46
  });
47
47
  } catch (err) {
@@ -88,7 +88,7 @@ function serverStart({
88
88
  host,
89
89
  base
90
90
  }) {
91
- const version = "14.0.0-alpha.0";
91
+ const version = "14.0.0-alpha.1";
92
92
  const localPrefix = `${colors.dim("\u2503")} Local `;
93
93
  const networkPrefix = `${colors.dim("\u2503")} Network `;
94
94
  const emptyPrefix = " ".repeat(11);
@@ -1,15 +1,22 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
3
+ const RETURN_REPLACE_RE = /(\/\/[^\n]*|\/\*[\s\S]*?\*\/|`(?:[^`\\]|\\.)*`|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(?<!\.)\breturn(\s*;|\b)/g;
4
+ function replaceTopLevelReturns(code) {
5
+ return code.replace(RETURN_REPLACE_RE, (_match, skip, tail) => {
6
+ if (skip !== void 0) return skip;
7
+ return tail.trim() === ";" ? "throw 0;" : "throw ";
8
+ });
9
+ }
3
10
  function astroFrontmatterScanPlugin() {
4
11
  return {
5
12
  name: "astro-frontmatter-scan",
6
13
  setup(build) {
7
- build.onLoad({ filter: /\.astro$/ }, async (args) => {
14
+ build.onLoad({ filter: /\.astro$/, namespace: "file" }, async (args) => {
8
15
  try {
9
16
  const code = await readFile(args.path, "utf-8");
10
17
  const frontmatterMatch = FRONTMATTER_RE.exec(code);
11
18
  if (frontmatterMatch) {
12
- const contents = frontmatterMatch[1].replace(/\breturn\s*;/g, "throw 0;").replace(/\breturn\b/g, "throw ");
19
+ const contents = replaceTopLevelReturns(frontmatterMatch[1]);
13
20
  return {
14
21
  contents: contents + "\nexport default {}",
15
22
  loader: "ts"
@@ -0,0 +1,12 @@
1
+ import type { FetchState } from 'astro/fetch';
2
+ /**
3
+ * Applies Cloudflare-specific setup to a `FetchState`:
4
+ * - Injects the SESSION KV binding
5
+ * - Serves static assets via the ASSETS binding
6
+ * - Sets `locals.cfContext`, client address, `waitUntil`, and error page fetch
7
+ *
8
+ * Returns a `Response` if the request was handled by the ASSETS binding
9
+ * (static file hit). Returns `undefined` when the caller should continue
10
+ * to Astro rendering.
11
+ */
12
+ export declare function cf(state: FetchState, env: Env, ctx: ExecutionContext): Promise<Response | undefined>;
package/dist/fetch.js ADDED
@@ -0,0 +1,37 @@
1
+ import { env as globalEnv } from "cloudflare:workers";
2
+ import { createApp } from "astro/app/entrypoint";
3
+ import { setGetEnv } from "astro/env/setup";
4
+ import { createGetEnv } from "./utils/env.js";
5
+ import {
6
+ injectSessionBinding,
7
+ matchStaticAsset,
8
+ fallbackToAssets,
9
+ createErrorPageFetch,
10
+ createLocals,
11
+ getClientAddress
12
+ } from "./utils/cf.js";
13
+ let app;
14
+ function ensureInitialized() {
15
+ if (!app) {
16
+ setGetEnv(createGetEnv(globalEnv));
17
+ app = createApp();
18
+ }
19
+ }
20
+ async function cf(state, env, ctx) {
21
+ ensureInitialized();
22
+ injectSessionBinding(app.manifest, env);
23
+ const staticAsset = matchStaticAsset(app.manifest, state.request.url, env);
24
+ if (staticAsset) return staticAsset;
25
+ if (!state.routeData) {
26
+ const asset = await fallbackToAssets(state.request.url, env);
27
+ if (asset) return asset;
28
+ }
29
+ Object.assign(state.locals, createLocals(ctx));
30
+ state.clientAddress = getClientAddress(state.request);
31
+ state.renderOptions.waitUntil = ctx.waitUntil.bind(ctx);
32
+ state.renderOptions.prerenderedErrorPageFetch = createErrorPageFetch(env);
33
+ return void 0;
34
+ }
35
+ export {
36
+ cf
37
+ };
package/dist/hono.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Duck-typed Hono context — matches Hono's `Context` shape for
3
+ * Cloudflare Workers without importing from `hono` at runtime.
4
+ */
5
+ type HonoCloudflareContextLike = {
6
+ req: {
7
+ raw: Request;
8
+ };
9
+ env: Env;
10
+ executionCtx: ExecutionContext;
11
+ get?: (key: string) => unknown;
12
+ set?: (key: string, value: unknown) => void;
13
+ };
14
+ type HonoMiddlewareHandler = (context: HonoCloudflareContextLike, next: () => Promise<void>) => Promise<Response | void>;
15
+ /**
16
+ * Hono middleware that applies Cloudflare-specific setup.
17
+ *
18
+ * Reads `env` and `executionCtx` from the Hono context (provided
19
+ * automatically by Hono on Cloudflare Workers). Handles static assets
20
+ * via the ASSETS binding, injects the SESSION KV binding, and sets
21
+ * `locals.cfContext`, client address, `waitUntil`, and error page fetch.
22
+ *
23
+ * If the request matches a static asset, returns the asset response
24
+ * directly. Otherwise calls `next()` to continue the middleware chain.
25
+ */
26
+ export declare function cf(): HonoMiddlewareHandler;
27
+ export {};
package/dist/hono.js ADDED
@@ -0,0 +1,21 @@
1
+ import { FetchState } from "astro/fetch";
2
+ import { cf as cfFetch } from "./fetch.js";
3
+ const FETCH_STATE_KEY = "fetchState";
4
+ function getFetchState(context) {
5
+ const state = context.get?.(FETCH_STATE_KEY);
6
+ if (state) return state;
7
+ const nextState = new FetchState(context.req.raw);
8
+ context.set?.(FETCH_STATE_KEY, nextState);
9
+ return nextState;
10
+ }
11
+ function cf() {
12
+ return async (context, next) => {
13
+ const state = getFetchState(context);
14
+ const asset = await cfFetch(state, context.env, context.executionCtx);
15
+ if (asset) return asset;
16
+ await next();
17
+ };
18
+ }
19
+ export {
20
+ cf
21
+ };
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { createReadStream, existsSync, readFileSync } from "node:fs";
2
- import { appendFile, stat } from "node:fs/promises";
2
+ import { appendFile, readFile, rename, stat, writeFile } from "node:fs/promises";
3
+ import { relative } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { normalizePath } from "vite";
3
6
  import { createInterface } from "node:readline/promises";
4
7
  import { removeLeadingForwardSlash } from "@astrojs/internal-helpers/path";
5
8
  import { createRedirectsFromAstroRoutes, printAsRedirects } from "@astrojs/underscore-redirects";
@@ -20,6 +23,7 @@ import {
20
23
  import { parseEnv } from "node:util";
21
24
  import { sessionDrivers } from "astro/config";
22
25
  import { createCloudflarePrerenderer } from "./prerenderer.js";
26
+ import cfPrismPlugin from "./vite-plugin-prism.js";
23
27
  const CLOUDFLARE_KV_SESSION_DRIVER_ENTRYPOINT = sessionDrivers.cloudflareKVBinding().entrypoint;
24
28
  function usesCloudflareKVSessionDriver(session) {
25
29
  const driver = session?.driver;
@@ -57,15 +61,16 @@ function createIntegration({
57
61
  ...cloudflareOptions
58
62
  } = {}) {
59
63
  let _config;
64
+ let _buildOutput;
65
+ let _originalClientDir;
60
66
  let _routes;
61
- let _isFullyStatic = false;
62
67
  let cfPluginConfig;
63
68
  const { buildService, runtimeService } = normalizeImageServiceConfig(imageService);
64
69
  const needsImagesBinding = runtimeService === "cloudflare-binding";
65
70
  return {
66
71
  name: "@astrojs/cloudflare",
67
72
  hooks: {
68
- "astro:config:setup": ({ command, config, updateConfig, logger, addWatchFile }) => {
73
+ "astro:config:setup": async ({ command, config, updateConfig, logger, addWatchFile }) => {
69
74
  if (!!process.versions.webcontainer) {
70
75
  throw new Error("`workerd` does not run on Stackblitz.");
71
76
  }
@@ -96,7 +101,7 @@ function createIntegration({
96
101
  const needsImagesBindingForDev = isCompile && command === "dev";
97
102
  const usesContentCollections = hasContentCollectionsConfig(config.srcDir);
98
103
  const prebundleContentRuntime = command === "dev" && usesContentCollections;
99
- cfPluginConfig = {
104
+ const adapterPluginConfig = {
100
105
  config: cloudflareConfigCustomizer({
101
106
  needsSessionKVBinding,
102
107
  sessionKVBindingName,
@@ -122,9 +127,17 @@ function createIntegration({
122
127
  }
123
128
  }
124
129
  };
130
+ cfPluginConfig = { ...cloudflareOptions, ...adapterPluginConfig };
125
131
  if (command === "preview") {
126
- globalThis.astroCloudflareOptions = cfPluginConfig;
132
+ globalThis.astroCloudflareConfig = cfPluginConfig;
127
133
  }
134
+ const prismFiles = [
135
+ "@astrojs/prism > prismjs",
136
+ "@astrojs/prism > prismjs/components.js",
137
+ "@astrojs/prism > prismjs/dependencies.js"
138
+ ];
139
+ const isAstroPrismPackageInstalled = await getIsAstroPrismInstalled(config.root);
140
+ const userOptimizeDeps = config.vite?.optimizeDeps;
128
141
  updateConfig({
129
142
  build: {
130
143
  redirects: false
@@ -134,9 +147,9 @@ function createIntegration({
134
147
  plugins: [
135
148
  ...prerenderEnvironment === "node" && command === "dev" ? [createNodePrerenderPlugin()] : [],
136
149
  cfVitePlugin({
137
- ...cloudflareOptions,
138
150
  ...cfPluginConfig,
139
- viteEnvironment: { name: "ssr" }
151
+ viteEnvironment: { name: "ssr" },
152
+ assetsOnly: () => _buildOutput === "static"
140
153
  }),
141
154
  {
142
155
  name: "@astrojs/cloudflare:cf-imports",
@@ -177,6 +190,9 @@ function createIntegration({
177
190
  "astro > piccolore",
178
191
  "astro > picomatch",
179
192
  "astro/app",
193
+ "astro/app/fetch/default-handler",
194
+ "astro/fetch",
195
+ "astro/hono",
180
196
  "astro/assets",
181
197
  "astro/assets/runtime",
182
198
  "astro/assets/utils/inferRemoteSize.js",
@@ -185,7 +201,9 @@ function createIntegration({
185
201
  "astro/compiler-runtime",
186
202
  "astro/jsx-runtime",
187
203
  "astro/app/entrypoint/dev",
188
- "astro/virtual-modules/middleware.js"
204
+ "astro/virtual-modules/middleware.js",
205
+ ...isAstroPrismPackageInstalled ? prismFiles : [],
206
+ ...Array.isArray(userOptimizeDeps?.include) ? userOptimizeDeps.include : []
189
207
  ],
190
208
  exclude: [
191
209
  "unstorage/drivers/cloudflare-kv-binding",
@@ -193,7 +211,8 @@ function createIntegration({
193
211
  "virtual:astro:*",
194
212
  "virtual:astro-cloudflare:*",
195
213
  "virtual:@astrojs/*",
196
- "@astrojs/starlight"
214
+ "@astrojs/starlight",
215
+ ...Array.isArray(userOptimizeDeps?.exclude) ? userOptimizeDeps.exclude : []
197
216
  ],
198
217
  esbuildOptions: {
199
218
  // Suppress Vite's `createRequire(import.meta.url)` banner to work around
@@ -201,7 +220,8 @@ function createIntegration({
201
220
  // incorrectly rewrites identifiers inside `import.meta` when an imported
202
221
  // binding shares the same name (e.g. zod v4 exports `meta`).
203
222
  banner: { js: "" },
204
- plugins: [astroFrontmatterScanPlugin()]
223
+ plugins: [astroFrontmatterScanPlugin()],
224
+ ...userOptimizeDeps?.esbuildOptions?.loader ? { loader: userOptimizeDeps.esbuildOptions.loader } : {}
205
225
  }
206
226
  }
207
227
  };
@@ -237,7 +257,8 @@ function createIntegration({
237
257
  imageServiceEntrypoint: "@astrojs/cloudflare/image-service-workerd",
238
258
  buildAssets: config.build.assets ?? "_astro"
239
259
  } : null
240
- })
260
+ }),
261
+ cfPrismPlugin()
241
262
  ]
242
263
  },
243
264
  image: setImageConfig(imageService, config.image, command, logger)
@@ -251,11 +272,14 @@ function createIntegration({
251
272
  },
252
273
  "astro:routes:resolved": ({ routes }) => {
253
274
  _routes = routes;
254
- const nonInternalRoutes = routes.filter((route) => route.origin !== "internal");
255
- _isFullyStatic = nonInternalRoutes.length > 0 && nonInternalRoutes.every((route) => route.isPrerendered);
256
275
  },
257
- "astro:config:done": ({ setAdapter, config, injectTypes, logger }) => {
276
+ "astro:config:done": ({ setAdapter, config, injectTypes, logger, buildOutput }) => {
258
277
  _config = config;
278
+ _buildOutput = buildOutput;
279
+ _originalClientDir = new URL(config.build.client.href);
280
+ if (config.base !== "/") {
281
+ config.build.client = new URL("." + config.base + "/", config.build.client);
282
+ }
259
283
  injectTypes({
260
284
  filename: "cloudflare.d.ts",
261
285
  content: '/// <reference types="@astrojs/cloudflare/types.d.ts" />'
@@ -263,9 +287,10 @@ function createIntegration({
263
287
  setAdapter({
264
288
  name: "@astrojs/cloudflare",
265
289
  adapterFeatures: {
266
- buildOutput: "server",
290
+ buildOutput,
267
291
  middlewareMode: "classic",
268
- preserveBuildClientDir: true
292
+ preserveBuildClientDir: true,
293
+ preserveBuildServerDir: true
269
294
  },
270
295
  entrypointResolution: "auto",
271
296
  previewEntrypoint: "@astrojs/cloudflare/entrypoints/preview",
@@ -330,9 +355,32 @@ function createIntegration({
330
355
  }
331
356
  },
332
357
  "astro:build:done": async ({ dir, logger, assets }) => {
358
+ if (_config.base !== "/") {
359
+ for (const file of [".assetsignore", "_headers"]) {
360
+ try {
361
+ await rename(
362
+ new URL(`./${file}`, _config.build.client),
363
+ new URL(`./${file}`, _originalClientDir)
364
+ );
365
+ } catch {
366
+ }
367
+ }
368
+ try {
369
+ const wranglerJsonUrl = new URL("./wrangler.json", _config.build.server);
370
+ const raw = await readFile(wranglerJsonUrl, "utf-8");
371
+ const wranglerConfig = JSON.parse(raw);
372
+ if (wranglerConfig.assets?.directory) {
373
+ wranglerConfig.assets.directory = normalizePath(
374
+ relative(fileURLToPath(_config.build.server), fileURLToPath(_originalClientDir))
375
+ );
376
+ await writeFile(wranglerJsonUrl, JSON.stringify(wranglerConfig));
377
+ }
378
+ } catch {
379
+ }
380
+ }
333
381
  let redirectsExists = false;
334
382
  try {
335
- const redirectsStat = await stat(new URL("./_redirects", _config.build.client));
383
+ const redirectsStat = await stat(new URL("./_redirects", _originalClientDir));
336
384
  if (redirectsStat.isFile()) {
337
385
  redirectsExists = true;
338
386
  }
@@ -342,7 +390,7 @@ function createIntegration({
342
390
  const redirects = [];
343
391
  if (redirectsExists) {
344
392
  const rl = createInterface({
345
- input: createReadStream(new URL("./_redirects", _config.build.client)),
393
+ input: createReadStream(new URL("./_redirects", _originalClientDir)),
346
394
  crlfDelay: Number.POSITIVE_INFINITY
347
395
  });
348
396
  for await (const line of rl) {
@@ -364,13 +412,13 @@ function createIntegration({
364
412
  )
365
413
  ),
366
414
  dir,
367
- buildOutput: _isFullyStatic ? "static" : "server",
415
+ buildOutput: _buildOutput,
368
416
  assets
369
417
  });
370
418
  if (!trueRedirects.empty()) {
371
419
  try {
372
420
  await appendFile(
373
- new URL("./_redirects", _config.build.client),
421
+ new URL("./_redirects", _originalClientDir),
374
422
  printAsRedirects(trueRedirects)
375
423
  );
376
424
  } catch (_error) {
@@ -382,6 +430,16 @@ function createIntegration({
382
430
  }
383
431
  };
384
432
  }
433
+ async function getIsAstroPrismInstalled(rootURL) {
434
+ try {
435
+ const pkgURL = new URL("./package.json", rootURL);
436
+ const input = await readFile(pkgURL, { encoding: "utf-8" });
437
+ const pkgJson = JSON.parse(input);
438
+ return Object.hasOwn(pkgJson["dependencies"], "@astrojs/prism");
439
+ } catch {
440
+ return false;
441
+ }
442
+ }
385
443
  export {
386
444
  createIntegration as default
387
445
  };
@@ -0,0 +1,33 @@
1
+ export interface Runtime {
2
+ cfContext: ExecutionContext;
3
+ }
4
+ /** Minimal manifest shape needed by the Cloudflare helpers. */
5
+ export interface ManifestLike {
6
+ assets: Set<string>;
7
+ sessionConfig?: {
8
+ options?: Record<string, unknown>;
9
+ } | undefined;
10
+ }
11
+ /**
12
+ * Returns a `Response` from the ASSETS binding if the request pathname
13
+ * is a known static asset. Returns `undefined` otherwise.
14
+ */
15
+ export declare function matchStaticAsset(manifest: ManifestLike, requestUrl: string, env: Env): Response | undefined;
16
+ /**
17
+ * Tries the ASSETS binding as a fallback for an unmatched route.
18
+ * Returns the asset `Response` if found (non-404), `undefined` otherwise.
19
+ */
20
+ export declare function fallbackToAssets(requestUrl: string, env: Env): Promise<Response | undefined>;
21
+ /**
22
+ * Creates a fetch function for prerendered error pages via the ASSETS binding.
23
+ */
24
+ export declare function createErrorPageFetch(env: Env): (url: string) => Promise<Response>;
25
+ /**
26
+ * Creates the Cloudflare-specific locals object with `cfContext`
27
+ * and deprecated `runtime` property getters.
28
+ */
29
+ export declare function createLocals(ctx: ExecutionContext): Runtime;
30
+ /**
31
+ * Extracts the client IP address from the `cf-connecting-ip` header.
32
+ */
33
+ export declare function getClientAddress(request: Request): string | undefined;
@@ -0,0 +1,63 @@
1
+ import { getValidatedIpFromHeader } from "@astrojs/internal-helpers/request";
2
+ function matchStaticAsset(manifest, requestUrl, env) {
3
+ const { pathname } = new URL(requestUrl);
4
+ if (manifest.assets.has(pathname)) {
5
+ return env.ASSETS.fetch(requestUrl.replace(/\.html$/, ""));
6
+ }
7
+ return void 0;
8
+ }
9
+ async function fallbackToAssets(requestUrl, env) {
10
+ const asset = await env.ASSETS.fetch(
11
+ requestUrl.replace(/index.html$/, "").replace(/\.html$/, "")
12
+ );
13
+ if (asset.status !== 404) {
14
+ return asset;
15
+ }
16
+ return void 0;
17
+ }
18
+ function createErrorPageFetch(env) {
19
+ return async (url) => {
20
+ return env.ASSETS.fetch(url.replace(/\.html$/, ""));
21
+ };
22
+ }
23
+ function createLocals(ctx) {
24
+ const locals = {
25
+ cfContext: ctx
26
+ };
27
+ Object.defineProperty(locals, "runtime", {
28
+ enumerable: false,
29
+ value: {
30
+ get env() {
31
+ throw new Error(
32
+ `Astro.locals.runtime.env has been removed in Astro v6. Use 'import { env } from "cloudflare:workers"' instead.`
33
+ );
34
+ },
35
+ get cf() {
36
+ throw new Error(
37
+ `Astro.locals.runtime.cf has been removed in Astro v6. Use 'Astro.request.cf' instead.`
38
+ );
39
+ },
40
+ get caches() {
41
+ throw new Error(
42
+ `Astro.locals.runtime.caches has been removed in Astro v6. Use the global 'caches' object instead.`
43
+ );
44
+ },
45
+ get ctx() {
46
+ throw new Error(
47
+ `Astro.locals.runtime.ctx has been removed in Astro v6. Use 'Astro.locals.cfContext' instead.`
48
+ );
49
+ }
50
+ }
51
+ });
52
+ return locals;
53
+ }
54
+ function getClientAddress(request) {
55
+ return getValidatedIpFromHeader(request.headers.get("cf-connecting-ip"));
56
+ }
57
+ export {
58
+ createErrorPageFetch,
59
+ createLocals,
60
+ fallbackToAssets,
61
+ getClientAddress,
62
+ matchStaticAsset
63
+ };
@@ -0,0 +1,8 @@
1
+ import type { ManifestLike } from './cf-helpers.js';
2
+ export type { Runtime, ManifestLike } from './cf-helpers.js';
3
+ export { matchStaticAsset, fallbackToAssets, createErrorPageFetch, createLocals, getClientAddress, } from './cf-helpers.js';
4
+ /**
5
+ * Injects the SESSION KV binding into the app manifest's session config.
6
+ * Idempotent — safe to call on every request.
7
+ */
8
+ export declare function injectSessionBinding(manifest: ManifestLike, env: Env): void;
@@ -0,0 +1,24 @@
1
+ import { sessionKVBindingName } from "virtual:astro-cloudflare:config";
2
+ import {
3
+ matchStaticAsset,
4
+ fallbackToAssets,
5
+ createErrorPageFetch,
6
+ createLocals,
7
+ getClientAddress
8
+ } from "./cf-helpers.js";
9
+ function injectSessionBinding(manifest, env) {
10
+ if (env[sessionKVBindingName]) {
11
+ const sessionConfigOptions = manifest.sessionConfig?.options ?? {};
12
+ Object.assign(sessionConfigOptions, {
13
+ binding: env[sessionKVBindingName]
14
+ });
15
+ }
16
+ }
17
+ export {
18
+ createErrorPageFetch,
19
+ createLocals,
20
+ fallbackToAssets,
21
+ getClientAddress,
22
+ injectSessionBinding,
23
+ matchStaticAsset
24
+ };
@@ -1,9 +1,7 @@
1
- export interface Runtime {
2
- cfContext: ExecutionContext;
3
- }
1
+ import { type Runtime } from './cf.js';
2
+ export type { Runtime };
4
3
  declare global {
5
4
  var __ASTRO_IMAGES_BINDING_NAME: string;
6
5
  }
7
6
  type CfResponse = Awaited<ReturnType<Required<ExportedHandler<Env>>['fetch']>>;
8
7
  export declare function handle(request: Request, env: Env, context: ExecutionContext): Promise<CfResponse>;
9
- export {};
@@ -1,9 +1,5 @@
1
1
  import { env as globalEnv } from "cloudflare:workers";
2
- import {
3
- sessionKVBindingName,
4
- compileImageConfig,
5
- isPrerender
6
- } from "virtual:astro-cloudflare:config";
2
+ import { compileImageConfig, isPrerender } from "virtual:astro-cloudflare:config";
7
3
  import { createApp } from "astro/app/entrypoint";
8
4
  import { setGetEnv } from "astro/env/setup";
9
5
  import { createGetEnv } from "../utils/env.js";
@@ -15,7 +11,14 @@ import {
15
11
  isStaticImagesRequest,
16
12
  handleStaticImagesRequest
17
13
  } from "./prerender.js";
18
- import { getValidatedIpFromHeader } from "@astrojs/internal-helpers/request";
14
+ import {
15
+ injectSessionBinding,
16
+ matchStaticAsset,
17
+ fallbackToAssets,
18
+ createErrorPageFetch,
19
+ createLocals,
20
+ getClientAddress
21
+ } from "./cf.js";
19
22
  setGetEnv(createGetEnv(globalEnv));
20
23
  const app = createApp();
21
24
  async function handle(request, env, context) {
@@ -34,16 +37,9 @@ async function handle(request, env, context) {
34
37
  return handleStaticImagesRequest();
35
38
  }
36
39
  }
37
- const { pathname: requestPathname } = new URL(request.url);
38
- if (env[sessionKVBindingName]) {
39
- const sessionConfigOptions = app.manifest.sessionConfig?.options ?? {};
40
- Object.assign(sessionConfigOptions, {
41
- binding: env[sessionKVBindingName]
42
- });
43
- }
44
- if (app.manifest.assets.has(requestPathname)) {
45
- return env.ASSETS.fetch(request.url.replace(/\.html$/, ""));
46
- }
40
+ injectSessionBinding(app.manifest, env);
41
+ const staticAsset = matchStaticAsset(app.manifest, request.url, env);
42
+ if (staticAsset) return staticAsset;
47
43
  let routeData = void 0;
48
44
  if (app.isDev()) {
49
45
  const result = await app.devMatch(app.getPathnameFromRequest(request));
@@ -54,50 +50,17 @@ async function handle(request, env, context) {
54
50
  routeData = app.match(request);
55
51
  }
56
52
  if (!routeData) {
57
- const asset = await env.ASSETS.fetch(
58
- request.url.replace(/index.html$/, "").replace(/\.html$/, "")
59
- );
60
- if (asset.status !== 404) {
61
- return asset;
62
- }
53
+ const asset = await fallbackToAssets(request.url, env);
54
+ if (asset) return asset;
63
55
  }
64
- const locals = {
65
- cfContext: context
66
- };
67
- Object.defineProperty(locals, "runtime", {
68
- enumerable: false,
69
- value: {
70
- get env() {
71
- throw new Error(
72
- `Astro.locals.runtime.env has been removed in Astro v6. Use 'import { env } from "cloudflare:workers"' instead.`
73
- );
74
- },
75
- get cf() {
76
- throw new Error(
77
- `Astro.locals.runtime.cf has been removed in Astro v6. Use 'Astro.request.cf' instead.`
78
- );
79
- },
80
- get caches() {
81
- throw new Error(
82
- `Astro.locals.runtime.caches has been removed in Astro v6. Use the global 'caches' object instead.`
83
- );
84
- },
85
- get ctx() {
86
- throw new Error(
87
- `Astro.locals.runtime.ctx has been removed in Astro v6. Use 'Astro.locals.cfContext' instead.`
88
- );
89
- }
90
- }
91
- });
56
+ const locals = createLocals(context);
92
57
  const waitUntil = context.waitUntil.bind(context);
93
58
  const response = await app.render(request, {
94
59
  routeData,
95
60
  locals,
96
61
  waitUntil,
97
- prerenderedErrorPageFetch: async (url) => {
98
- return env.ASSETS.fetch(url.replace(/\.html$/, ""));
99
- },
100
- clientAddress: getValidatedIpFromHeader(request.headers.get("cf-connecting-ip"))
62
+ prerenderedErrorPageFetch: createErrorPageFetch(env),
63
+ clientAddress: getClientAddress(request)
101
64
  });
102
65
  if (app.setCookieHeaders) {
103
66
  for (const setCookieHeader of app.setCookieHeaders(response)) {
@@ -1,6 +1,7 @@
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
+ import { fetchWithRedirects } from "astro/assets";
4
5
  const qualityTable = {
5
6
  low: 25,
6
7
  mid: 50,
@@ -14,9 +15,21 @@ async function transform(rawUrl, images, assets) {
14
15
  return new Response("Forbidden", { status: 403 });
15
16
  }
16
17
  const imageSrc = new URL(href, url.origin);
17
- const content = await (isRemotePath(href) ? fetch(imageSrc, { redirect: "manual" }) : assets.fetch(imageSrc));
18
- if (content.status >= 300 && content.status < 400) {
19
- return new Response("Not Found", { status: 404 });
18
+ let content;
19
+ if (isRemotePath(href)) {
20
+ try {
21
+ content = await fetchWithRedirects({
22
+ url: imageSrc,
23
+ imageConfig
24
+ });
25
+ if (!isRemoteAllowed(content.url, imageConfig)) {
26
+ return new Response("Forbidden", { status: 403 });
27
+ }
28
+ } catch {
29
+ return new Response("Not Found", { status: 404 });
30
+ }
31
+ } else {
32
+ content = await assets.fetch(imageSrc);
20
33
  }
21
34
  if (!content.body) {
22
35
  return new Response(null, { status: 404 });
@@ -16,6 +16,7 @@ export declare function setImageConfig(service: ImageServiceConfig | undefined,
16
16
  endpoint: {
17
17
  entrypoint: string;
18
18
  };
19
+ dangerouslyProcessSVG: boolean;
19
20
  domains: string[];
20
21
  remotePatterns: {
21
22
  protocol?: string | undefined;
@@ -34,6 +35,7 @@ export declare function setImageConfig(service: ImageServiceConfig | undefined,
34
35
  route: string;
35
36
  entrypoint?: string | undefined;
36
37
  };
38
+ dangerouslyProcessSVG: boolean;
37
39
  domains: string[];
38
40
  remotePatterns: {
39
41
  protocol?: string | undefined;
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from 'vite';
2
+ export default function cfPrismPlugin(): Plugin;
@@ -0,0 +1,56 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import components from "prismjs/components.js";
3
+ const MODULE_ID = "virtual:astro-cloudflare:prism";
4
+ const RESOLVED_MODULE_ID = "\0" + MODULE_ID;
5
+ const languages = Object.keys(components.languages).filter((l) => l !== "meta");
6
+ function cfPrismPlugin() {
7
+ return {
8
+ name: "@astrojs/cloudflare:prism",
9
+ configEnvironment(environmentName) {
10
+ if (environmentName === "ssr") {
11
+ return {
12
+ // Because this virtual module adds a large number of dynamic import statements,
13
+ // Vite’s logs will consequently display the message “new dependencies optimized” for all languages.
14
+ // To avoid this, we explicitly specify that the module should be optimized in advance.
15
+ optimizeDeps: {
16
+ include: ["prismjs/components/prism-*.js"]
17
+ }
18
+ };
19
+ }
20
+ },
21
+ resolveId: {
22
+ filter: {
23
+ id: new RegExp(`^${MODULE_ID}$`)
24
+ },
25
+ handler() {
26
+ return RESOLVED_MODULE_ID;
27
+ }
28
+ },
29
+ load: {
30
+ filter: {
31
+ id: new RegExp(`^${RESOLVED_MODULE_ID}$`)
32
+ },
33
+ async handler() {
34
+ const importerPath = fileURLToPath(import.meta.url);
35
+ const resolvedModules = await Promise.all(
36
+ languages.map(async (lang) => {
37
+ const resolvedId = await this.resolve(
38
+ `prismjs/components/prism-${lang}.js`,
39
+ importerPath
40
+ );
41
+ return { resolvedId: resolvedId?.id, lang };
42
+ })
43
+ );
44
+ const prismBundledLanguages = resolvedModules.filter(({ resolvedId }) => resolvedId !== void 0).map(
45
+ ({ resolvedId, lang }) => `${JSON.stringify(lang)}: () => import(${JSON.stringify(resolvedId)})`
46
+ );
47
+ return `
48
+ export const bundledLanguages = { ${prismBundledLanguages.join(",")} };
49
+ `;
50
+ }
51
+ }
52
+ };
53
+ }
54
+ export {
55
+ cfPrismPlugin as default
56
+ };
package/dist/wrangler.js CHANGED
@@ -13,11 +13,7 @@ function cloudflareConfigCustomizer(options) {
13
13
  );
14
14
  const hasImagesBinding = nonInheritableConfig?.images?.binding !== void 0;
15
15
  return {
16
- kv_namespaces: !needsSessionKVBinding || hasSessionBinding ? void 0 : [
17
- {
18
- binding: sessionKVBindingName
19
- }
20
- ],
16
+ kv_namespaces: !needsSessionKVBinding || hasSessionBinding ? void 0 : [{ binding: sessionKVBindingName }],
21
17
  images: hasImagesBinding || !imagesBindingName ? void 0 : {
22
18
  binding: imagesBindingName
23
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": "14.0.0-alpha.0",
4
+ "version": "14.0.0-alpha.1",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -27,6 +27,8 @@
27
27
  "./image-passthrough-endpoint": "./dist/entrypoints/image-passthrough-endpoint.js",
28
28
  "./image-service-workerd": "./dist/entrypoints/image-service-workerd.js",
29
29
  "./handler": "./dist/utils/handler.js",
30
+ "./fetch": "./dist/fetch.js",
31
+ "./hono": "./dist/hono.js",
30
32
  "./types.d.ts": "./types.d.ts",
31
33
  "./package.json": "./package.json"
32
34
  },
@@ -35,23 +37,25 @@
35
37
  "types.d.ts"
36
38
  ],
37
39
  "dependencies": {
38
- "@cloudflare/vite-plugin": "^1.32.3",
40
+ "@cloudflare/vite-plugin": "^1.39.0",
39
41
  "piccolore": "^0.1.3",
40
42
  "tinyglobby": "^0.2.15",
41
- "vite": "^8.0.8",
42
- "@astrojs/internal-helpers": "0.9.0",
43
+ "vite": "^8.0.13",
44
+ "@astrojs/internal-helpers": "0.10.0",
43
45
  "@astrojs/underscore-redirects": "1.0.3"
44
46
  },
45
47
  "peerDependencies": {
46
- "astro": "^7.0.0-alpha.0",
48
+ "astro": "^7.0.0-alpha.2",
47
49
  "wrangler": "^4.83.0"
48
50
  },
49
51
  "devDependencies": {
50
- "@cloudflare/workers-types": "^4.20260228.0",
51
- "@types/node": "^25.2.2",
52
+ "@cloudflare/workers-types": "^4.20260526.1",
53
+ "@types/node": "^22.10.6",
54
+ "@types/prismjs": "1.26.6",
52
55
  "cheerio": "1.2.0",
53
- "devalue": "^5.6.3",
54
- "astro": "7.0.0-alpha.0",
56
+ "devalue": "^5.8.1",
57
+ "prismjs": "^1.30.0",
58
+ "astro": "7.0.0-alpha.2",
55
59
  "astro-scripts": "0.0.14"
56
60
  },
57
61
  "publishConfig": {
@@ -59,9 +63,8 @@
59
63
  },
60
64
  "scripts": {
61
65
  "dev": "astro-scripts dev \"src/**/*.ts\"",
62
- "build": "astro-scripts build \"src/**/*.ts\" --clean-dts && tsc",
66
+ "build": "astro-scripts build \"src/**/*.ts\" --clean-dts && tsc -b",
63
67
  "build:ci": "astro-scripts build \"src/**/*.ts\"",
64
- "test": "astro-scripts test --force-exit \"test/**/*.test.ts\"",
65
- "typecheck:tests": "tsc --build tsconfig.test.json"
68
+ "test": "astro-scripts test --force-exit \"test/**/*.test.ts\""
66
69
  }
67
70
  }